tlc-claude-code 0.6.2 → 0.6.3

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 (35) hide show
  1. package/dashboard/dist/App.d.ts +5 -0
  2. package/dashboard/dist/App.js +49 -0
  3. package/dashboard/dist/App.test.d.ts +1 -0
  4. package/dashboard/dist/App.test.js +137 -0
  5. package/dashboard/dist/components/AgentsPane.d.ts +18 -0
  6. package/dashboard/dist/components/AgentsPane.js +77 -0
  7. package/dashboard/dist/components/AgentsPane.test.d.ts +1 -0
  8. package/dashboard/dist/components/AgentsPane.test.js +52 -0
  9. package/dashboard/dist/components/ChatPane.d.ts +6 -0
  10. package/dashboard/dist/components/ChatPane.js +34 -0
  11. package/dashboard/dist/components/ChatPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/ChatPane.test.js +36 -0
  13. package/dashboard/dist/components/GitHubPane.d.ts +16 -0
  14. package/dashboard/dist/components/GitHubPane.js +121 -0
  15. package/dashboard/dist/components/GitHubPane.test.d.ts +1 -0
  16. package/dashboard/dist/components/GitHubPane.test.js +79 -0
  17. package/dashboard/dist/components/PhasesPane.d.ts +8 -0
  18. package/dashboard/dist/components/PhasesPane.js +65 -0
  19. package/dashboard/dist/components/PhasesPane.test.d.ts +1 -0
  20. package/dashboard/dist/components/PhasesPane.test.js +119 -0
  21. package/dashboard/dist/components/PlanSync.d.ts +13 -0
  22. package/dashboard/dist/components/PlanSync.js +89 -0
  23. package/dashboard/dist/components/PlanSync.test.d.ts +1 -0
  24. package/dashboard/dist/components/PlanSync.test.js +117 -0
  25. package/dashboard/dist/components/PreviewPane.d.ts +6 -0
  26. package/dashboard/dist/components/PreviewPane.js +43 -0
  27. package/dashboard/dist/components/PreviewPane.test.d.ts +1 -0
  28. package/dashboard/dist/components/PreviewPane.test.js +36 -0
  29. package/dashboard/dist/components/StatusPane.d.ts +1 -0
  30. package/dashboard/dist/components/StatusPane.js +32 -0
  31. package/dashboard/dist/components/StatusPane.test.d.ts +1 -0
  32. package/dashboard/dist/components/StatusPane.test.js +51 -0
  33. package/dashboard/dist/index.d.ts +2 -0
  34. package/dashboard/dist/index.js +13 -0
  35. package/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ interface AppProps {
2
+ isTTY?: boolean;
3
+ }
4
+ export declare function App({ isTTY }: AppProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import { useState, useCallback } from 'react';
4
+ import { ChatPane } from './components/ChatPane.js';
5
+ import { StatusPane } from './components/StatusPane.js';
6
+ import { PreviewPane } from './components/PreviewPane.js';
7
+ import { AgentsPane } from './components/AgentsPane.js';
8
+ import { GitHubPane } from './components/GitHubPane.js';
9
+ import { markIssueComplete, markIssueInProgress } from './components/PlanSync.js';
10
+ export function App({ isTTY = true }) {
11
+ const { exit } = useApp();
12
+ const [activePane, setActivePane] = useState('chat');
13
+ const [pendingTasks, setPendingTasks] = useState(new Map());
14
+ useInput((input, key) => {
15
+ if (input === 'q' && key.ctrl) {
16
+ exit();
17
+ }
18
+ if (key.tab) {
19
+ setActivePane(prev => {
20
+ const panes = ['chat', 'github', 'agents', 'preview'];
21
+ const idx = panes.indexOf(prev);
22
+ return panes[(idx + 1) % panes.length];
23
+ });
24
+ }
25
+ // Number keys to quick-switch panes
26
+ if (input === '1')
27
+ setActivePane('chat');
28
+ if (input === '2')
29
+ setActivePane('github');
30
+ if (input === '3')
31
+ setActivePane('agents');
32
+ if (input === '4')
33
+ setActivePane('preview');
34
+ }, { isActive: isTTY });
35
+ const handleAssignToAgent = useCallback(async (issue) => {
36
+ await markIssueInProgress(issue.number);
37
+ setPendingTasks(prev => new Map(prev).set(issue.number, issue.title));
38
+ // Agent assignment happens in AgentsPane
39
+ }, []);
40
+ const handleTaskComplete = useCallback(async (issueNumber) => {
41
+ await markIssueComplete(issueNumber);
42
+ setPendingTasks(prev => {
43
+ const next = new Map(prev);
44
+ next.delete(issueNumber);
45
+ return next;
46
+ });
47
+ }, []);
48
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsxs(Box, { borderStyle: "single", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "TDD Dashboard" }), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: activePane === 'chat' ? 'cyan' : 'gray', children: "[1]Chat " }), _jsx(Text, { color: activePane === 'github' ? 'cyan' : 'gray', children: "[2]GitHub " }), _jsx(Text, { color: activePane === 'agents' ? 'cyan' : 'gray', children: "[3]Agents " }), _jsx(Text, { color: activePane === 'preview' ? 'cyan' : 'gray', children: "[4]Preview" })] }), _jsx(Box, { children: _jsx(Text, { color: "cyan", bold: true, children: "| TDD |" }) })] }), _jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", width: "60%", borderStyle: "single", borderColor: activePane === 'chat' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'chat' ? 'cyan' : 'white', children: "Chat" }) }), _jsx(ChatPane, { isActive: activePane === 'chat', isTTY: isTTY })] }), _jsxs(Box, { flexDirection: "column", width: "40%", children: [_jsxs(Box, { flexDirection: "column", height: "30%", borderStyle: "single", borderColor: activePane === 'github' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'github' ? 'cyan' : 'white', children: "GitHub Issues" }) }), _jsx(GitHubPane, { isActive: activePane === 'github', isTTY: isTTY, onAssignToAgent: handleAssignToAgent })] }), _jsxs(Box, { flexDirection: "column", height: "30%", borderStyle: "single", borderColor: activePane === 'agents' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'agents' ? 'cyan' : 'white', children: "Agents" }) }), _jsx(AgentsPane, { isActive: activePane === 'agents', isTTY: isTTY, onTaskComplete: handleTaskComplete })] }), _jsxs(Box, { flexDirection: "column", height: "20%", borderStyle: "single", borderColor: "gray", children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, children: "Status" }) }), _jsx(StatusPane, {})] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: activePane === 'preview' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'preview' ? 'cyan' : 'white', children: "Preview" }) }), _jsx(PreviewPane, { isActive: activePane === 'preview', isTTY: isTTY })] })] })] }), _jsx(Box, { borderStyle: "single", paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Tab: cycle panes | 1-4: jump to pane | Ctrl+Q: quit" }) })] }));
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { App } from './App.js';
5
+ // Mock the PlanSync module
6
+ vi.mock('./components/PlanSync.js', () => ({
7
+ markIssueComplete: vi.fn().mockResolvedValue(undefined),
8
+ markIssueInProgress: vi.fn().mockResolvedValue(undefined),
9
+ }));
10
+ // Mock child_process for StatusPane
11
+ vi.mock('child_process', () => ({
12
+ exec: vi.fn((cmd, opts, cb) => {
13
+ if (typeof opts === 'function') {
14
+ cb = opts;
15
+ }
16
+ const error = new Error('command not found');
17
+ if (cb)
18
+ cb(error, '', '');
19
+ return { stdout: null, stderr: null };
20
+ }),
21
+ }));
22
+ vi.mock('util', async () => {
23
+ const actual = await vi.importActual('util');
24
+ return {
25
+ ...actual,
26
+ promisify: (fn) => async (...args) => {
27
+ return new Promise((resolve, reject) => {
28
+ fn(...args, (err, stdout, stderr) => {
29
+ if (err)
30
+ reject(err);
31
+ else
32
+ resolve({ stdout, stderr });
33
+ });
34
+ });
35
+ },
36
+ };
37
+ });
38
+ describe('App', () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ });
42
+ describe('initial render', () => {
43
+ it('renders without error', () => {
44
+ const { lastFrame } = render(_jsx(App, {}));
45
+ expect(lastFrame()).toBeDefined();
46
+ });
47
+ it('shows TDD Dashboard header', () => {
48
+ const { lastFrame } = render(_jsx(App, {}));
49
+ const output = lastFrame();
50
+ expect(output).toContain('TDD Dashboard');
51
+ });
52
+ it('shows all pane labels in header', () => {
53
+ const { lastFrame } = render(_jsx(App, {}));
54
+ const output = lastFrame();
55
+ expect(output).toContain('[1]Chat');
56
+ expect(output).toContain('[2]GitHub');
57
+ expect(output).toContain('[3]Agents');
58
+ expect(output).toContain('[4]Preview');
59
+ });
60
+ it('shows footer with keyboard hints', () => {
61
+ const { lastFrame } = render(_jsx(App, {}));
62
+ const output = lastFrame();
63
+ expect(output).toContain('Tab: cycle panes');
64
+ expect(output).toContain('Ctrl+Q: quit');
65
+ });
66
+ });
67
+ describe('pane sections', () => {
68
+ it('shows Chat pane', () => {
69
+ const { lastFrame } = render(_jsx(App, {}));
70
+ const output = lastFrame();
71
+ expect(output).toContain('Chat');
72
+ });
73
+ it('shows GitHub Issues pane', () => {
74
+ const { lastFrame } = render(_jsx(App, {}));
75
+ const output = lastFrame();
76
+ expect(output).toContain('GitHub Issues');
77
+ });
78
+ it('shows Agents pane', () => {
79
+ const { lastFrame } = render(_jsx(App, {}));
80
+ const output = lastFrame();
81
+ expect(output).toContain('Agents');
82
+ });
83
+ it('shows Status pane', () => {
84
+ const { lastFrame } = render(_jsx(App, {}));
85
+ const output = lastFrame();
86
+ expect(output).toContain('Status');
87
+ });
88
+ it('shows Preview pane', () => {
89
+ const { lastFrame } = render(_jsx(App, {}));
90
+ const output = lastFrame();
91
+ expect(output).toContain('Preview');
92
+ });
93
+ });
94
+ describe('keyboard navigation', () => {
95
+ it('switches to github pane when pressing 2', () => {
96
+ const { lastFrame, stdin } = render(_jsx(App, {}));
97
+ stdin.write('2');
98
+ const output = lastFrame();
99
+ // GitHub pane should now be active (shown by highlighting)
100
+ expect(output).toContain('GitHub');
101
+ });
102
+ it('switches to agents pane when pressing 3', () => {
103
+ const { lastFrame, stdin } = render(_jsx(App, {}));
104
+ stdin.write('3');
105
+ const output = lastFrame();
106
+ expect(output).toContain('Agents');
107
+ });
108
+ it('switches to preview pane when pressing 4', () => {
109
+ const { lastFrame, stdin } = render(_jsx(App, {}));
110
+ stdin.write('4');
111
+ const output = lastFrame();
112
+ expect(output).toContain('Preview');
113
+ });
114
+ it('switches back to chat pane when pressing 1', () => {
115
+ const { lastFrame, stdin } = render(_jsx(App, {}));
116
+ stdin.write('2'); // switch away
117
+ stdin.write('1'); // switch back
118
+ const output = lastFrame();
119
+ expect(output).toContain('Chat');
120
+ });
121
+ it('cycles panes with tab', () => {
122
+ const { lastFrame, stdin } = render(_jsx(App, {}));
123
+ // Tab key
124
+ stdin.write('\t');
125
+ const output = lastFrame();
126
+ // Should have cycled to next pane
127
+ expect(output).toBeDefined();
128
+ });
129
+ });
130
+ describe('TDD branding', () => {
131
+ it('shows TDD indicator in header', () => {
132
+ const { lastFrame } = render(_jsx(App, {}));
133
+ const output = lastFrame();
134
+ expect(output).toContain('TDD');
135
+ });
136
+ });
137
+ });
@@ -0,0 +1,18 @@
1
+ import { ChildProcess } from 'child_process';
2
+ type AgentStatus = 'idle' | 'working' | 'done' | 'error';
3
+ interface Agent {
4
+ id: number;
5
+ status: AgentStatus;
6
+ task: string | null;
7
+ issueNumber: number | null;
8
+ output: string[];
9
+ process: ChildProcess | null;
10
+ }
11
+ interface AgentsPaneProps {
12
+ isActive: boolean;
13
+ isTTY?: boolean;
14
+ onTaskComplete?: (issueNumber: number) => void;
15
+ }
16
+ export declare function AgentsPane({ isActive, isTTY, onTaskComplete }: AgentsPaneProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function getIdleAgent(agents: Agent[]): Agent | undefined;
18
+ export {};
@@ -0,0 +1,77 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useState, useCallback } from 'react';
4
+ import Spinner from 'ink-spinner';
5
+ import { spawn } from 'child_process';
6
+ const MAX_AGENTS = 3;
7
+ export function AgentsPane({ isActive, isTTY = true, onTaskComplete }) {
8
+ const [agents, setAgents] = useState([
9
+ { id: 1, status: 'idle', task: null, issueNumber: null, output: [], process: null },
10
+ { id: 2, status: 'idle', task: null, issueNumber: null, output: [], process: null },
11
+ { id: 3, status: 'idle', task: null, issueNumber: null, output: [], process: null },
12
+ ]);
13
+ const assignTask = useCallback((agentId, task, issueNumber) => {
14
+ setAgents(prev => prev.map(agent => {
15
+ if (agent.id !== agentId)
16
+ return agent;
17
+ // Spawn Claude Code process for this task
18
+ const proc = spawn('claude', ['-p', `Work on task: ${task}. When done, output TASK_COMPLETE.`], {
19
+ cwd: process.cwd(),
20
+ stdio: ['pipe', 'pipe', 'pipe']
21
+ });
22
+ const output = [];
23
+ proc.stdout?.on('data', (data) => {
24
+ const text = data.toString();
25
+ output.push(text);
26
+ // Check if task is complete
27
+ if (text.includes('TASK_COMPLETE')) {
28
+ setAgents(prev => prev.map(a => a.id === agentId ? { ...a, status: 'done', output } : a));
29
+ onTaskComplete?.(issueNumber);
30
+ }
31
+ });
32
+ proc.stderr?.on('data', (data) => {
33
+ output.push(`[ERROR] ${data.toString()}`);
34
+ });
35
+ proc.on('close', (code) => {
36
+ setAgents(prev => prev.map(a => a.id === agentId ? {
37
+ ...a,
38
+ status: code === 0 ? 'done' : 'error',
39
+ process: null
40
+ } : a));
41
+ });
42
+ return {
43
+ ...agent,
44
+ status: 'working',
45
+ task,
46
+ issueNumber,
47
+ output: [],
48
+ process: proc
49
+ };
50
+ }));
51
+ }, [onTaskComplete]);
52
+ const stopAgent = useCallback((agentId) => {
53
+ setAgents(prev => prev.map(agent => {
54
+ if (agent.id !== agentId)
55
+ return agent;
56
+ agent.process?.kill();
57
+ return { ...agent, status: 'idle', task: null, issueNumber: null, process: null };
58
+ }));
59
+ }, []);
60
+ useInput((input, key) => {
61
+ if (!isActive)
62
+ return;
63
+ // 1, 2, 3 to select agent
64
+ // s to stop selected agent
65
+ const agentNum = parseInt(input);
66
+ if (agentNum >= 1 && agentNum <= 3) {
67
+ const agent = agents[agentNum - 1];
68
+ if (agent.status === 'working') {
69
+ stopAgent(agentNum);
70
+ }
71
+ }
72
+ }, { isActive: isTTY });
73
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Agents (", agents.filter(a => a.status === 'working').length, "/", MAX_AGENTS, " active)"] }) }), agents.map((agent) => (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "gray", children: ["[", agent.id, "] "] }), agent.status === 'idle' && (_jsx(Text, { color: "gray", children: "Idle" })), agent.status === 'working' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "cyan", children: [" #", agent.issueNumber, ": "] }), _jsxs(Text, { children: [agent.task?.slice(0, 30), "..."] })] })), agent.status === 'done' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", children: "Done " }), _jsxs(Text, { color: "gray", children: ["#", agent.issueNumber] })] })), agent.status === 'error' && (_jsx(Text, { color: "red", children: "Error" }))] }, agent.id))), isActive && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[1-3] Stop agent" }) }))] }));
74
+ }
75
+ export function getIdleAgent(agents) {
76
+ return agents.find(a => a.status === 'idle');
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
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 { AgentsPane, getIdleAgent } from './AgentsPane.js';
5
+ describe('AgentsPane', () => {
6
+ describe('getIdleAgent', () => {
7
+ it('returns first idle agent', () => {
8
+ const agents = [
9
+ { id: 1, status: 'working', task: 'task1', issueNumber: 1, output: [], process: null },
10
+ { id: 2, status: 'idle', task: null, issueNumber: null, output: [], process: null },
11
+ { id: 3, status: 'idle', task: null, issueNumber: null, output: [], process: null },
12
+ ];
13
+ const idle = getIdleAgent(agents);
14
+ expect(idle?.id).toBe(2);
15
+ });
16
+ it('returns undefined when all agents busy', () => {
17
+ const agents = [
18
+ { id: 1, status: 'working', task: 'task1', issueNumber: 1, output: [], process: null },
19
+ { id: 2, status: 'working', task: 'task2', issueNumber: 2, output: [], process: null },
20
+ { id: 3, status: 'done', task: 'task3', issueNumber: 3, output: [], process: null },
21
+ ];
22
+ const idle = getIdleAgent(agents);
23
+ expect(idle).toBeUndefined();
24
+ });
25
+ it('returns undefined for empty array', () => {
26
+ const idle = getIdleAgent([]);
27
+ expect(idle).toBeUndefined();
28
+ });
29
+ });
30
+ describe('component rendering', () => {
31
+ it('renders with all agents idle', () => {
32
+ const { lastFrame } = render(_jsx(AgentsPane, { isActive: false }));
33
+ const output = lastFrame();
34
+ expect(output).toContain('Agents');
35
+ expect(output).toContain('0/3 active');
36
+ expect(output).toContain('[1]');
37
+ expect(output).toContain('[2]');
38
+ expect(output).toContain('[3]');
39
+ expect(output).toContain('Idle');
40
+ });
41
+ it('shows controls when active', () => {
42
+ const { lastFrame } = render(_jsx(AgentsPane, { isActive: true }));
43
+ const output = lastFrame();
44
+ expect(output).toContain('[1-3] Stop agent');
45
+ });
46
+ it('hides controls when inactive', () => {
47
+ const { lastFrame } = render(_jsx(AgentsPane, { isActive: false }));
48
+ const output = lastFrame();
49
+ expect(output).not.toContain('[1-3] Stop agent');
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,6 @@
1
+ interface ChatPaneProps {
2
+ isActive: boolean;
3
+ isTTY?: boolean;
4
+ }
5
+ export declare function ChatPane({ isActive, isTTY }: ChatPaneProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,34 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { useState, useCallback } from 'react';
5
+ export function ChatPane({ isActive, isTTY = true }) {
6
+ const [messages, setMessages] = useState([
7
+ { role: 'system', content: 'TDD Dashboard ready. Type a message to start.' }
8
+ ]);
9
+ const [input, setInput] = useState('');
10
+ const [isLoading, setIsLoading] = useState(false);
11
+ const sendMessage = useCallback(async (text) => {
12
+ if (!text.trim() || isLoading)
13
+ return;
14
+ // Add user message
15
+ setMessages(prev => [...prev, { role: 'user', content: text }]);
16
+ setInput('');
17
+ setIsLoading(true);
18
+ // TODO: Integrate with Claude Code CLI
19
+ // For now, simulate a response
20
+ setTimeout(() => {
21
+ setMessages(prev => [...prev, {
22
+ role: 'assistant',
23
+ content: `Received: "${text}"\n\nClaude Code integration coming soon...`
24
+ }]);
25
+ setIsLoading(false);
26
+ }, 500);
27
+ }, [isLoading]);
28
+ const handleSubmit = useCallback((value) => {
29
+ sendMessage(value);
30
+ }, [sendMessage]);
31
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", children: [messages.slice(-10).map((msg, i) => (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: msg.role === 'user' ? 'green' :
32
+ msg.role === 'assistant' ? 'cyan' :
33
+ 'gray', children: [msg.role === 'user' ? '> ' : msg.role === 'assistant' ? ' ' : '# ', msg.content] }) }, i))), isLoading && (_jsx(Text, { color: "yellow", children: "Thinking..." }))] }), _jsxs(Box, { borderStyle: "round", borderColor: isActive ? 'green' : 'gray', paddingX: 1, children: [_jsx(Text, { color: "green", children: "> " }), isTTY ? (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, focus: isActive, placeholder: "Type a message..." })) : (_jsx(Text, { color: "gray", children: "Input disabled (no TTY)" }))] })] }));
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
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 { ChatPane } from './ChatPane.js';
5
+ describe('ChatPane', () => {
6
+ describe('initial render', () => {
7
+ it('shows welcome message', () => {
8
+ const { lastFrame } = render(_jsx(ChatPane, { isActive: false }));
9
+ const output = lastFrame();
10
+ expect(output).toContain('TDD Dashboard ready');
11
+ });
12
+ it('shows input prompt', () => {
13
+ const { lastFrame } = render(_jsx(ChatPane, { isActive: false }));
14
+ const output = lastFrame();
15
+ expect(output).toContain('>');
16
+ });
17
+ });
18
+ describe('active state', () => {
19
+ it('renders without error when active', () => {
20
+ const { lastFrame } = render(_jsx(ChatPane, { isActive: true }));
21
+ expect(lastFrame()).toBeDefined();
22
+ });
23
+ it('renders without error when inactive', () => {
24
+ const { lastFrame } = render(_jsx(ChatPane, { isActive: false }));
25
+ expect(lastFrame()).toBeDefined();
26
+ });
27
+ });
28
+ describe('message display', () => {
29
+ it('shows system messages with # prefix', () => {
30
+ const { lastFrame } = render(_jsx(ChatPane, { isActive: false }));
31
+ const output = lastFrame();
32
+ // System message has # prefix
33
+ expect(output).toContain('#');
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,16 @@
1
+ interface Issue {
2
+ number: number;
3
+ title: string;
4
+ state: 'open' | 'closed';
5
+ labels: string[];
6
+ assignee: string | null;
7
+ }
8
+ interface GitHubPaneProps {
9
+ isActive: boolean;
10
+ isTTY?: boolean;
11
+ onAssignToAgent?: (issue: Issue) => void;
12
+ }
13
+ export declare function GitHubPane({ isActive, isTTY, onAssignToAgent }: GitHubPaneProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function syncTaskToGitHub(title: string, body: string): Promise<number | null>;
15
+ export declare function markIssueComplete(number: number): Promise<void>;
16
+ export {};
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+ const execAsync = promisify(exec);
7
+ export function GitHubPane({ isActive, isTTY = true, onAssignToAgent }) {
8
+ const [issues, setIssues] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ const [error, setError] = useState(null);
11
+ const [selectedIndex, setSelectedIndex] = useState(0);
12
+ const fetchIssues = useCallback(async () => {
13
+ try {
14
+ // Use gh CLI to fetch issues
15
+ const { stdout } = await execAsync('gh issue list --label "tdd" --state open --json number,title,state,labels,assignee --limit 20', { cwd: process.cwd() });
16
+ const parsed = JSON.parse(stdout || '[]');
17
+ setIssues(parsed.map((i) => ({
18
+ number: i.number,
19
+ title: i.title,
20
+ state: i.state,
21
+ labels: i.labels?.map((l) => l.name) || [],
22
+ assignee: i.assignee?.login || null
23
+ })));
24
+ setError(null);
25
+ }
26
+ catch (e) {
27
+ // Try without label filter
28
+ try {
29
+ const { stdout } = await execAsync('gh issue list --state open --json number,title,state,labels,assignee --limit 10', { cwd: process.cwd() });
30
+ const parsed = JSON.parse(stdout || '[]');
31
+ setIssues(parsed.map((i) => ({
32
+ number: i.number,
33
+ title: i.title,
34
+ state: i.state,
35
+ labels: i.labels?.map((l) => l.name) || [],
36
+ assignee: i.assignee?.login || null
37
+ })));
38
+ setError(null);
39
+ }
40
+ catch (e2) {
41
+ setError('gh CLI not available or not in a repo');
42
+ setIssues([]);
43
+ }
44
+ }
45
+ setLoading(false);
46
+ }, []);
47
+ useEffect(() => {
48
+ fetchIssues();
49
+ const interval = setInterval(fetchIssues, 30000); // Refresh every 30s
50
+ return () => clearInterval(interval);
51
+ }, [fetchIssues]);
52
+ const createIssue = useCallback(async (title, body) => {
53
+ try {
54
+ await execAsync(`gh issue create --title "${title}" --body "${body}" --label "tdd"`, { cwd: process.cwd() });
55
+ fetchIssues();
56
+ }
57
+ catch (e) {
58
+ setError('Failed to create issue');
59
+ }
60
+ }, [fetchIssues]);
61
+ const closeIssue = useCallback(async (number) => {
62
+ try {
63
+ await execAsync(`gh issue close ${number}`, { cwd: process.cwd() });
64
+ fetchIssues();
65
+ }
66
+ catch (e) {
67
+ setError('Failed to close issue');
68
+ }
69
+ }, [fetchIssues]);
70
+ useInput((input, key) => {
71
+ if (!isActive)
72
+ return;
73
+ if (key.upArrow && selectedIndex > 0) {
74
+ setSelectedIndex(prev => prev - 1);
75
+ }
76
+ if (key.downArrow && selectedIndex < issues.length - 1) {
77
+ setSelectedIndex(prev => prev + 1);
78
+ }
79
+ if (input === 'a' && issues[selectedIndex]) {
80
+ onAssignToAgent?.(issues[selectedIndex]);
81
+ }
82
+ if (input === 'c' && issues[selectedIndex]) {
83
+ closeIssue(issues[selectedIndex].number);
84
+ }
85
+ if (input === 'r') {
86
+ setLoading(true);
87
+ fetchIssues();
88
+ }
89
+ }, { isActive: isTTY });
90
+ if (loading) {
91
+ return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "Loading issues..." }) }));
92
+ }
93
+ if (error) {
94
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "red", children: error }), _jsx(Text, { dimColor: true, children: "Make sure gh CLI is installed and authenticated." })] }));
95
+ }
96
+ if (issues.length === 0) {
97
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "No open issues." }), _jsx(Text, { dimColor: true, children: "Create issues with 'gh issue create'" })] }));
98
+ }
99
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [issues.slice(0, 6).map((issue, idx) => (_jsxs(Box, { children: [_jsx(Text, { color: idx === selectedIndex && isActive ? 'cyan' : 'white', children: idx === selectedIndex && isActive ? '> ' : ' ' }), _jsxs(Text, { color: "green", children: ["#", issue.number, " "] }), _jsxs(Text, { children: [issue.title.slice(0, 35), issue.title.length > 35 ? '...' : ''] }), issue.labels.includes('in-progress') && (_jsx(Text, { color: "yellow", children: " [WIP]" }))] }, issue.number))), isActive && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "[a] Assign to agent | [c] Close | [r] Refresh" }) }))] }));
100
+ }
101
+ // Helper to sync a task to GitHub
102
+ export async function syncTaskToGitHub(title, body) {
103
+ try {
104
+ const { stdout } = await promisify(exec)(`gh issue create --title "${title}" --body "${body}" --label "tdd" --json number`, { cwd: process.cwd() });
105
+ const parsed = JSON.parse(stdout);
106
+ return parsed.number;
107
+ }
108
+ catch (e) {
109
+ return null;
110
+ }
111
+ }
112
+ export async function markIssueComplete(number) {
113
+ try {
114
+ await promisify(exec)(`gh issue close ${number} --comment "Completed by TDD agent"`, {
115
+ cwd: process.cwd()
116
+ });
117
+ }
118
+ catch (e) {
119
+ // Ignore errors
120
+ }
121
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { GitHubPane } from './GitHubPane.js';
5
+ // Mock child_process to avoid actual gh calls
6
+ vi.mock('child_process', () => ({
7
+ exec: vi.fn((cmd, opts, cb) => {
8
+ if (typeof opts === 'function') {
9
+ cb = opts;
10
+ }
11
+ // Simulate gh CLI not available by default
12
+ const error = new Error('gh not found');
13
+ if (cb)
14
+ cb(error, '', '');
15
+ return { stdout: null, stderr: null };
16
+ }),
17
+ }));
18
+ vi.mock('util', async () => {
19
+ const actual = await vi.importActual('util');
20
+ return {
21
+ ...actual,
22
+ promisify: (fn) => async (...args) => {
23
+ return new Promise((resolve, reject) => {
24
+ fn(...args, (err, stdout, stderr) => {
25
+ if (err)
26
+ reject(err);
27
+ else
28
+ resolve({ stdout, stderr });
29
+ });
30
+ });
31
+ },
32
+ };
33
+ });
34
+ describe('GitHubPane', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+ describe('loading state', () => {
39
+ it('shows loading message initially', () => {
40
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: false }));
41
+ const output = lastFrame();
42
+ expect(output).toContain('Loading issues...');
43
+ });
44
+ });
45
+ describe('error state', () => {
46
+ it('shows error when gh CLI fails', async () => {
47
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: false }));
48
+ // Wait for effect to run
49
+ await new Promise(resolve => setTimeout(resolve, 100));
50
+ const output = lastFrame();
51
+ expect(output).toContain('gh CLI not available');
52
+ });
53
+ it('shows install hint on error', async () => {
54
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: false }));
55
+ await new Promise(resolve => setTimeout(resolve, 100));
56
+ const output = lastFrame();
57
+ expect(output).toContain('Make sure gh CLI is installed');
58
+ });
59
+ });
60
+ describe('controls', () => {
61
+ it('shows controls when active and has issues', async () => {
62
+ // This test would need proper mocking of successful gh response
63
+ // For now, we test that the component doesn't crash
64
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: true }));
65
+ await new Promise(resolve => setTimeout(resolve, 100));
66
+ const output = lastFrame();
67
+ // Either shows controls or error state
68
+ expect(output).toBeDefined();
69
+ });
70
+ });
71
+ describe('callback', () => {
72
+ it('accepts onAssignToAgent callback', () => {
73
+ const mockCallback = vi.fn();
74
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: true, onAssignToAgent: mockCallback }));
75
+ // Component should render without error
76
+ expect(lastFrame()).toBeDefined();
77
+ });
78
+ });
79
+ });