chronicle-ai 0.0.1

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 (155) hide show
  1. package/README.md +132 -0
  2. package/package.json +86 -0
  3. package/packages/README.md +139 -0
  4. package/packages/cli/README.md +92 -0
  5. package/packages/cli/bin/chronicle.js +2 -0
  6. package/packages/cli/dist/commands/add.d.ts +2 -0
  7. package/packages/cli/dist/commands/add.d.ts.map +1 -0
  8. package/packages/cli/dist/commands/add.js +82 -0
  9. package/packages/cli/dist/commands/add.js.map +1 -0
  10. package/packages/cli/dist/commands/diff.d.ts +11 -0
  11. package/packages/cli/dist/commands/diff.d.ts.map +1 -0
  12. package/packages/cli/dist/commands/diff.js +164 -0
  13. package/packages/cli/dist/commands/diff.js.map +1 -0
  14. package/packages/cli/dist/commands/init.d.ts +2 -0
  15. package/packages/cli/dist/commands/init.d.ts.map +1 -0
  16. package/packages/cli/dist/commands/init.js +54 -0
  17. package/packages/cli/dist/commands/init.js.map +1 -0
  18. package/packages/cli/dist/commands/list.d.ts +2 -0
  19. package/packages/cli/dist/commands/list.d.ts.map +1 -0
  20. package/packages/cli/dist/commands/list.js +62 -0
  21. package/packages/cli/dist/commands/list.js.map +1 -0
  22. package/packages/cli/dist/commands/log.d.ts +4 -0
  23. package/packages/cli/dist/commands/log.d.ts.map +1 -0
  24. package/packages/cli/dist/commands/log.js +223 -0
  25. package/packages/cli/dist/commands/log.js.map +1 -0
  26. package/packages/cli/dist/commands/pause.d.ts +3 -0
  27. package/packages/cli/dist/commands/pause.d.ts.map +1 -0
  28. package/packages/cli/dist/commands/pause.js +49 -0
  29. package/packages/cli/dist/commands/pause.js.map +1 -0
  30. package/packages/cli/dist/commands/queue.d.ts +2 -0
  31. package/packages/cli/dist/commands/queue.d.ts.map +1 -0
  32. package/packages/cli/dist/commands/queue.js +96 -0
  33. package/packages/cli/dist/commands/queue.js.map +1 -0
  34. package/packages/cli/dist/commands/remove.d.ts +2 -0
  35. package/packages/cli/dist/commands/remove.d.ts.map +1 -0
  36. package/packages/cli/dist/commands/remove.js +80 -0
  37. package/packages/cli/dist/commands/remove.js.map +1 -0
  38. package/packages/cli/dist/commands/reset.d.ts +5 -0
  39. package/packages/cli/dist/commands/reset.d.ts.map +1 -0
  40. package/packages/cli/dist/commands/reset.js +90 -0
  41. package/packages/cli/dist/commands/reset.js.map +1 -0
  42. package/packages/cli/dist/commands/restart.d.ts +2 -0
  43. package/packages/cli/dist/commands/restart.d.ts.map +1 -0
  44. package/packages/cli/dist/commands/restart.js +72 -0
  45. package/packages/cli/dist/commands/restart.js.map +1 -0
  46. package/packages/cli/dist/commands/start.d.ts +2 -0
  47. package/packages/cli/dist/commands/start.d.ts.map +1 -0
  48. package/packages/cli/dist/commands/start.js +127 -0
  49. package/packages/cli/dist/commands/start.js.map +1 -0
  50. package/packages/cli/dist/commands/status.d.ts +2 -0
  51. package/packages/cli/dist/commands/status.d.ts.map +1 -0
  52. package/packages/cli/dist/commands/status.js +181 -0
  53. package/packages/cli/dist/commands/status.js.map +1 -0
  54. package/packages/cli/dist/commands/stop.d.ts +2 -0
  55. package/packages/cli/dist/commands/stop.d.ts.map +1 -0
  56. package/packages/cli/dist/commands/stop.js +64 -0
  57. package/packages/cli/dist/commands/stop.js.map +1 -0
  58. package/packages/cli/dist/index.d.ts +2 -0
  59. package/packages/cli/dist/index.d.ts.map +1 -0
  60. package/packages/cli/dist/index.js +85 -0
  61. package/packages/cli/dist/index.js.map +1 -0
  62. package/packages/cli/dist/utils/paths.d.ts +26 -0
  63. package/packages/cli/dist/utils/paths.d.ts.map +1 -0
  64. package/packages/cli/dist/utils/paths.js +183 -0
  65. package/packages/cli/dist/utils/paths.js.map +1 -0
  66. package/packages/cli/package.json +25 -0
  67. package/packages/daemon/README.md +83 -0
  68. package/packages/daemon/dist/ai_test.d.ts +2 -0
  69. package/packages/daemon/dist/ai_test.d.ts.map +1 -0
  70. package/packages/daemon/dist/ai_test.js +4 -0
  71. package/packages/daemon/dist/ai_test.js.map +1 -0
  72. package/packages/daemon/dist/index.d.ts +2 -0
  73. package/packages/daemon/dist/index.d.ts.map +1 -0
  74. package/packages/daemon/dist/index.js +147 -0
  75. package/packages/daemon/dist/index.js.map +1 -0
  76. package/packages/daemon/dist/jobs/AIProcessor.d.ts +6 -0
  77. package/packages/daemon/dist/jobs/AIProcessor.d.ts.map +1 -0
  78. package/packages/daemon/dist/jobs/AIProcessor.js +58 -0
  79. package/packages/daemon/dist/jobs/AIProcessor.js.map +1 -0
  80. package/packages/daemon/dist/jobs/DocProcessor.d.ts +8 -0
  81. package/packages/daemon/dist/jobs/DocProcessor.d.ts.map +1 -0
  82. package/packages/daemon/dist/jobs/DocProcessor.js +336 -0
  83. package/packages/daemon/dist/jobs/DocProcessor.js.map +1 -0
  84. package/packages/daemon/dist/jobs/FileProcessor.d.ts +7 -0
  85. package/packages/daemon/dist/jobs/FileProcessor.d.ts.map +1 -0
  86. package/packages/daemon/dist/jobs/FileProcessor.js +29 -0
  87. package/packages/daemon/dist/jobs/FileProcessor.js.map +1 -0
  88. package/packages/daemon/dist/jobs/SystemProcessor.d.ts +13 -0
  89. package/packages/daemon/dist/jobs/SystemProcessor.d.ts.map +1 -0
  90. package/packages/daemon/dist/jobs/SystemProcessor.js +38 -0
  91. package/packages/daemon/dist/jobs/SystemProcessor.js.map +1 -0
  92. package/packages/daemon/dist/jobs/UpdateProcessor.d.ts +21 -0
  93. package/packages/daemon/dist/jobs/UpdateProcessor.d.ts.map +1 -0
  94. package/packages/daemon/dist/jobs/UpdateProcessor.js +222 -0
  95. package/packages/daemon/dist/jobs/UpdateProcessor.js.map +1 -0
  96. package/packages/daemon/dist/services/AIService.d.ts +90 -0
  97. package/packages/daemon/dist/services/AIService.d.ts.map +1 -0
  98. package/packages/daemon/dist/services/AIService.js +451 -0
  99. package/packages/daemon/dist/services/AIService.js.map +1 -0
  100. package/packages/daemon/dist/services/ConfigService.d.ts +30 -0
  101. package/packages/daemon/dist/services/ConfigService.d.ts.map +1 -0
  102. package/packages/daemon/dist/services/ConfigService.js +69 -0
  103. package/packages/daemon/dist/services/ConfigService.js.map +1 -0
  104. package/packages/daemon/dist/services/DatabaseService.d.ts +204 -0
  105. package/packages/daemon/dist/services/DatabaseService.d.ts.map +1 -0
  106. package/packages/daemon/dist/services/DatabaseService.js +692 -0
  107. package/packages/daemon/dist/services/DatabaseService.js.map +1 -0
  108. package/packages/daemon/dist/services/GitService.d.ts +12 -0
  109. package/packages/daemon/dist/services/GitService.d.ts.map +1 -0
  110. package/packages/daemon/dist/services/GitService.js +68 -0
  111. package/packages/daemon/dist/services/GitService.js.map +1 -0
  112. package/packages/daemon/dist/services/QueueService.d.ts +16 -0
  113. package/packages/daemon/dist/services/QueueService.d.ts.map +1 -0
  114. package/packages/daemon/dist/services/QueueService.js +87 -0
  115. package/packages/daemon/dist/services/QueueService.js.map +1 -0
  116. package/packages/daemon/dist/services/TriggerService.d.ts +37 -0
  117. package/packages/daemon/dist/services/TriggerService.d.ts.map +1 -0
  118. package/packages/daemon/dist/services/TriggerService.js +150 -0
  119. package/packages/daemon/dist/services/TriggerService.js.map +1 -0
  120. package/packages/daemon/dist/services/WatcherService.d.ts +12 -0
  121. package/packages/daemon/dist/services/WatcherService.d.ts.map +1 -0
  122. package/packages/daemon/dist/services/WatcherService.js +77 -0
  123. package/packages/daemon/dist/services/WatcherService.js.map +1 -0
  124. package/packages/daemon/dist/services/index.d.ts +2 -0
  125. package/packages/daemon/dist/services/index.d.ts.map +1 -0
  126. package/packages/daemon/dist/services/index.js +18 -0
  127. package/packages/daemon/dist/services/index.js.map +1 -0
  128. package/packages/daemon/dist/test-ignore.js +0 -0
  129. package/packages/daemon/package.json +28 -0
  130. package/packages/ui/app/actions.ts +73 -0
  131. package/packages/ui/app/api/ai-activity/route.ts +14 -0
  132. package/packages/ui/app/globals.css +98 -0
  133. package/packages/ui/app/layout.tsx +29 -0
  134. package/packages/ui/app/page.tsx +109 -0
  135. package/packages/ui/components/AutoRefresh.tsx +18 -0
  136. package/packages/ui/components/DocumentationViewer.tsx +276 -0
  137. package/packages/ui/components/FileList.tsx +69 -0
  138. package/packages/ui/components/HeaderWithThinking.tsx +102 -0
  139. package/packages/ui/components/JobQueue.tsx +194 -0
  140. package/packages/ui/components/JobQueueWrapper.tsx +31 -0
  141. package/packages/ui/components/MermaidInit.tsx +24 -0
  142. package/packages/ui/components/ProjectContentArea.tsx +186 -0
  143. package/packages/ui/components/ProjectList.tsx +136 -0
  144. package/packages/ui/components/ThinkingDrawer.tsx +377 -0
  145. package/packages/ui/components/ThinkingPanel.tsx +63 -0
  146. package/packages/ui/components/TriggerSettings.tsx +185 -0
  147. package/packages/ui/components/VersionSelector.tsx +132 -0
  148. package/packages/ui/lib/db.ts +521 -0
  149. package/packages/ui/next-env.d.ts +5 -0
  150. package/packages/ui/next.config.js +4 -0
  151. package/packages/ui/package.json +32 -0
  152. package/packages/ui/postcss.config.js +6 -0
  153. package/packages/ui/public/logo.png +0 -0
  154. package/packages/ui/tailwind.config.ts +32 -0
  155. package/packages/ui/tsconfig.json +40 -0
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Brain } from 'lucide-react';
5
+ import { ThinkingDrawer } from './ThinkingDrawer';
6
+ import { clsx } from 'clsx';
7
+
8
+ interface AIActivity {
9
+ id: number;
10
+ project_id: number | null;
11
+ version_id: number | null;
12
+ activity_type: 'analyze' | 'generate' | 'plan' | 'embed' | 'other';
13
+ prompt_summary: string | null;
14
+ response_preview: string | null;
15
+ thinking_summary: string | null;
16
+ tokens_used: number | null;
17
+ duration_ms: number | null;
18
+ status: 'running' | 'completed' | 'error';
19
+ created_at: number;
20
+ project_path?: string;
21
+ }
22
+
23
+ interface Props {
24
+ initialActivities: AIActivity[];
25
+ }
26
+
27
+ export function HeaderWithThinking({ initialActivities }: Props) {
28
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
29
+ const [activities, setActivities] = useState<AIActivity[]>(initialActivities);
30
+
31
+ // Poll for new activities every 3 seconds
32
+ useEffect(() => {
33
+ const interval = setInterval(async () => {
34
+ try {
35
+ const res = await fetch('/api/ai-activity');
36
+ if (res.ok) {
37
+ const data = await res.json();
38
+ setActivities(data);
39
+ }
40
+ } catch {
41
+ // Silently fail
42
+ }
43
+ }, 3000);
44
+
45
+ return () => clearInterval(interval);
46
+ }, []);
47
+
48
+ const runningCount = activities.filter(a => a.status === 'running').length;
49
+
50
+ return (
51
+ <>
52
+ <header className="h-12 border-b border-gray-200 flex items-center justify-between px-4 bg-gray-50 flex-none select-none">
53
+ <div className="flex items-center gap-2">
54
+ <img src="/logo.png" alt="Chronicle Logo" className="w-6 h-6 object-contain" />
55
+ <h1 className="font-bold text-lg tracking-tight text-gray-800">
56
+ Chronicle <span className="text-gray-400 font-normal text-sm">Daemon</span>
57
+ </h1>
58
+ </div>
59
+ <div className="flex gap-3 items-center">
60
+ {/* Thinking Toggle Button */}
61
+ <button
62
+ onClick={() => setIsDrawerOpen(true)}
63
+ className={clsx(
64
+ "flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-medium transition-all",
65
+ runningCount > 0
66
+ ? "bg-purple-100 text-purple-700 hover:bg-purple-200 ring-2 ring-purple-300 ring-offset-1"
67
+ : activities.length > 0
68
+ ? "bg-purple-50 text-purple-600 hover:bg-purple-100"
69
+ : "bg-gray-100 text-gray-500 hover:bg-gray-200"
70
+ )}
71
+ title="Open Thinking Platform"
72
+ >
73
+ <Brain size={14} className={runningCount > 0 ? 'animate-pulse' : ''} />
74
+ <span>Thinking</span>
75
+ {activities.length > 0 && (
76
+ <span className={clsx(
77
+ "text-[9px] px-1.5 py-0.5 rounded-full font-bold",
78
+ runningCount > 0
79
+ ? "bg-purple-500 text-white animate-pulse"
80
+ : "bg-purple-200 text-purple-700"
81
+ )}>
82
+ {runningCount > 0 ? `${runningCount} ACTIVE` : activities.length}
83
+ </span>
84
+ )}
85
+ </button>
86
+
87
+ {/* Status Indicator */}
88
+ <div className="flex items-center gap-1.5 opacity-60">
89
+ <div className="w-1.5 h-1.5 rounded-full bg-emerald-500"></div>
90
+ <span className="text-xs font-bold text-gray-600">ONLINE</span>
91
+ </div>
92
+ </div>
93
+ </header>
94
+
95
+ <ThinkingDrawer
96
+ isOpen={isDrawerOpen}
97
+ onClose={() => setIsDrawerOpen(false)}
98
+ activities={activities}
99
+ />
100
+ </>
101
+ );
102
+ }
@@ -0,0 +1,194 @@
1
+ 'use client';
2
+
3
+ import { Activity, ChevronUp, ChevronDown, CheckCircle2, AlertCircle, Clock, Loader2, XCircle, Trash2 } from 'lucide-react';
4
+ import { formatDistanceToNow } from 'date-fns';
5
+ import { useState } from 'react';
6
+ import { clsx } from 'clsx';
7
+
8
+ interface JobRecord {
9
+ id: number;
10
+ name: string;
11
+ data: string;
12
+ status: string;
13
+ created_at: number;
14
+ updated_at: number;
15
+ }
16
+
17
+ interface Props {
18
+ jobs: JobRecord[];
19
+ onKillJob?: () => void;
20
+ onClearQueue?: () => void;
21
+ }
22
+
23
+ // Format job data into readable text
24
+ function formatJobData(data: string): string {
25
+ try {
26
+ const parsed = JSON.parse(data);
27
+ if (parsed.filePath) {
28
+ const filename = parsed.filePath.split('/').pop();
29
+ return `${filename}${parsed.event ? ` (${parsed.event})` : ''}`;
30
+ }
31
+ if (parsed.projectPath) {
32
+ const folder = parsed.projectPath.split('/').pop();
33
+ return folder || 'project';
34
+ }
35
+ if (parsed.path) {
36
+ const folder = parsed.path.split('/').pop();
37
+ return folder || 'path';
38
+ }
39
+ const keys = Object.keys(parsed);
40
+ if (keys.length > 0) {
41
+ const firstKey = keys[0];
42
+ const value = String(parsed[firstKey]).split('/').pop();
43
+ return value || parsed[firstKey];
44
+ }
45
+ return '';
46
+ } catch {
47
+ return data.slice(0, 50);
48
+ }
49
+ }
50
+
51
+ export function JobQueue({ jobs, onKillJob, onClearQueue }: Props) {
52
+ // false = collapsed (header only), true = expanded
53
+ const [isExpanded, setIsExpanded] = useState(true);
54
+
55
+ // Get processing job for collapsed view
56
+ const processingJob = jobs.find(j => j.status === 'processing');
57
+
58
+ // Also find recently completed/failed jobs (last 5 seconds) to show activity even if fast
59
+ const recentJob = jobs.find(j =>
60
+ j.status !== 'pending' &&
61
+ j.status !== 'processing' &&
62
+ (Date.now() - (j.updated_at * 1000)) < 5000
63
+ );
64
+
65
+ const activeJob = processingJob || recentJob;
66
+ const pendingCount = jobs.filter(j => j.status === 'pending').length;
67
+
68
+ return (
69
+ <div className={clsx(
70
+ "bg-gray-900 text-gray-400 flex flex-col transition-all duration-300 ease-in-out",
71
+ isExpanded ? "h-72" : "h-10"
72
+ )}>
73
+ {/* Header - always visible */}
74
+ <div className="px-4 py-2.5 bg-gray-950 border-b border-gray-800 flex justify-between items-center select-none flex-none">
75
+ <div className="flex items-center gap-3">
76
+ <div className="flex items-center gap-2">
77
+ <Activity size={14} className={clsx(activeJob ? "text-blue-400 animate-pulse" : "text-gray-600")} />
78
+ <span className="text-xs font-bold uppercase tracking-wider font-mono text-gray-500">System Jobs</span>
79
+ </div>
80
+ {pendingCount > 0 && (
81
+ <span className="px-1.5 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-[10px] font-mono border border-blue-500/20">
82
+ {pendingCount} pending
83
+ </span>
84
+ )}
85
+ </div>
86
+ <div className="text-[9px] text-gray-700 font-mono">
87
+ Updated: {new Date().toLocaleTimeString()}
88
+ </div>
89
+
90
+ {/* Active job shown in header when collapsed */}
91
+ {!isExpanded && activeJob && (
92
+ <div className="flex items-center gap-2 flex-1 justify-center mx-4">
93
+ {activeJob.status === 'processing' ? (
94
+ <Loader2 size={12} className="text-blue-400 animate-spin" />
95
+ ) : (
96
+ <CheckCircle2 size={12} className="text-green-500" />
97
+ )}
98
+ <span className="font-mono text-xs text-blue-300">{activeJob.name}</span>
99
+ <span className="text-xs text-gray-500">{formatJobData(activeJob.data)}</span>
100
+ </div>
101
+ )}
102
+
103
+ {!isExpanded && !processingJob && (
104
+ <div className="flex-1 text-center">
105
+ <span className="text-[10px] text-gray-600 font-mono">Idle</span>
106
+ </div>
107
+ )}
108
+
109
+ {/* Action buttons */}
110
+ <div className="flex items-center gap-1">
111
+ {processingJob && (
112
+ <button
113
+ onClick={onKillJob}
114
+ className="px-2 py-1 text-[10px] font-mono text-red-400 hover:bg-red-500/20 rounded transition-colors flex items-center gap-1"
115
+ title="Kill current job"
116
+ >
117
+ <XCircle size={12} />
118
+ Kill
119
+ </button>
120
+ )}
121
+ {pendingCount > 0 && (
122
+ <button
123
+ onClick={onClearQueue}
124
+ className="px-2 py-1 text-[10px] font-mono text-orange-400 hover:bg-orange-500/20 rounded transition-colors flex items-center gap-1"
125
+ title="Clear all pending jobs"
126
+ >
127
+ <Trash2 size={12} />
128
+ Clear
129
+ </button>
130
+ )}
131
+
132
+ {/* Chevron button - only this controls expand/collapse */}
133
+ <button
134
+ onClick={() => setIsExpanded(!isExpanded)}
135
+ className="p-1.5 hover:bg-gray-800 rounded transition-colors ml-1"
136
+ title={isExpanded ? "Collapse" : "Expand"}
137
+ >
138
+ {isExpanded ? <ChevronDown size={16} className="text-gray-400" /> : <ChevronUp size={16} className="text-gray-400" />}
139
+ </button>
140
+ </div>
141
+ </div>
142
+
143
+ {/* Content Area - only visible when expanded */}
144
+ {isExpanded && (
145
+ <div className="flex-1 overflow-y-auto p-2 space-y-0.5 bg-gray-900">
146
+ {jobs.map(job => (
147
+ <div
148
+ key={job.id}
149
+ className={clsx(
150
+ "group flex items-center justify-between px-3 py-2 rounded transition-colors border border-transparent",
151
+ job.status === 'processing' ? "bg-blue-500/10 border-blue-500/20" : "hover:bg-white/5 hover:border-white/5"
152
+ )}
153
+ >
154
+ <div className="flex items-center gap-3 overflow-hidden">
155
+ <StatusIcon status={job.status} />
156
+ <span className={clsx(
157
+ "font-mono text-xs font-bold whitespace-nowrap",
158
+ job.status === 'processing' ? "text-blue-300" : "text-gray-300"
159
+ )}>
160
+ {job.name}
161
+ </span>
162
+ <span className="text-xs text-gray-500 truncate max-w-[400px] group-hover:text-gray-400 transition-colors">
163
+ {formatJobData(job.data)}
164
+ </span>
165
+ </div>
166
+ <span className="text-[10px] text-gray-600 font-mono whitespace-nowrap pl-4 flex items-center gap-1" suppressHydrationWarning>
167
+ <Clock size={10} />
168
+ {formatDistanceToNow(job.created_at * 1000, { addSuffix: true })}
169
+ </span>
170
+ </div>
171
+ ))}
172
+ {jobs.length === 0 && (
173
+ <div className="p-8 text-center text-gray-700 text-xs font-mono">
174
+ System idle. No jobs.
175
+ </div>
176
+ )}
177
+ </div>
178
+ )}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ function StatusIcon({ status }: { status: string }) {
184
+ switch (status) {
185
+ case 'processing':
186
+ return <Loader2 size={14} className="text-blue-400 animate-spin" />;
187
+ case 'completed':
188
+ return <CheckCircle2 size={14} className="text-emerald-500" />;
189
+ case 'failed':
190
+ return <AlertCircle size={14} className="text-red-500" />;
191
+ default:
192
+ return <div className="w-2 h-2 rounded-full bg-gray-600" />;
193
+ }
194
+ }
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import { JobQueue } from './JobQueue';
4
+ import { clearQueueAction, killCurrentJobAction } from '../app/actions';
5
+
6
+ interface JobRecord {
7
+ id: number;
8
+ name: string;
9
+ data: string;
10
+ status: string;
11
+ created_at: number;
12
+ updated_at: number;
13
+ }
14
+
15
+ export function JobQueueWrapper({ jobs }: { jobs: JobRecord[] }) {
16
+ const handleKillJob = async () => {
17
+ await killCurrentJobAction();
18
+ };
19
+
20
+ const handleClearQueue = async () => {
21
+ await clearQueueAction();
22
+ };
23
+
24
+ return (
25
+ <JobQueue
26
+ jobs={jobs}
27
+ onKillJob={handleKillJob}
28
+ onClearQueue={handleClearQueue}
29
+ />
30
+ );
31
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ export function MermaidInit() {
6
+ useEffect(() => {
7
+ // Wait for mermaid to load and initialize
8
+ const initMermaid = () => {
9
+ if (typeof window !== 'undefined' && (window as any).mermaid) {
10
+ (window as any).mermaid.initialize({
11
+ startOnLoad: true,
12
+ theme: 'neutral',
13
+ securityLevel: 'loose'
14
+ });
15
+ } else {
16
+ // Retry if mermaid isn't loaded yet
17
+ setTimeout(initMermaid, 100);
18
+ }
19
+ };
20
+ initMermaid();
21
+ }, []);
22
+
23
+ return null;
24
+ }
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { FileText, Clock, RefreshCw } from 'lucide-react';
5
+ import { clsx } from 'clsx';
6
+ import { DocumentationViewer } from './DocumentationViewer';
7
+ import { FileList } from './FileList';
8
+ import { TriggerSettings } from './TriggerSettings';
9
+
10
+ interface DocFile {
11
+ name: string;
12
+ path: string;
13
+ content: string;
14
+ type: 'root' | 'docs';
15
+ }
16
+
17
+ interface FileRecord {
18
+ id: number;
19
+ path: string;
20
+ hash: string;
21
+ last_modified: number;
22
+ last_indexed: number;
23
+ }
24
+
25
+ interface TriggerConfig {
26
+ on_change: { enabled: boolean; debounce_ms: number };
27
+ on_commit: { enabled: boolean };
28
+ schedule: { enabled: boolean; interval_hours: number };
29
+ }
30
+
31
+ interface DocVersion {
32
+ id: number;
33
+ version_number: number;
34
+ trigger_type: string;
35
+ commit_hash: string | null;
36
+ status: 'draft' | 'pending_review' | 'active' | 'archived';
37
+ created_at: number;
38
+ }
39
+
40
+ interface ThoughtLog {
41
+ doc_path: string;
42
+ thinking_summary: string;
43
+ }
44
+
45
+ interface Props {
46
+ docs: DocFile[];
47
+ files: FileRecord[];
48
+ projectName: string;
49
+ projectPath: string;
50
+ hasProject: boolean;
51
+ triggerConfig: TriggerConfig | null;
52
+ docStage?: { currentStage: number; totalStages: number } | null;
53
+ isPaused: boolean;
54
+ docVersions?: DocVersion[];
55
+ activeVersionId?: number | null;
56
+ thoughtLogs?: ThoughtLog[];
57
+ }
58
+
59
+ type Tab = 'docs' | 'files';
60
+
61
+ export function ProjectContentArea({
62
+ docs,
63
+ files,
64
+ projectName,
65
+ projectPath,
66
+ hasProject,
67
+ triggerConfig,
68
+ docStage,
69
+ isPaused,
70
+ docVersions = [],
71
+ activeVersionId = null,
72
+ thoughtLogs = []
73
+ }: Props) {
74
+ const [activeTab, setActiveTab] = useState<Tab>('docs');
75
+ const [isUpdating, setIsUpdating] = useState(false);
76
+
77
+ if (!hasProject) {
78
+ return <FileList files={files} title="Recent Files" />;
79
+ }
80
+
81
+ return (
82
+ <div className="flex flex-col h-full">
83
+ {/* Tab Header */}
84
+ <div className="flex items-center gap-1 px-2 py-1.5 border-b border-gray-200 bg-gray-50 flex-none">
85
+ <button
86
+ onClick={() => setActiveTab('docs')}
87
+ className={clsx(
88
+ "flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors",
89
+ activeTab === 'docs'
90
+ ? "bg-white shadow-sm text-blue-600 ring-1 ring-gray-200"
91
+ : "text-gray-600 hover:bg-gray-100"
92
+ )}
93
+ >
94
+ <FileText size={12} />
95
+ Documentation
96
+ </button>
97
+ <button
98
+ onClick={() => setActiveTab('files')}
99
+ className={clsx(
100
+ "flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors",
101
+ activeTab === 'files'
102
+ ? "bg-white shadow-sm text-blue-600 ring-1 ring-gray-200"
103
+ : "text-gray-600 hover:bg-gray-100"
104
+ )}
105
+ >
106
+ <Clock size={12} />
107
+ Recent Files
108
+ </button>
109
+ <div className="flex-1" />
110
+
111
+ {/* Trigger Settings */}
112
+ {triggerConfig && (
113
+ <TriggerSettings
114
+ config={triggerConfig}
115
+ projectPath={projectPath}
116
+ />
117
+ )}
118
+
119
+ {/* Update Docs Button */}
120
+ {hasProject && (
121
+ <button
122
+ onClick={async () => {
123
+ setIsUpdating(true);
124
+ try {
125
+ const { triggerDocUpdate } = await import('../app/actions');
126
+ await triggerDocUpdate(projectPath);
127
+ } finally {
128
+ // Small delay so user sees the feedback
129
+ setTimeout(() => setIsUpdating(false), 1000);
130
+ }
131
+ }}
132
+ disabled={isUpdating}
133
+ className={clsx(
134
+ "flex items-center gap-1.5 px-2 py-1 rounded ml-2 text-[10px] font-bold uppercase tracking-wider transition-colors",
135
+ isUpdating
136
+ ? "bg-blue-200 text-blue-500 cursor-wait"
137
+ : "bg-blue-100 text-blue-700 hover:bg-blue-200"
138
+ )}
139
+ title="Trigger incremental doc update"
140
+ >
141
+ <RefreshCw size={10} className={isUpdating ? 'animate-spin' : ''} />
142
+ {isUpdating ? 'Updating...' : 'Update'}
143
+ </button>
144
+ )}
145
+
146
+ {/* Pause/Resume Control */}
147
+ {hasProject && (
148
+ <button
149
+ onClick={async () => {
150
+ const { togglePause } = await import('../app/actions');
151
+ await togglePause(projectPath, !isPaused);
152
+ }}
153
+ className={clsx(
154
+ "flex items-center gap-1.5 px-2 py-1 rounded ml-2 text-[10px] font-bold uppercase tracking-wider transition-colors",
155
+ isPaused
156
+ ? "bg-amber-100 text-amber-700 hover:bg-amber-200"
157
+ : "bg-gray-100 text-gray-500 hover:bg-gray-200"
158
+ )}
159
+ title={isPaused ? "Resume Updates" : "Pause Updates"}
160
+ >
161
+ {isPaused ? "Paused" : "Active"}
162
+ </button>
163
+ )}
164
+
165
+ <span className="text-[10px] text-gray-400 font-mono ml-2 truncate max-w-[200px]">{projectName}</span>
166
+ </div>
167
+
168
+ {/* Tab Content */}
169
+ <div className="flex-1 overflow-hidden">
170
+ {activeTab === 'docs' ? (
171
+ <DocumentationViewer
172
+ docs={docs}
173
+ projectName={projectName}
174
+ docStage={docStage}
175
+ versions={docVersions}
176
+ currentVersionId={activeVersionId}
177
+ thoughtLogs={thoughtLogs}
178
+ />
179
+ ) : (
180
+ <FileList files={files} title={projectName} />
181
+ )}
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+
3
+ import { Folder, ExternalLink, Plus, Trash2 } from 'lucide-react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { clsx } from 'clsx';
6
+ import { addProjectAction, removeProjectAction } from '../app/actions';
7
+ import { useState } from 'react';
8
+
9
+ interface Project {
10
+ id: number;
11
+ path: string;
12
+ created_at: number;
13
+ }
14
+
15
+ export function ProjectList({ projects, activeProjectId }: { projects: Project[], activeProjectId?: string }) {
16
+ const router = useRouter();
17
+ const [isAdding, setIsAdding] = useState(false);
18
+ const [removingPath, setRemovingPath] = useState<string | null>(null);
19
+
20
+ const handleRemove = async (projectPath: string) => {
21
+ await removeProjectAction(projectPath);
22
+ setRemovingPath(null);
23
+ router.push('/');
24
+ };
25
+
26
+ return (
27
+ <div className="flex flex-col h-full bg-gray-50 border-r border-gray-200">
28
+ {/* Confirmation Modal */}
29
+ {removingPath && (
30
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
31
+ <div className="bg-white rounded-lg shadow-xl p-4 max-w-sm mx-4">
32
+ <h3 className="font-bold text-gray-800 mb-2">Remove Project?</h3>
33
+ <p className="text-sm text-gray-600 mb-4">
34
+ Stop monitoring <span className="font-mono text-xs bg-gray-100 px-1 rounded">{removingPath.split('/').pop()}</span>?
35
+ <br />
36
+ <span className="text-xs text-gray-400">Files will remain on disk but won&apos;t be tracked.</span>
37
+ </p>
38
+ <div className="flex justify-end gap-2">
39
+ <button
40
+ onClick={() => setRemovingPath(null)}
41
+ className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800"
42
+ >
43
+ Cancel
44
+ </button>
45
+ <button
46
+ onClick={() => handleRemove(removingPath)}
47
+ className="px-3 py-1.5 text-sm bg-red-500 text-white rounded hover:bg-red-600"
48
+ >
49
+ Remove
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ )}
55
+
56
+ <div className="p-3 border-b border-gray-200 bg-gray-100 flex items-center justify-between">
57
+ <h2 className="text-xs font-bold uppercase tracking-wider text-gray-500 flex items-center gap-2">
58
+ <Folder size={14} />
59
+ <span>Projects</span>
60
+ </h2>
61
+ <button
62
+ onClick={() => setIsAdding(!isAdding)}
63
+ className="p-1 hover:bg-gray-200 rounded text-gray-600 transition-colors"
64
+ title="Add Project"
65
+ >
66
+ <Plus size={14} />
67
+ </button>
68
+ </div>
69
+
70
+ {isAdding && (
71
+ <form action={async (formData) => {
72
+ await addProjectAction(formData);
73
+ setIsAdding(false);
74
+ }} className="p-2 border-b border-gray-200 bg-white">
75
+ <input
76
+ name="path"
77
+ autoFocus
78
+ placeholder="/absolute/path/to/repo"
79
+ className="w-full text-xs font-mono border border-gray-300 rounded px-2 py-1 mb-1 focus:outline-none focus:border-blue-500"
80
+ />
81
+ <div className="flex justify-end gap-2">
82
+ <button type="button" onClick={() => setIsAdding(false)} className="text-[10px] text-gray-500 hover:text-gray-800">Cancel</button>
83
+ <button type="submit" className="text-[10px] bg-blue-500 text-white px-2 py-0.5 rounded hover:bg-blue-600">Add</button>
84
+ </div>
85
+ </form>
86
+ )}
87
+
88
+ <div className="flex-1 overflow-y-auto p-1 space-y-0.5">
89
+ <div
90
+ onClick={() => router.push('/')}
91
+ className={clsx(
92
+ "group flex items-center gap-2 p-2 rounded cursor-pointer transition-all",
93
+ !activeProjectId ? "bg-white shadow-sm ring-1 ring-gray-200" : "hover:bg-gray-100 text-gray-600"
94
+ )}
95
+ >
96
+ <Folder size={14} className={clsx(!activeProjectId ? "text-blue-500" : "text-gray-400")} />
97
+ <span className="font-mono text-xs font-medium">All Projects</span>
98
+ </div>
99
+
100
+ {projects.map(project => {
101
+ const isActive = activeProjectId === String(project.id);
102
+ return (
103
+ <div
104
+ key={project.id}
105
+ onClick={() => router.push(`/?projectId=${project.id}`)}
106
+ className={clsx(
107
+ "group flex items-center gap-2 p-2 rounded cursor-pointer transition-all",
108
+ isActive ? "bg-white shadow-sm ring-1 ring-gray-200" : "hover:bg-gray-100 text-gray-600"
109
+ )}
110
+ >
111
+ <Folder size={14} className={clsx(isActive ? "text-blue-500" : "text-gray-400")} />
112
+ <div className="flex flex-col min-w-0 flex-1">
113
+ <span className="font-mono text-xs font-medium truncate" title={project.path}>
114
+ {project.path.split('/').pop()}
115
+ </span>
116
+ <span className="text-[10px] text-gray-400 truncate hidden group-hover:block">
117
+ {project.path}
118
+ </span>
119
+ </div>
120
+ <button
121
+ onClick={(e) => {
122
+ e.stopPropagation();
123
+ setRemovingPath(project.path);
124
+ }}
125
+ className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 rounded text-gray-400 hover:text-red-500 transition-all"
126
+ title="Remove project"
127
+ >
128
+ <Trash2 size={12} />
129
+ </button>
130
+ </div>
131
+ );
132
+ })}
133
+ </div>
134
+ </div>
135
+ );
136
+ }