@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.
- package/CLAUDE.md +4 -0
- package/README.md +264 -0
- package/app/api/auth/[...nextauth]/route.ts +3 -0
- package/app/api/claude/[id]/route.ts +31 -0
- package/app/api/claude/[id]/stream/route.ts +63 -0
- package/app/api/claude/route.ts +28 -0
- package/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/app/api/claude-sessions/sync/route.ts +17 -0
- package/app/api/flows/route.ts +6 -0
- package/app/api/flows/run/route.ts +19 -0
- package/app/api/notify/test/route.ts +33 -0
- package/app/api/projects/route.ts +7 -0
- package/app/api/sessions/[id]/chat/route.ts +64 -0
- package/app/api/sessions/[id]/messages/route.ts +9 -0
- package/app/api/sessions/[id]/route.ts +17 -0
- package/app/api/sessions/route.ts +20 -0
- package/app/api/settings/route.ts +15 -0
- package/app/api/status/route.ts +12 -0
- package/app/api/tasks/[id]/route.ts +36 -0
- package/app/api/tasks/[id]/stream/route.ts +77 -0
- package/app/api/tasks/link/route.ts +37 -0
- package/app/api/tasks/route.ts +43 -0
- package/app/api/tasks/session/route.ts +14 -0
- package/app/api/templates/route.ts +6 -0
- package/app/api/tunnel/route.ts +20 -0
- package/app/api/watchers/route.ts +33 -0
- package/app/globals.css +26 -0
- package/app/icon.svg +26 -0
- package/app/layout.tsx +17 -0
- package/app/login/page.tsx +61 -0
- package/app/page.tsx +9 -0
- package/cli/mw.ts +377 -0
- package/components/ChatPanel.tsx +191 -0
- package/components/ClaudeTerminal.tsx +267 -0
- package/components/Dashboard.tsx +270 -0
- package/components/MarkdownContent.tsx +57 -0
- package/components/NewSessionModal.tsx +93 -0
- package/components/NewTaskModal.tsx +456 -0
- package/components/ProjectList.tsx +108 -0
- package/components/SessionList.tsx +74 -0
- package/components/SessionView.tsx +655 -0
- package/components/SettingsModal.tsx +366 -0
- package/components/StatusBar.tsx +99 -0
- package/components/TaskBoard.tsx +110 -0
- package/components/TaskDetail.tsx +351 -0
- package/components/TunnelToggle.tsx +163 -0
- package/components/WebTerminal.tsx +1069 -0
- package/docs/LOCAL-DEPLOY.md +144 -0
- package/docs/roadmap-multi-agent-workflow.md +330 -0
- package/instrumentation.ts +14 -0
- package/lib/auth.ts +47 -0
- package/lib/claude-process.ts +352 -0
- package/lib/claude-sessions.ts +267 -0
- package/lib/cloudflared.ts +218 -0
- package/lib/flows.ts +86 -0
- package/lib/init.ts +82 -0
- package/lib/notify.ts +75 -0
- package/lib/password.ts +77 -0
- package/lib/projects.ts +86 -0
- package/lib/session-manager.ts +156 -0
- package/lib/session-watcher.ts +345 -0
- package/lib/settings.ts +44 -0
- package/lib/task-manager.ts +668 -0
- package/lib/telegram-bot.ts +912 -0
- package/lib/terminal-server.ts +70 -0
- package/lib/terminal-standalone.ts +363 -0
- package/middleware.ts +33 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +16 -0
- package/package.json +66 -0
- package/postcss.config.mjs +7 -0
- package/src/config/index.ts +119 -0
- package/src/core/db/database.ts +133 -0
- package/src/core/memory/strategy.ts +32 -0
- package/src/core/providers/chat.ts +65 -0
- package/src/core/providers/registry.ts +60 -0
- package/src/core/session/manager.ts +190 -0
- package/src/types/index.ts +128 -0
- 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
|
+
}
|