tlc-claude-code 1.2.28 → 1.3.0

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 (87) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  3. package/dashboard/dist/components/UsagePane.js +51 -0
  4. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  5. package/dashboard/dist/components/UsagePane.test.js +142 -0
  6. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  7. package/dashboard/dist/components/WorkspaceDocsPane.js +146 -0
  8. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  10. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  11. package/dashboard/dist/components/WorkspacePane.js +17 -0
  12. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  13. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  14. package/package.json +15 -4
  15. package/scripts/capture-screenshots.js +170 -0
  16. package/scripts/docs-update.js +253 -0
  17. package/scripts/generate-screenshots.js +321 -0
  18. package/scripts/project-docs.js +377 -0
  19. package/scripts/vps-setup.sh +477 -0
  20. package/server/lib/architecture-command.js +450 -0
  21. package/server/lib/architecture-command.test.js +754 -0
  22. package/server/lib/ast-analyzer.js +324 -0
  23. package/server/lib/ast-analyzer.test.js +437 -0
  24. package/server/lib/auth-system.test.js +4 -1
  25. package/server/lib/boundary-detector.js +427 -0
  26. package/server/lib/boundary-detector.test.js +320 -0
  27. package/server/lib/budget-alerts.js +138 -0
  28. package/server/lib/budget-alerts.test.js +235 -0
  29. package/server/lib/candidates-tracker.js +210 -0
  30. package/server/lib/candidates-tracker.test.js +300 -0
  31. package/server/lib/checkpoint-manager.js +251 -0
  32. package/server/lib/checkpoint-manager.test.js +474 -0
  33. package/server/lib/circular-detector.js +337 -0
  34. package/server/lib/circular-detector.test.js +353 -0
  35. package/server/lib/cohesion-analyzer.js +310 -0
  36. package/server/lib/cohesion-analyzer.test.js +447 -0
  37. package/server/lib/contract-testing.js +625 -0
  38. package/server/lib/contract-testing.test.js +342 -0
  39. package/server/lib/conversion-planner.js +469 -0
  40. package/server/lib/conversion-planner.test.js +361 -0
  41. package/server/lib/convert-command.js +351 -0
  42. package/server/lib/convert-command.test.js +608 -0
  43. package/server/lib/coupling-calculator.js +189 -0
  44. package/server/lib/coupling-calculator.test.js +509 -0
  45. package/server/lib/dependency-graph.js +367 -0
  46. package/server/lib/dependency-graph.test.js +516 -0
  47. package/server/lib/duplication-detector.js +349 -0
  48. package/server/lib/duplication-detector.test.js +401 -0
  49. package/server/lib/example-service.js +616 -0
  50. package/server/lib/example-service.test.js +397 -0
  51. package/server/lib/impact-scorer.js +184 -0
  52. package/server/lib/impact-scorer.test.js +211 -0
  53. package/server/lib/mermaid-generator.js +358 -0
  54. package/server/lib/mermaid-generator.test.js +301 -0
  55. package/server/lib/messaging-patterns.js +750 -0
  56. package/server/lib/messaging-patterns.test.js +213 -0
  57. package/server/lib/microservice-template.js +386 -0
  58. package/server/lib/microservice-template.test.js +325 -0
  59. package/server/lib/new-project-microservice.js +450 -0
  60. package/server/lib/new-project-microservice.test.js +600 -0
  61. package/server/lib/refactor-command.js +326 -0
  62. package/server/lib/refactor-command.test.js +528 -0
  63. package/server/lib/refactor-executor.js +254 -0
  64. package/server/lib/refactor-executor.test.js +305 -0
  65. package/server/lib/refactor-observer.js +292 -0
  66. package/server/lib/refactor-observer.test.js +422 -0
  67. package/server/lib/refactor-progress.js +193 -0
  68. package/server/lib/refactor-progress.test.js +251 -0
  69. package/server/lib/refactor-reporter.js +237 -0
  70. package/server/lib/refactor-reporter.test.js +247 -0
  71. package/server/lib/semantic-analyzer.js +198 -0
  72. package/server/lib/semantic-analyzer.test.js +474 -0
  73. package/server/lib/service-scaffold.js +486 -0
  74. package/server/lib/service-scaffold.test.js +373 -0
  75. package/server/lib/shared-kernel.js +578 -0
  76. package/server/lib/shared-kernel.test.js +255 -0
  77. package/server/lib/traefik-config.js +282 -0
  78. package/server/lib/traefik-config.test.js +312 -0
  79. package/server/lib/usage-command.js +218 -0
  80. package/server/lib/usage-command.test.js +391 -0
  81. package/server/lib/usage-formatter.js +192 -0
  82. package/server/lib/usage-formatter.test.js +267 -0
  83. package/server/lib/usage-history.js +122 -0
  84. package/server/lib/usage-history.test.js +206 -0
  85. package/server/package-lock.json +14 -0
  86. package/server/package.json +1 -0
  87. package/templates/docs-sync.yml +91 -0
package/README.md CHANGED
@@ -58,10 +58,12 @@ No manual testing. No "does this work?" No vibes.
58
58
  ### Then Just Run
59
59
 
60
60
  ```bash
61
- /tlc
61
+ /tlc:next
62
62
  ```
63
63
 
64
- TLC knows where you are and what's next.
64
+ Shows what's next, asks "Proceed? [Y/n]", then executes. That's it.
65
+
66
+ Or use `/tlc` for the full dashboard.
65
67
 
66
68
  ---
67
69
 
@@ -70,6 +72,8 @@ TLC knows where you are and what's next.
70
72
  ### For Solo Developers
71
73
 
72
74
  - **Test-first by default** — Claude writes tests before code
75
+ - **Auto-parallelization** — Up to 10 agents run independent tasks simultaneously
76
+ - **`/tlc:next`** — One command to progress. No decisions needed.
73
77
  - **Smart dashboard** — See progress, run actions
74
78
  - **Coverage gaps** — Find and fix untested code
75
79
  - **Auto-fix** — Automatically repair failing tests
@@ -95,10 +99,11 @@ TLC knows where you are and what's next.
95
99
 
96
100
  | Command | What It Does |
97
101
  |---------|--------------|
98
- | `/tlc` | **Smart entry pointknows what's next** |
102
+ | `/tlc:next` | **Just do itshows next action, asks once, executes** |
103
+ | `/tlc` | Smart dashboard — full status view |
99
104
  | `/tlc:new-project` | Start new project with roadmap |
100
105
  | `/tlc:init` | Add TLC to existing codebase |
101
- | `/tlc:build` | Write tests → implement verify |
106
+ | `/tlc:build` | Write tests → implement (auto-parallelizes up to 10 agents) |
102
107
  | `/tlc:coverage` | Find and fix untested code |
103
108
  | `/tlc:quality` | Test quality scoring |
104
109
  | `/tlc:autofix` | Auto-repair failing tests |
@@ -0,0 +1,13 @@
1
+ interface ModelUsage {
2
+ daily: number;
3
+ monthly: number;
4
+ requests: number;
5
+ budgetDaily: number;
6
+ budgetMonthly: number;
7
+ }
8
+ interface UsagePaneProps {
9
+ data?: Record<string, ModelUsage>;
10
+ alerts?: string[];
11
+ }
12
+ export declare function UsagePane({ data, alerts }: UsagePaneProps): import("react/jsx-runtime").JSX.Element;
13
+ export {};
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ function formatCurrency(amount) {
4
+ return '$' + amount.toFixed(2);
5
+ }
6
+ function getUsageColor(used, budget) {
7
+ if (budget === 0)
8
+ return 'gray';
9
+ const percent = (used / budget) * 100;
10
+ if (percent >= 100)
11
+ return 'red';
12
+ if (percent >= 80)
13
+ return 'yellow';
14
+ return 'green';
15
+ }
16
+ function createBar(used, budget, width = 15) {
17
+ if (budget === 0)
18
+ return '░'.repeat(width);
19
+ const percent = Math.min(used / budget, 1.5); // Cap at 150% for display
20
+ const filled = Math.round(percent * width);
21
+ const bar = '█'.repeat(Math.min(filled, width)) + '░'.repeat(Math.max(0, width - filled));
22
+ return bar;
23
+ }
24
+ export function UsagePane({ data, alerts }) {
25
+ if (!data || Object.keys(data).length === 0) {
26
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Usage Dashboard" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No usage data available." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Run /tlc:usage to view." }) })] }));
27
+ }
28
+ const models = Object.keys(data);
29
+ // Calculate totals
30
+ let totalDaily = 0;
31
+ let totalMonthly = 0;
32
+ let totalRequests = 0;
33
+ let totalBudgetDaily = 0;
34
+ let totalBudgetMonthly = 0;
35
+ for (const model of models) {
36
+ const usage = data[model];
37
+ totalDaily += usage.daily;
38
+ totalMonthly += usage.monthly;
39
+ totalRequests += usage.requests;
40
+ totalBudgetDaily += usage.budgetDaily;
41
+ totalBudgetMonthly += usage.budgetMonthly;
42
+ }
43
+ const totalDailyPercent = totalBudgetDaily > 0 ? Math.round((totalDaily / totalBudgetDaily) * 100) : 0;
44
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Usage Dashboard" }), alerts && alerts.length > 0 && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: alerts.map((alert, idx) => (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", alert] }) }, idx))) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: models.map((model) => {
45
+ const usage = data[model];
46
+ const dailyPercent = usage.budgetDaily > 0 ? Math.round((usage.daily / usage.budgetDaily) * 100) : 0;
47
+ const isOver = usage.daily > usage.budgetDaily;
48
+ const color = getUsageColor(usage.daily, usage.budgetDaily);
49
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: model }), isOver && _jsx(Text, { color: "red", children: " OVER" })] }), _jsxs(Box, { children: [_jsxs(Text, { color: color, children: ["[", createBar(usage.daily, usage.budgetDaily), "]"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: color, children: formatCurrency(usage.daily) }), _jsxs(Text, { dimColor: true, children: [" / ", formatCurrency(usage.budgetDaily)] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: color, children: [dailyPercent, "%"] })] }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" Requests: ", usage.requests] }) })] }, model));
50
+ }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Totals" }), _jsxs(Box, { children: [_jsx(Text, { children: "Daily: " }), _jsx(Text, { color: getUsageColor(totalDaily, totalBudgetDaily), children: formatCurrency(totalDaily) }), _jsxs(Text, { dimColor: true, children: [" / ", formatCurrency(totalBudgetDaily)] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: getUsageColor(totalDaily, totalBudgetDaily), children: [totalDailyPercent, "%"] })] }), _jsxs(Box, { children: [_jsx(Text, { children: "Monthly: " }), _jsx(Text, { color: getUsageColor(totalMonthly, totalBudgetMonthly), children: formatCurrency(totalMonthly) }), _jsxs(Text, { dimColor: true, children: [" / ", formatCurrency(totalBudgetMonthly)] })] }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["Requests: ", totalRequests] }) })] })] }));
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { UsagePane } from './UsagePane.js';
5
+ describe('UsagePane', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
12
+ it('renders without error', () => {
13
+ const { lastFrame } = render(_jsx(UsagePane, {}));
14
+ expect(lastFrame()).toBeDefined();
15
+ });
16
+ it('shows placeholder when no usage data', () => {
17
+ const { lastFrame } = render(_jsx(UsagePane, {}));
18
+ const output = lastFrame();
19
+ expect(output).toContain('Usage');
20
+ });
21
+ it('renders usage bars for each model', () => {
22
+ const usageData = {
23
+ openai: { daily: 5, monthly: 50, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
24
+ deepseek: { daily: 2, monthly: 20, requests: 50, budgetDaily: 5, budgetMonthly: 50 },
25
+ };
26
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
27
+ const output = lastFrame();
28
+ expect(output).toContain('openai');
29
+ expect(output).toContain('deepseek');
30
+ });
31
+ it('shows model names', () => {
32
+ const usageData = {
33
+ 'gpt-4': { daily: 3, monthly: 30, requests: 60, budgetDaily: 10, budgetMonthly: 100 },
34
+ };
35
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
36
+ const output = lastFrame();
37
+ expect(output).toContain('gpt-4');
38
+ });
39
+ it('shows dollar amounts', () => {
40
+ const usageData = {
41
+ openai: { daily: 5.50, monthly: 55.25, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
42
+ };
43
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
44
+ const output = lastFrame();
45
+ expect(output).toContain('$5.50');
46
+ });
47
+ it('shows budget limit', () => {
48
+ const usageData = {
49
+ openai: { daily: 5, monthly: 50, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
50
+ };
51
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
52
+ const output = lastFrame();
53
+ expect(output).toContain('$10');
54
+ });
55
+ it('highlights over-budget with warning color', () => {
56
+ const usageData = {
57
+ openai: { daily: 12, monthly: 120, requests: 150, budgetDaily: 10, budgetMonthly: 100 },
58
+ };
59
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
60
+ const output = lastFrame();
61
+ // Should show the over-budget amount
62
+ expect(output).toContain('$12');
63
+ // Should have some indicator of over-budget
64
+ expect(output).toContain('OVER');
65
+ });
66
+ it('shows alert messages when provided', () => {
67
+ const usageData = {
68
+ openai: { daily: 8, monthly: 80, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
69
+ };
70
+ const alerts = ['Budget Alert: openai at 80% of daily budget'];
71
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData, alerts: alerts }));
72
+ const output = lastFrame();
73
+ expect(output).toContain('80%');
74
+ });
75
+ it('shows percentage of budget used', () => {
76
+ const usageData = {
77
+ openai: { daily: 5, monthly: 50, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
78
+ };
79
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
80
+ const output = lastFrame();
81
+ expect(output).toContain('50%');
82
+ });
83
+ it('updates on data change', () => {
84
+ const usageData1 = {
85
+ openai: { daily: 5, monthly: 50, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
86
+ };
87
+ const { lastFrame, rerender } = render(_jsx(UsagePane, { data: usageData1 }));
88
+ let output = lastFrame();
89
+ expect(output).toContain('$5');
90
+ const usageData2 = {
91
+ openai: { daily: 7, monthly: 70, requests: 140, budgetDaily: 10, budgetMonthly: 100 },
92
+ };
93
+ rerender(_jsx(UsagePane, { data: usageData2 }));
94
+ output = lastFrame();
95
+ expect(output).toContain('$7');
96
+ });
97
+ it('shows totals across all models', () => {
98
+ const usageData = {
99
+ openai: { daily: 5, monthly: 50, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
100
+ deepseek: { daily: 3, monthly: 30, requests: 60, budgetDaily: 5, budgetMonthly: 50 },
101
+ };
102
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
103
+ const output = lastFrame();
104
+ // Total daily: $8, Total budget: $15
105
+ expect(output).toContain('$8');
106
+ });
107
+ it('shows visual progress bar for usage', () => {
108
+ const usageData = {
109
+ openai: { daily: 5, monthly: 50, requests: 100, budgetDaily: 10, budgetMonthly: 100 },
110
+ };
111
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
112
+ const output = lastFrame();
113
+ // Should contain some visual bar representation
114
+ expect(output).toBeTruthy();
115
+ });
116
+ it('handles zero usage gracefully', () => {
117
+ const usageData = {
118
+ openai: { daily: 0, monthly: 0, requests: 0, budgetDaily: 10, budgetMonthly: 100 },
119
+ };
120
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
121
+ const output = lastFrame();
122
+ expect(output).toContain('$0');
123
+ expect(output).toContain('0%');
124
+ });
125
+ it('handles missing budget values', () => {
126
+ const usageData = {
127
+ openai: { daily: 5, monthly: 50, requests: 100, budgetDaily: 0, budgetMonthly: 0 },
128
+ };
129
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
130
+ const output = lastFrame();
131
+ // Should still render without errors
132
+ expect(output).toBeDefined();
133
+ });
134
+ it('shows request count', () => {
135
+ const usageData = {
136
+ openai: { daily: 5, monthly: 50, requests: 150, budgetDaily: 10, budgetMonthly: 100 },
137
+ };
138
+ const { lastFrame } = render(_jsx(UsagePane, { data: usageData }));
139
+ const output = lastFrame();
140
+ expect(output).toContain('150');
141
+ });
142
+ });
@@ -0,0 +1,19 @@
1
+ export interface WorkspaceDoc {
2
+ id: string;
3
+ name: string;
4
+ path: string;
5
+ type: 'markdown' | 'mermaid';
6
+ content?: string;
7
+ linkedDocs?: string[];
8
+ }
9
+ export interface WorkspaceDocsPaneProps {
10
+ docs?: WorkspaceDoc[];
11
+ selectedDocId?: string;
12
+ loading?: boolean;
13
+ isActive: boolean;
14
+ onRefresh?: () => void;
15
+ onRegenerate?: () => void;
16
+ onSelectDoc?: (docId: string) => void;
17
+ }
18
+ export declare function WorkspaceDocsPane({ docs, selectedDocId, loading, isActive, onRefresh, onRegenerate, onSelectDoc, }: WorkspaceDocsPaneProps): import("react/jsx-runtime").JSX.Element;
19
+ export default WorkspaceDocsPane;
@@ -0,0 +1,146 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useState } from 'react';
4
+ /**
5
+ * Parse markdown content and render as terminal-friendly text
6
+ */
7
+ function renderMarkdownLine(line) {
8
+ // Heading detection
9
+ if (line.startsWith('# ')) {
10
+ return { text: line.slice(2), bold: true, dimColor: false, color: 'cyan' };
11
+ }
12
+ if (line.startsWith('## ')) {
13
+ return { text: line.slice(3), bold: true, dimColor: false, color: 'cyan' };
14
+ }
15
+ if (line.startsWith('### ')) {
16
+ return { text: line.slice(4), bold: true, dimColor: false };
17
+ }
18
+ return { text: line, bold: false, dimColor: false };
19
+ }
20
+ function parseMarkdown(content) {
21
+ const blocks = [];
22
+ const lines = content.split('\n');
23
+ let i = 0;
24
+ while (i < lines.length) {
25
+ const line = lines[i];
26
+ // Check for code blocks (including mermaid)
27
+ if (line.startsWith('```')) {
28
+ const language = line.slice(3).trim();
29
+ const codeLines = [];
30
+ i++;
31
+ while (i < lines.length && !lines[i].startsWith('```')) {
32
+ codeLines.push(lines[i]);
33
+ i++;
34
+ }
35
+ if (language === 'mermaid') {
36
+ blocks.push({
37
+ type: 'mermaid',
38
+ content: codeLines.join('\n'),
39
+ language: 'mermaid'
40
+ });
41
+ }
42
+ else {
43
+ blocks.push({
44
+ type: 'code',
45
+ content: codeLines.join('\n'),
46
+ language: language || 'text'
47
+ });
48
+ }
49
+ i++;
50
+ continue;
51
+ }
52
+ // Check for headings
53
+ if (line.startsWith('#')) {
54
+ blocks.push({
55
+ type: 'heading',
56
+ content: line.replace(/^#+\s*/, '')
57
+ });
58
+ i++;
59
+ continue;
60
+ }
61
+ // Regular text
62
+ if (line.trim()) {
63
+ blocks.push({
64
+ type: 'text',
65
+ content: line
66
+ });
67
+ }
68
+ i++;
69
+ }
70
+ return blocks;
71
+ }
72
+ /**
73
+ * Render a content block as Ink elements
74
+ */
75
+ function ContentBlockView({ block }) {
76
+ switch (block.type) {
77
+ case 'heading':
78
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: "cyan", children: block.content }) }));
79
+ case 'code':
80
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsx(Text, { dimColor: true, children: block.language }), _jsx(Text, { children: block.content })] }));
81
+ case 'mermaid':
82
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "double", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: "Mermaid Diagram" }), _jsx(Text, { dimColor: true, children: block.content })] }));
83
+ case 'text':
84
+ default:
85
+ return _jsx(Text, { children: block.content });
86
+ }
87
+ }
88
+ /**
89
+ * Doc list item component
90
+ */
91
+ function DocListItem({ doc, isSelected, }) {
92
+ const linkCount = doc.linkedDocs?.length || 0;
93
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, underline: isSelected, children: [isSelected ? '> ' : ' ', doc.name] }), linkCount > 0 && (_jsxs(Text, { dimColor: true, children: [" (", linkCount, " links)"] }))] }));
94
+ }
95
+ /**
96
+ * Document content viewer
97
+ */
98
+ function DocContentView({ doc }) {
99
+ if (!doc.content) {
100
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No content available" }) }));
101
+ }
102
+ const blocks = parseMarkdown(doc.content);
103
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", paddingX: 1, children: [_jsx(Text, { bold: true, children: doc.name }), blocks.map((block, idx) => (_jsx(ContentBlockView, { block: block }, idx)))] }));
104
+ }
105
+ export function WorkspaceDocsPane({ docs = [], selectedDocId, loading = false, isActive, onRefresh, onRegenerate, onSelectDoc, }) {
106
+ const [selectedIndex, setSelectedIndex] = useState(0);
107
+ // Get actual selected doc (from prop or from index)
108
+ const actualSelectedDocId = selectedDocId || docs[selectedIndex]?.id;
109
+ const selectedDoc = docs.find(d => d.id === actualSelectedDocId);
110
+ useInput((input, key) => {
111
+ if (!isActive)
112
+ return;
113
+ // Navigation
114
+ if (key.upArrow) {
115
+ const newIndex = Math.max(0, selectedIndex - 1);
116
+ setSelectedIndex(newIndex);
117
+ if (docs[newIndex] && onSelectDoc) {
118
+ onSelectDoc(docs[newIndex].id);
119
+ }
120
+ }
121
+ if (key.downArrow) {
122
+ const newIndex = Math.min(docs.length - 1, selectedIndex + 1);
123
+ setSelectedIndex(newIndex);
124
+ if (docs[newIndex] && onSelectDoc) {
125
+ onSelectDoc(docs[newIndex].id);
126
+ }
127
+ }
128
+ // Actions
129
+ if (input === 'r') {
130
+ onRefresh?.();
131
+ }
132
+ if (input === 'g') {
133
+ onRegenerate?.();
134
+ }
135
+ }, { isActive });
136
+ // Loading state
137
+ if (loading) {
138
+ return (_jsx(Box, { padding: 1, flexDirection: "column", children: _jsx(Text, { color: "yellow", children: "Loading docs..." }) }));
139
+ }
140
+ // Empty state
141
+ if (docs.length === 0) {
142
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Workspace Docs" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No docs generated yet." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Run /tlc:workspace-docs to generate documentation" }) })] }));
143
+ }
144
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Workspace Docs" }), _jsxs(Text, { dimColor: true, children: [" (", docs.length, ")"] })] }), _jsxs(Box, { flexDirection: "column", children: [docs.slice(0, 10).map((doc, idx) => (_jsx(DocListItem, { doc: doc, isSelected: doc.id === actualSelectedDocId }, doc.id))), docs.length > 10 && (_jsxs(Text, { dimColor: true, children: ["... and ", docs.length - 10, " more"] }))] }), selectedDoc && _jsx(DocContentView, { doc: selectedDoc }), isActive && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[arrows] Navigate [r] Refresh [g] Regenerate" }) }))] }));
145
+ }
146
+ export default WorkspaceDocsPane;
@@ -0,0 +1,242 @@
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 { WorkspaceDocsPane } from './WorkspaceDocsPane.js';
5
+ describe('WorkspaceDocsPane', () => {
6
+ describe('doc list', () => {
7
+ it('renders doc list', () => {
8
+ const docs = [
9
+ { id: 'readme', name: 'README.md', path: '/docs/README.md', type: 'markdown' },
10
+ { id: 'api', name: 'API.md', path: '/docs/API.md', type: 'markdown' },
11
+ ];
12
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: false }));
13
+ const output = lastFrame();
14
+ expect(output).toContain('README.md');
15
+ expect(output).toContain('API.md');
16
+ });
17
+ it('shows doc count in header', () => {
18
+ const docs = [
19
+ { id: 'doc1', name: 'Doc1.md', path: '/docs/Doc1.md', type: 'markdown' },
20
+ { id: 'doc2', name: 'Doc2.md', path: '/docs/Doc2.md', type: 'markdown' },
21
+ { id: 'doc3', name: 'Doc3.md', path: '/docs/Doc3.md', type: 'markdown' },
22
+ ];
23
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: false }));
24
+ const output = lastFrame();
25
+ expect(output).toContain('3');
26
+ });
27
+ });
28
+ describe('markdown content', () => {
29
+ it('shows markdown content for selected doc', () => {
30
+ const docs = [
31
+ {
32
+ id: 'readme',
33
+ name: 'README.md',
34
+ path: '/docs/README.md',
35
+ type: 'markdown',
36
+ content: '# Project Title\n\nThis is the readme content.'
37
+ },
38
+ ];
39
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, selectedDocId: "readme", isActive: false }));
40
+ const output = lastFrame();
41
+ expect(output).toContain('Project Title');
42
+ expect(output).toContain('readme content');
43
+ });
44
+ it('renders headings distinctly', () => {
45
+ const docs = [
46
+ {
47
+ id: 'doc1',
48
+ name: 'Doc.md',
49
+ path: '/docs/Doc.md',
50
+ type: 'markdown',
51
+ content: '## Section Heading\n\nParagraph text.'
52
+ },
53
+ ];
54
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, selectedDocId: "doc1", isActive: false }));
55
+ const output = lastFrame();
56
+ expect(output).toContain('Section Heading');
57
+ });
58
+ it('renders code blocks', () => {
59
+ const docs = [
60
+ {
61
+ id: 'code-doc',
62
+ name: 'Code.md',
63
+ path: '/docs/Code.md',
64
+ type: 'markdown',
65
+ content: '```typescript\nconst x = 1;\n```'
66
+ },
67
+ ];
68
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, selectedDocId: "code-doc", isActive: false }));
69
+ const output = lastFrame();
70
+ expect(output).toContain('const x = 1');
71
+ });
72
+ });
73
+ describe('mermaid diagrams', () => {
74
+ it('renders Mermaid diagrams', () => {
75
+ const docs = [
76
+ {
77
+ id: 'arch',
78
+ name: 'Architecture.md',
79
+ path: '/docs/Architecture.md',
80
+ type: 'markdown',
81
+ content: '```mermaid\ngraph TD\n A[Start] --> B[End]\n```'
82
+ },
83
+ ];
84
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, selectedDocId: "arch", isActive: false }));
85
+ const output = lastFrame();
86
+ // In terminal, mermaid is rendered as text representation
87
+ expect(output).toContain('Mermaid');
88
+ });
89
+ it('shows diagram type indicator', () => {
90
+ const docs = [
91
+ {
92
+ id: 'flow',
93
+ name: 'Flow.md',
94
+ path: '/docs/Flow.md',
95
+ type: 'markdown',
96
+ content: '```mermaid\nsequenceDiagram\n A->>B: Message\n```'
97
+ },
98
+ ];
99
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, selectedDocId: "flow", isActive: false }));
100
+ const output = lastFrame();
101
+ expect(output).toContain('sequenceDiagram');
102
+ });
103
+ });
104
+ describe('doc links', () => {
105
+ it('shows links between related docs', () => {
106
+ const docs = [
107
+ {
108
+ id: 'main',
109
+ name: 'Main.md',
110
+ path: '/docs/Main.md',
111
+ type: 'markdown',
112
+ content: 'See [API Documentation](./API.md)',
113
+ linkedDocs: ['api']
114
+ },
115
+ {
116
+ id: 'api',
117
+ name: 'API.md',
118
+ path: '/docs/API.md',
119
+ type: 'markdown'
120
+ },
121
+ ];
122
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, selectedDocId: "main", isActive: false }));
123
+ const output = lastFrame();
124
+ expect(output).toContain('API');
125
+ });
126
+ it('indicates linked docs in list view', () => {
127
+ const docs = [
128
+ {
129
+ id: 'main',
130
+ name: 'Main.md',
131
+ path: '/docs/Main.md',
132
+ type: 'markdown',
133
+ linkedDocs: ['api', 'readme']
134
+ },
135
+ ];
136
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: false }));
137
+ const output = lastFrame();
138
+ // Should show link count
139
+ expect(output).toContain('2');
140
+ });
141
+ });
142
+ describe('loading state', () => {
143
+ it('shows loading state', () => {
144
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { loading: true, isActive: false }));
145
+ const output = lastFrame();
146
+ expect(output).toContain('Loading');
147
+ });
148
+ it('shows loading indicator while refreshing', () => {
149
+ const docs = [
150
+ { id: 'doc1', name: 'Doc.md', path: '/docs/Doc.md', type: 'markdown' },
151
+ ];
152
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, loading: true, isActive: false }));
153
+ const output = lastFrame();
154
+ expect(output).toContain('Loading');
155
+ });
156
+ });
157
+ describe('empty state', () => {
158
+ it('shows empty state when no docs', () => {
159
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: [], isActive: false }));
160
+ const output = lastFrame();
161
+ expect(output).toContain('No docs');
162
+ });
163
+ it('shows hint to generate docs', () => {
164
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: [], isActive: false }));
165
+ const output = lastFrame();
166
+ expect(output).toContain('/tlc');
167
+ });
168
+ });
169
+ describe('refresh/regenerate', () => {
170
+ it('shows refresh button when active', () => {
171
+ const docs = [
172
+ { id: 'doc1', name: 'Doc.md', path: '/docs/Doc.md', type: 'markdown' },
173
+ ];
174
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: true }));
175
+ const output = lastFrame();
176
+ expect(output).toContain('Refresh');
177
+ });
178
+ it('accepts onRefresh callback prop', () => {
179
+ const onRefresh = vi.fn();
180
+ const docs = [
181
+ { id: 'doc1', name: 'Doc.md', path: '/docs/Doc.md', type: 'markdown' },
182
+ ];
183
+ // Verify component renders with callback without errors
184
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: true, onRefresh: onRefresh }));
185
+ const output = lastFrame();
186
+ // Controls should be shown indicating 'r' key works
187
+ expect(output).toContain('r');
188
+ expect(output).toContain('Refresh');
189
+ });
190
+ it('shows regenerate option', () => {
191
+ const docs = [
192
+ { id: 'doc1', name: 'Doc.md', path: '/docs/Doc.md', type: 'markdown' },
193
+ ];
194
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: true }));
195
+ const output = lastFrame();
196
+ expect(output).toContain('Regenerate');
197
+ });
198
+ it('accepts onRegenerate callback prop', () => {
199
+ const onRegenerate = vi.fn();
200
+ const docs = [
201
+ { id: 'doc1', name: 'Doc.md', path: '/docs/Doc.md', type: 'markdown' },
202
+ ];
203
+ // Verify component renders with callback without errors
204
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: true, onRegenerate: onRegenerate }));
205
+ const output = lastFrame();
206
+ // Controls should be shown indicating 'g' key works
207
+ expect(output).toContain('g');
208
+ expect(output).toContain('Regenerate');
209
+ });
210
+ it('hides controls when not active', () => {
211
+ const docs = [
212
+ { id: 'doc1', name: 'Doc.md', path: '/docs/Doc.md', type: 'markdown' },
213
+ ];
214
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: false }));
215
+ const output = lastFrame();
216
+ expect(output).not.toContain('Refresh');
217
+ });
218
+ });
219
+ describe('navigation', () => {
220
+ it('accepts onSelectDoc callback prop', () => {
221
+ const onSelectDoc = vi.fn();
222
+ const docs = [
223
+ { id: 'doc1', name: 'First.md', path: '/docs/First.md', type: 'markdown' },
224
+ { id: 'doc2', name: 'Second.md', path: '/docs/Second.md', type: 'markdown' },
225
+ ];
226
+ // Verify component renders with callback without errors
227
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: true, onSelectDoc: onSelectDoc }));
228
+ const output = lastFrame();
229
+ // Controls should be shown indicating arrow keys work
230
+ expect(output).toContain('arrows');
231
+ expect(output).toContain('Navigate');
232
+ });
233
+ it('shows navigation hints when active', () => {
234
+ const docs = [
235
+ { id: 'doc1', name: 'Doc.md', path: '/docs/Doc.md', type: 'markdown' },
236
+ ];
237
+ const { lastFrame } = render(_jsx(WorkspaceDocsPane, { docs: docs, isActive: true }));
238
+ const output = lastFrame();
239
+ expect(output).toContain('Navigate');
240
+ });
241
+ });
242
+ });