@aion0/forge 0.1.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 (80) hide show
  1. package/CLAUDE.md +4 -0
  2. package/README.md +264 -0
  3. package/app/api/auth/[...nextauth]/route.ts +3 -0
  4. package/app/api/claude/[id]/route.ts +31 -0
  5. package/app/api/claude/[id]/stream/route.ts +63 -0
  6. package/app/api/claude/route.ts +28 -0
  7. package/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  8. package/app/api/claude-sessions/[projectName]/route.ts +37 -0
  9. package/app/api/claude-sessions/sync/route.ts +17 -0
  10. package/app/api/flows/route.ts +6 -0
  11. package/app/api/flows/run/route.ts +19 -0
  12. package/app/api/notify/test/route.ts +33 -0
  13. package/app/api/projects/route.ts +7 -0
  14. package/app/api/sessions/[id]/chat/route.ts +64 -0
  15. package/app/api/sessions/[id]/messages/route.ts +9 -0
  16. package/app/api/sessions/[id]/route.ts +17 -0
  17. package/app/api/sessions/route.ts +20 -0
  18. package/app/api/settings/route.ts +15 -0
  19. package/app/api/status/route.ts +12 -0
  20. package/app/api/tasks/[id]/route.ts +36 -0
  21. package/app/api/tasks/[id]/stream/route.ts +77 -0
  22. package/app/api/tasks/link/route.ts +37 -0
  23. package/app/api/tasks/route.ts +43 -0
  24. package/app/api/tasks/session/route.ts +14 -0
  25. package/app/api/templates/route.ts +6 -0
  26. package/app/api/tunnel/route.ts +20 -0
  27. package/app/api/watchers/route.ts +33 -0
  28. package/app/globals.css +26 -0
  29. package/app/icon.svg +26 -0
  30. package/app/layout.tsx +17 -0
  31. package/app/login/page.tsx +61 -0
  32. package/app/page.tsx +9 -0
  33. package/cli/mw.ts +377 -0
  34. package/components/ChatPanel.tsx +191 -0
  35. package/components/ClaudeTerminal.tsx +267 -0
  36. package/components/Dashboard.tsx +270 -0
  37. package/components/MarkdownContent.tsx +57 -0
  38. package/components/NewSessionModal.tsx +93 -0
  39. package/components/NewTaskModal.tsx +456 -0
  40. package/components/ProjectList.tsx +108 -0
  41. package/components/SessionList.tsx +74 -0
  42. package/components/SessionView.tsx +655 -0
  43. package/components/SettingsModal.tsx +366 -0
  44. package/components/StatusBar.tsx +99 -0
  45. package/components/TaskBoard.tsx +110 -0
  46. package/components/TaskDetail.tsx +351 -0
  47. package/components/TunnelToggle.tsx +163 -0
  48. package/components/WebTerminal.tsx +1069 -0
  49. package/docs/LOCAL-DEPLOY.md +144 -0
  50. package/docs/roadmap-multi-agent-workflow.md +330 -0
  51. package/instrumentation.ts +14 -0
  52. package/lib/auth.ts +47 -0
  53. package/lib/claude-process.ts +352 -0
  54. package/lib/claude-sessions.ts +267 -0
  55. package/lib/cloudflared.ts +218 -0
  56. package/lib/flows.ts +86 -0
  57. package/lib/init.ts +82 -0
  58. package/lib/notify.ts +75 -0
  59. package/lib/password.ts +77 -0
  60. package/lib/projects.ts +86 -0
  61. package/lib/session-manager.ts +156 -0
  62. package/lib/session-watcher.ts +345 -0
  63. package/lib/settings.ts +44 -0
  64. package/lib/task-manager.ts +668 -0
  65. package/lib/telegram-bot.ts +912 -0
  66. package/lib/terminal-server.ts +70 -0
  67. package/lib/terminal-standalone.ts +363 -0
  68. package/middleware.ts +33 -0
  69. package/next-env.d.ts +6 -0
  70. package/next.config.ts +16 -0
  71. package/package.json +66 -0
  72. package/postcss.config.mjs +7 -0
  73. package/src/config/index.ts +119 -0
  74. package/src/core/db/database.ts +133 -0
  75. package/src/core/memory/strategy.ts +32 -0
  76. package/src/core/providers/chat.ts +65 -0
  77. package/src/core/providers/registry.ts +60 -0
  78. package/src/core/session/manager.ts +190 -0
  79. package/src/types/index.ts +128 -0
  80. package/tsconfig.json +41 -0
@@ -0,0 +1,456 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import type { TaskMode, WatchConfig } from '@/src/types';
5
+
6
+ interface Project {
7
+ name: string;
8
+ path: string;
9
+ language: string | null;
10
+ }
11
+
12
+ interface SessionInfo {
13
+ sessionId: string;
14
+ summary?: string;
15
+ firstPrompt?: string;
16
+ modified?: string;
17
+ gitBranch?: string;
18
+ }
19
+
20
+ export default function NewTaskModal({
21
+ onClose,
22
+ onCreate,
23
+ }: {
24
+ onClose: () => void;
25
+ onCreate: (data: {
26
+ projectName: string;
27
+ prompt: string;
28
+ priority?: number;
29
+ conversationId?: string;
30
+ newSession?: boolean;
31
+ scheduledAt?: string;
32
+ mode?: TaskMode;
33
+ watchConfig?: WatchConfig;
34
+ }) => void;
35
+ }) {
36
+ const [projects, setProjects] = useState<Project[]>([]);
37
+ const [selectedProject, setSelectedProject] = useState('');
38
+ const [prompt, setPrompt] = useState('');
39
+ const [priority, setPriority] = useState(0);
40
+
41
+ // Task mode
42
+ const [taskMode, setTaskMode] = useState<TaskMode>('prompt');
43
+
44
+ // Monitor config
45
+ const [watchCondition, setWatchCondition] = useState<WatchConfig['condition']>('change');
46
+ const [watchKeyword, setWatchKeyword] = useState('');
47
+ const [watchIdleMinutes, setWatchIdleMinutes] = useState(10);
48
+ const [watchAction, setWatchAction] = useState<WatchConfig['action']>('notify');
49
+ const [watchActionPrompt, setWatchActionPrompt] = useState('');
50
+ const [watchRepeat, setWatchRepeat] = useState(false);
51
+
52
+ // Session selection
53
+ const [sessionMode, setSessionMode] = useState<'auto' | 'select' | 'new'>('auto');
54
+ const [sessions, setSessions] = useState<SessionInfo[]>([]);
55
+ const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
56
+ const [autoSessionId, setAutoSessionId] = useState<string | null>(null);
57
+
58
+ // Scheduling
59
+ const [scheduleMode, setScheduleMode] = useState<'now' | 'delay' | 'time'>('now');
60
+ const [delayMinutes, setDelayMinutes] = useState(30);
61
+ const [scheduledTime, setScheduledTime] = useState('');
62
+
63
+ useEffect(() => {
64
+ fetch('/api/projects').then(r => r.json()).then((p: Project[]) => {
65
+ setProjects(p);
66
+ if (p.length > 0) setSelectedProject(p[0].name);
67
+ });
68
+ }, []);
69
+
70
+ // Fetch sessions when project changes
71
+ useEffect(() => {
72
+ if (!selectedProject) return;
73
+
74
+ // Get auto-inherited session
75
+ fetch(`/api/tasks/session?project=${encodeURIComponent(selectedProject)}`)
76
+ .then(r => r.json())
77
+ .then(data => setAutoSessionId(data.conversationId || null))
78
+ .catch(() => setAutoSessionId(null));
79
+
80
+ // Get all sessions for picker
81
+ fetch(`/api/claude-sessions/${encodeURIComponent(selectedProject)}`)
82
+ .then(r => r.json())
83
+ .then((s: SessionInfo[]) => setSessions(s))
84
+ .catch(() => setSessions([]));
85
+ }, [selectedProject]);
86
+
87
+ const getScheduledAt = (): string | undefined => {
88
+ if (scheduleMode === 'now') return undefined;
89
+ if (scheduleMode === 'delay') {
90
+ return new Date(Date.now() + delayMinutes * 60_000).toISOString();
91
+ }
92
+ if (scheduleMode === 'time' && scheduledTime) {
93
+ return new Date(scheduledTime).toISOString();
94
+ }
95
+ return undefined;
96
+ };
97
+
98
+ const handleSubmit = (e: React.FormEvent) => {
99
+ e.preventDefault();
100
+ if (!selectedProject) return;
101
+ // Monitor mode requires session selection; prompt mode requires prompt text
102
+ if (taskMode === 'prompt' && !prompt.trim()) return;
103
+ if (taskMode === 'monitor' && sessionMode !== 'select') return;
104
+
105
+ const data: Parameters<typeof onCreate>[0] = {
106
+ projectName: selectedProject,
107
+ prompt: taskMode === 'monitor' ? `Monitor session ${selectedSessionId}` : prompt.trim(),
108
+ priority,
109
+ scheduledAt: getScheduledAt(),
110
+ mode: taskMode,
111
+ };
112
+
113
+ if (sessionMode === 'new') {
114
+ data.newSession = true;
115
+ } else if (sessionMode === 'select' && selectedSessionId) {
116
+ data.conversationId = selectedSessionId;
117
+ }
118
+
119
+ if (taskMode === 'monitor') {
120
+ const wc: WatchConfig = {
121
+ condition: watchCondition,
122
+ action: watchAction,
123
+ repeat: watchRepeat,
124
+ };
125
+ if (watchCondition === 'keyword') wc.keyword = watchKeyword;
126
+ if (watchCondition === 'idle') wc.idleMinutes = watchIdleMinutes;
127
+ if (watchAction !== 'notify') wc.actionPrompt = watchActionPrompt;
128
+ data.watchConfig = wc;
129
+ }
130
+
131
+ onCreate(data);
132
+ };
133
+
134
+ return (
135
+ <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={onClose}>
136
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg w-[560px] max-h-[80vh] overflow-auto" onClick={e => e.stopPropagation()}>
137
+ <div className="p-4 border-b border-[var(--border)]">
138
+ <h2 className="text-sm font-semibold">New Task</h2>
139
+ <p className="text-[11px] text-[var(--text-secondary)] mt-0.5">
140
+ Submit a task for Claude Code to work on autonomously
141
+ </p>
142
+ </div>
143
+
144
+ <form onSubmit={handleSubmit} className="p-4 space-y-4">
145
+ {/* Project */}
146
+ <div>
147
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">Project</label>
148
+ <select
149
+ value={selectedProject}
150
+ onChange={e => { setSelectedProject(e.target.value); setSelectedSessionId(null); }}
151
+ className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
152
+ >
153
+ {projects.map(p => (
154
+ <option key={p.name} value={p.name}>
155
+ {p.name} {p.language ? `(${p.language})` : ''}
156
+ </option>
157
+ ))}
158
+ </select>
159
+ </div>
160
+
161
+ {/* Task Mode */}
162
+ <div>
163
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">Mode</label>
164
+ <div className="flex gap-2">
165
+ {([
166
+ { value: 'prompt' as const, label: 'Prompt', desc: 'Send a message to Claude' },
167
+ { value: 'monitor' as const, label: 'Monitor', desc: 'Watch a session, trigger actions' },
168
+ ]).map(m => (
169
+ <button
170
+ key={m.value}
171
+ type="button"
172
+ onClick={() => {
173
+ setTaskMode(m.value);
174
+ if (m.value === 'monitor') setSessionMode('select');
175
+ }}
176
+ className={`text-[11px] px-3 py-1 rounded border transition-colors ${
177
+ taskMode === m.value
178
+ ? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10'
179
+ : 'border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--text-secondary)]'
180
+ }`}
181
+ >
182
+ {m.label}
183
+ </button>
184
+ ))}
185
+ </div>
186
+ <p className="text-[10px] text-[var(--text-secondary)] mt-1">
187
+ {taskMode === 'prompt' ? 'Send a message to Claude to work on autonomously' : 'Watch a session and trigger actions on conditions'}
188
+ </p>
189
+ </div>
190
+
191
+ {/* Session */}
192
+ <div>
193
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">Session</label>
194
+ {taskMode === 'prompt' && (
195
+ <div className="flex gap-2">
196
+ {(['auto', 'select', 'new'] as const).map(mode => (
197
+ <button
198
+ key={mode}
199
+ type="button"
200
+ onClick={() => setSessionMode(mode)}
201
+ className={`text-[11px] px-3 py-1 rounded border transition-colors ${
202
+ sessionMode === mode
203
+ ? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10'
204
+ : 'border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--text-secondary)]'
205
+ }`}
206
+ >
207
+ {mode === 'auto' ? 'Auto Continue' : mode === 'select' ? 'Choose Session' : 'New Session'}
208
+ </button>
209
+ ))}
210
+ </div>
211
+ )}
212
+ {taskMode === 'monitor' && (
213
+ <p className="text-[10px] text-[var(--text-secondary)]">Select a session to monitor</p>
214
+ )}
215
+
216
+ {sessionMode === 'auto' && taskMode === 'prompt' && (
217
+ <p className="text-[10px] text-[var(--text-secondary)] mt-1">
218
+ {autoSessionId
219
+ ? <>Will continue <span className="font-mono text-[var(--accent)]">{autoSessionId.slice(0, 12)}</span></>
220
+ : 'No existing session — will start new'}
221
+ </p>
222
+ )}
223
+
224
+ {sessionMode === 'select' && (
225
+ <div className="mt-2 max-h-32 overflow-y-auto border border-[var(--border)] rounded">
226
+ {sessions.length === 0 ? (
227
+ <p className="text-[10px] text-[var(--text-secondary)] p-2">No sessions found</p>
228
+ ) : sessions.map(s => (
229
+ <button
230
+ key={s.sessionId}
231
+ type="button"
232
+ onClick={() => setSelectedSessionId(s.sessionId)}
233
+ className={`w-full text-left px-2 py-1.5 text-[10px] hover:bg-[var(--bg-tertiary)] transition-colors ${
234
+ selectedSessionId === s.sessionId ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : ''
235
+ }`}
236
+ >
237
+ <div className="text-[var(--text-primary)] truncate">
238
+ {s.summary || s.firstPrompt?.slice(0, 50) || s.sessionId.slice(0, 8)}
239
+ </div>
240
+ <div className="flex gap-2 mt-0.5">
241
+ <span className="font-mono text-[var(--text-secondary)]">{s.sessionId.slice(0, 8)}</span>
242
+ {s.gitBranch && <span className="text-[var(--accent)]">{s.gitBranch}</span>}
243
+ {s.modified && <span className="text-[var(--text-secondary)]">{new Date(s.modified).toLocaleDateString()}</span>}
244
+ </div>
245
+ </button>
246
+ ))}
247
+ </div>
248
+ )}
249
+
250
+ {sessionMode === 'new' && (
251
+ <p className="text-[10px] text-[var(--text-secondary)] mt-1">
252
+ Will start a fresh session with no prior context
253
+ </p>
254
+ )}
255
+ </div>
256
+
257
+ {/* Monitor Config — only in monitor mode */}
258
+ {taskMode === 'monitor' && (
259
+ <div className="space-y-3 p-3 bg-[var(--bg-tertiary)] rounded border border-[var(--border)]">
260
+ <div>
261
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">Trigger when</label>
262
+ <div className="flex gap-1 flex-wrap">
263
+ {([
264
+ { value: 'change' as const, label: 'Content changes' },
265
+ { value: 'idle' as const, label: 'Session idle' },
266
+ { value: 'complete' as const, label: 'Session completes' },
267
+ { value: 'error' as const, label: 'Error occurs' },
268
+ { value: 'keyword' as const, label: 'Keyword found' },
269
+ ]).map(c => (
270
+ <button
271
+ key={c.value}
272
+ type="button"
273
+ onClick={() => setWatchCondition(c.value)}
274
+ className={`text-[10px] px-2 py-0.5 rounded border transition-colors ${
275
+ watchCondition === c.value
276
+ ? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10'
277
+ : 'border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--text-secondary)]'
278
+ }`}
279
+ >
280
+ {c.label}
281
+ </button>
282
+ ))}
283
+ </div>
284
+ {watchCondition === 'keyword' && (
285
+ <input
286
+ type="text"
287
+ value={watchKeyword}
288
+ onChange={e => setWatchKeyword(e.target.value)}
289
+ placeholder="Enter keyword to watch for..."
290
+ className="mt-2 w-full px-2 py-1 bg-[var(--bg-secondary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none"
291
+ />
292
+ )}
293
+ {watchCondition === 'idle' && (
294
+ <div className="flex items-center gap-2 mt-2">
295
+ <span className="text-[10px] text-[var(--text-secondary)]">Idle for</span>
296
+ <input
297
+ type="number"
298
+ value={watchIdleMinutes}
299
+ onChange={e => setWatchIdleMinutes(Number(e.target.value))}
300
+ min={1}
301
+ className="w-16 px-2 py-1 bg-[var(--bg-secondary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none"
302
+ />
303
+ <span className="text-[10px] text-[var(--text-secondary)]">minutes</span>
304
+ </div>
305
+ )}
306
+ </div>
307
+
308
+ <div>
309
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">Then</label>
310
+ <div className="flex gap-1 flex-wrap">
311
+ {([
312
+ { value: 'notify' as const, label: 'Send Telegram notification' },
313
+ { value: 'message' as const, label: 'Send message to session' },
314
+ { value: 'task' as const, label: 'Create new task' },
315
+ ]).map(a => (
316
+ <button
317
+ key={a.value}
318
+ type="button"
319
+ onClick={() => setWatchAction(a.value)}
320
+ className={`text-[10px] px-2 py-0.5 rounded border transition-colors ${
321
+ watchAction === a.value
322
+ ? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10'
323
+ : 'border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--text-secondary)]'
324
+ }`}
325
+ >
326
+ {a.label}
327
+ </button>
328
+ ))}
329
+ </div>
330
+ {watchAction !== 'notify' && (
331
+ <textarea
332
+ value={watchActionPrompt}
333
+ onChange={e => setWatchActionPrompt(e.target.value)}
334
+ placeholder={watchAction === 'message' ? 'Message to send to the session...' : 'Prompt for the new task...'}
335
+ rows={2}
336
+ className="mt-2 w-full px-2 py-1 bg-[var(--bg-secondary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] resize-none focus:outline-none"
337
+ />
338
+ )}
339
+ </div>
340
+
341
+ <label className="flex items-center gap-2 cursor-pointer">
342
+ <input
343
+ type="checkbox"
344
+ checked={watchRepeat}
345
+ onChange={e => setWatchRepeat(e.target.checked)}
346
+ className="rounded"
347
+ />
348
+ <span className="text-[10px] text-[var(--text-secondary)]">Keep watching after trigger (repeat)</span>
349
+ </label>
350
+ </div>
351
+ )}
352
+
353
+ {/* Task prompt — only in prompt mode */}
354
+ {taskMode === 'prompt' && (
355
+ <div>
356
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">What should Claude do?</label>
357
+ <textarea
358
+ value={prompt}
359
+ onChange={e => setPrompt(e.target.value)}
360
+ placeholder="e.g. Refactor the authentication module to use JWT tokens..."
361
+ rows={5}
362
+ className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] resize-none focus:outline-none focus:border-[var(--accent)]"
363
+ autoFocus
364
+ />
365
+ </div>
366
+ )}
367
+
368
+ {/* Schedule */}
369
+ <div>
370
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">When</label>
371
+ <div className="flex gap-2">
372
+ {(['now', 'delay', 'time'] as const).map(mode => (
373
+ <button
374
+ key={mode}
375
+ type="button"
376
+ onClick={() => setScheduleMode(mode)}
377
+ className={`text-[11px] px-3 py-1 rounded border transition-colors ${
378
+ scheduleMode === mode
379
+ ? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10'
380
+ : 'border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--text-secondary)]'
381
+ }`}
382
+ >
383
+ {mode === 'now' ? 'Now' : mode === 'delay' ? 'Delay' : 'Schedule'}
384
+ </button>
385
+ ))}
386
+ </div>
387
+
388
+ {scheduleMode === 'delay' && (
389
+ <div className="flex items-center gap-2 mt-2">
390
+ <span className="text-[10px] text-[var(--text-secondary)]">Run in</span>
391
+ <input
392
+ type="number"
393
+ value={delayMinutes}
394
+ onChange={e => setDelayMinutes(Number(e.target.value))}
395
+ min={1}
396
+ className="w-20 px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none"
397
+ />
398
+ <span className="text-[10px] text-[var(--text-secondary)]">minutes</span>
399
+ </div>
400
+ )}
401
+
402
+ {scheduleMode === 'time' && (
403
+ <div className="mt-2">
404
+ <input
405
+ type="datetime-local"
406
+ value={scheduledTime}
407
+ onChange={e => setScheduledTime(e.target.value)}
408
+ className="px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none"
409
+ />
410
+ </div>
411
+ )}
412
+ </div>
413
+
414
+ {/* Priority */}
415
+ <div>
416
+ <label className="text-[11px] text-[var(--text-secondary)] block mb-1">Priority</label>
417
+ <div className="flex gap-2">
418
+ {[
419
+ { value: 0, label: 'Normal' },
420
+ { value: 1, label: 'High' },
421
+ { value: 2, label: 'Urgent' },
422
+ ].map(p => (
423
+ <button
424
+ key={p.value}
425
+ type="button"
426
+ onClick={() => setPriority(p.value)}
427
+ className={`text-[11px] px-3 py-1 rounded border transition-colors ${
428
+ priority === p.value
429
+ ? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10'
430
+ : 'border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--text-secondary)]'
431
+ }`}
432
+ >
433
+ {p.label}
434
+ </button>
435
+ ))}
436
+ </div>
437
+ </div>
438
+
439
+ {/* Actions */}
440
+ <div className="flex justify-end gap-2 pt-2">
441
+ <button type="button" onClick={onClose} className="text-xs px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)]">
442
+ Cancel
443
+ </button>
444
+ <button
445
+ type="submit"
446
+ disabled={!selectedProject || (taskMode === 'prompt' && !prompt.trim()) || (taskMode === 'monitor' && !selectedSessionId)}
447
+ className="text-xs px-4 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50"
448
+ >
449
+ {taskMode === 'monitor' ? 'Start Monitor' : scheduleMode === 'now' ? 'Submit Task' : 'Schedule Task'}
450
+ </button>
451
+ </div>
452
+ </form>
453
+ </div>
454
+ </div>
455
+ );
456
+ }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ interface LocalProject {
6
+ name: string;
7
+ path: string;
8
+ hasGit: boolean;
9
+ hasClaudeMd: boolean;
10
+ language: string | null;
11
+ lastModified: string;
12
+ }
13
+
14
+ interface ClaudeProcess {
15
+ id: string;
16
+ projectName: string;
17
+ status: string;
18
+ }
19
+
20
+ const langIcons: Record<string, string> = {
21
+ java: 'JV',
22
+ kotlin: 'KT',
23
+ typescript: 'TS',
24
+ python: 'PY',
25
+ go: 'GO',
26
+ rust: 'RS',
27
+ };
28
+
29
+ export default function ProjectList({
30
+ onLaunch,
31
+ claudeProcesses,
32
+ }: {
33
+ onLaunch: (projectName: string) => void;
34
+ claudeProcesses: ClaudeProcess[];
35
+ }) {
36
+ const [projects, setProjects] = useState<LocalProject[]>([]);
37
+ const [filter, setFilter] = useState('');
38
+
39
+ useEffect(() => {
40
+ fetch('/api/projects').then(r => r.json()).then(setProjects);
41
+ }, []);
42
+
43
+ const filtered = projects.filter(p =>
44
+ p.name.toLowerCase().includes(filter.toLowerCase())
45
+ );
46
+
47
+ const getProcessForProject = (name: string) =>
48
+ claudeProcesses.find(p => p.projectName === name && p.status === 'running');
49
+
50
+ return (
51
+ <div className="flex flex-col h-full">
52
+ <div className="p-3 border-b border-[var(--border)]">
53
+ <input
54
+ value={filter}
55
+ onChange={e => setFilter(e.target.value)}
56
+ placeholder="Filter projects..."
57
+ className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
58
+ />
59
+ </div>
60
+ <div className="flex-1 overflow-y-auto">
61
+ {filtered.map(p => {
62
+ const proc = getProcessForProject(p.name);
63
+ return (
64
+ <div
65
+ key={p.name}
66
+ className="flex items-center justify-between px-3 py-2 border-b border-[var(--border)] hover:bg-[var(--bg-tertiary)] group"
67
+ >
68
+ <div className="min-w-0">
69
+ <div className="flex items-center gap-2">
70
+ {p.language && (
71
+ <span className="text-[9px] px-1 py-0.5 bg-[var(--bg-tertiary)] rounded text-[var(--text-secondary)] font-mono">
72
+ {langIcons[p.language] || p.language.slice(0, 2).toUpperCase()}
73
+ </span>
74
+ )}
75
+ <span className="text-xs font-medium truncate">{p.name}</span>
76
+ {p.hasClaudeMd && (
77
+ <span className="text-[9px] text-[var(--accent)]" title="Has CLAUDE.md">C</span>
78
+ )}
79
+ </div>
80
+ </div>
81
+ <div className="flex items-center gap-1 shrink-0">
82
+ {proc ? (
83
+ <span className="text-[9px] text-[var(--green)]">● running</span>
84
+ ) : (
85
+ <button
86
+ onClick={() => onLaunch(p.name)}
87
+ className="text-[10px] px-2 py-0.5 bg-[var(--accent)] text-white rounded opacity-0 group-hover:opacity-100 transition-opacity"
88
+ >
89
+ Launch Claude
90
+ </button>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ })}
96
+ </div>
97
+ {filtered.length === 0 && projects.length === 0 && (
98
+ <div className="p-4 text-center text-xs text-[var(--text-secondary)] space-y-2">
99
+ <p>No projects found</p>
100
+ <p className="text-[10px]">Go to Settings to add project directories</p>
101
+ </div>
102
+ )}
103
+ <div className="p-2 border-t border-[var(--border)] text-[10px] text-[var(--text-secondary)]">
104
+ {projects.length} projects
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import type { Session } from '@/src/types';
4
+
5
+ const statusConfig = {
6
+ running: { icon: '●', color: 'text-[var(--green)]' },
7
+ idle: { icon: '●', color: 'text-[var(--accent)]' },
8
+ paused: { icon: '○', color: 'text-[var(--yellow)]' },
9
+ archived: { icon: '○', color: 'text-[var(--text-secondary)]' },
10
+ error: { icon: '●', color: 'text-[var(--red)]' },
11
+ };
12
+
13
+ const providerLabels: Record<string, string> = {
14
+ anthropic: 'Claude',
15
+ google: 'Gemini',
16
+ openai: 'OpenAI',
17
+ grok: 'Grok',
18
+ };
19
+
20
+ export default function SessionList({
21
+ sessions,
22
+ activeId,
23
+ onSelect,
24
+ }: {
25
+ sessions: Session[];
26
+ activeId: string | null;
27
+ onSelect: (id: string) => void;
28
+ }) {
29
+ return (
30
+ <div className="flex-1 overflow-y-auto">
31
+ {sessions.map(s => {
32
+ const cfg = statusConfig[s.status] || statusConfig.idle;
33
+ const isActive = s.id === activeId;
34
+
35
+ return (
36
+ <button
37
+ key={s.id}
38
+ onClick={() => onSelect(s.id)}
39
+ className={`w-full text-left px-3 py-2.5 border-b border-[var(--border)] hover:bg-[var(--bg-tertiary)] transition-colors ${
40
+ isActive ? 'bg-[var(--bg-tertiary)] border-l-2 border-l-[var(--accent)]' : ''
41
+ }`}
42
+ >
43
+ <div className="flex items-center gap-2">
44
+ <span className={`text-xs ${cfg.color}`}>{cfg.icon}</span>
45
+ <span className="text-sm font-medium truncate">{s.name}</span>
46
+ </div>
47
+ <div className="flex items-center gap-2 mt-0.5 ml-4">
48
+ <span className="text-[10px] text-[var(--text-secondary)]">
49
+ {providerLabels[s.provider] || s.provider}
50
+ </span>
51
+ <span className="text-[10px] text-[var(--text-secondary)]">
52
+ {s.memory.strategy}
53
+ </span>
54
+ <span className="text-[10px] text-[var(--text-secondary)]">
55
+ {s.messageCount}msg
56
+ </span>
57
+ </div>
58
+ {s.lastMessage && (
59
+ <p className="text-[10px] text-[var(--text-secondary)] mt-0.5 ml-4 truncate">
60
+ {s.lastMessage.slice(0, 60)}
61
+ </p>
62
+ )}
63
+ </button>
64
+ );
65
+ })}
66
+
67
+ {sessions.length === 0 && (
68
+ <div className="p-4 text-center text-xs text-[var(--text-secondary)]">
69
+ No sessions yet
70
+ </div>
71
+ )}
72
+ </div>
73
+ );
74
+ }