idea-manager 0.6.0 → 0.6.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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/app/projects/[id]/page.tsx +30 -3
- package/src/components/task/ProjectTree.tsx +14 -1
- package/src/components/task/TaskChat.tsx +27 -3
- package/src/components/task/TaskDetail.tsx +7 -1
- package/src/components/task/TaskList.tsx +14 -1
- package/src/lib/db/queries/projects.ts +22 -4
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/watcher.ts +68 -22
- package/src/types/index.ts +1 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ interface IProject {
|
|
|
16
16
|
description: string;
|
|
17
17
|
project_path: string | null;
|
|
18
18
|
ai_context: string;
|
|
19
|
+
watch_enabled: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function WorkspaceInner({ id }: { id: string }) {
|
|
@@ -189,9 +190,10 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
189
190
|
}
|
|
190
191
|
};
|
|
191
192
|
|
|
192
|
-
const handleTaskDelete = () => {
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
const handleTaskDelete = (taskId?: string) => {
|
|
194
|
+
const id = taskId || selectedTaskId;
|
|
195
|
+
if (!id) return;
|
|
196
|
+
setConfirmAction({ type: 'delete-task', id });
|
|
195
197
|
};
|
|
196
198
|
|
|
197
199
|
const handleConfirmAction = async () => {
|
|
@@ -236,6 +238,18 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
236
238
|
}
|
|
237
239
|
};
|
|
238
240
|
|
|
241
|
+
const handleToggleWatch = async () => {
|
|
242
|
+
if (!project) return;
|
|
243
|
+
const res = await fetch(`/api/projects/${id}`, {
|
|
244
|
+
method: 'PUT',
|
|
245
|
+
headers: { 'Content-Type': 'application/json' },
|
|
246
|
+
body: JSON.stringify({ watch_enabled: !project.watch_enabled }),
|
|
247
|
+
});
|
|
248
|
+
if (res.ok) {
|
|
249
|
+
setProject(await res.json());
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
239
253
|
// Keyboard shortcuts (use e.code for Korean IME compatibility)
|
|
240
254
|
useEffect(() => {
|
|
241
255
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -304,6 +318,18 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
304
318
|
)}
|
|
305
319
|
</div>
|
|
306
320
|
<div className="flex items-center gap-2">
|
|
321
|
+
<button
|
|
322
|
+
onClick={handleToggleWatch}
|
|
323
|
+
className={`px-3 py-1.5 text-xs border rounded-md transition-colors flex items-center gap-1.5 ${
|
|
324
|
+
project.watch_enabled
|
|
325
|
+
? 'bg-success/15 text-success border-success/30 hover:bg-success/25'
|
|
326
|
+
: 'bg-muted hover:bg-card-hover text-muted-foreground border-border'
|
|
327
|
+
}`}
|
|
328
|
+
title={project.watch_enabled ? 'Watch ON — submitted 태스크 자동 실행' : 'Watch OFF'}
|
|
329
|
+
>
|
|
330
|
+
<span className={`inline-block w-2 h-2 rounded-full ${project.watch_enabled ? 'bg-success animate-pulse' : 'bg-muted-foreground/40'}`} />
|
|
331
|
+
Watch
|
|
332
|
+
</button>
|
|
307
333
|
<button
|
|
308
334
|
onClick={() => setShowAiPolicy(true)}
|
|
309
335
|
className={`px-3 py-1.5 text-xs border rounded-md transition-colors ${
|
|
@@ -391,6 +417,7 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
391
417
|
onCreateTask={handleCreateTask}
|
|
392
418
|
onStatusChange={handleTaskStatusChange}
|
|
393
419
|
onTodayToggle={handleTaskTodayToggle}
|
|
420
|
+
onDeleteTask={handleTaskDelete}
|
|
394
421
|
/>
|
|
395
422
|
</div>
|
|
396
423
|
|
|
@@ -22,6 +22,7 @@ export default function ProjectTree({
|
|
|
22
22
|
onCreateTask,
|
|
23
23
|
onStatusChange,
|
|
24
24
|
onTodayToggle,
|
|
25
|
+
onDeleteTask,
|
|
25
26
|
}: {
|
|
26
27
|
subProjects: ISubProjectWithStats[];
|
|
27
28
|
tasks: ITask[];
|
|
@@ -34,6 +35,7 @@ export default function ProjectTree({
|
|
|
34
35
|
onCreateTask: (title: string) => void;
|
|
35
36
|
onStatusChange: (taskId: string, status: TaskStatus) => void;
|
|
36
37
|
onTodayToggle: (taskId: string, isToday: boolean) => void;
|
|
38
|
+
onDeleteTask: (taskId: string) => void;
|
|
37
39
|
}) {
|
|
38
40
|
const [collapsedSubs, setCollapsedSubs] = useState<Set<string>>(new Set());
|
|
39
41
|
const [addingTaskFor, setAddingTaskFor] = useState<string | null>(null);
|
|
@@ -130,7 +132,7 @@ export default function ProjectTree({
|
|
|
130
132
|
<div
|
|
131
133
|
key={task.id}
|
|
132
134
|
onClick={() => onSelectTask(task.id)}
|
|
133
|
-
className={`flex items-center gap-1.5 pl-4 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
|
|
135
|
+
className={`group/task flex items-center gap-1.5 pl-4 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
|
|
134
136
|
selectedTaskId === task.id
|
|
135
137
|
? 'bg-card-hover border-l-primary'
|
|
136
138
|
: 'border-l-transparent hover:bg-card-hover/50'
|
|
@@ -159,6 +161,17 @@ export default function ProjectTree({
|
|
|
159
161
|
*
|
|
160
162
|
</button>
|
|
161
163
|
)}
|
|
164
|
+
<button
|
|
165
|
+
onClick={(e) => {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
onDeleteTask(task.id);
|
|
168
|
+
}}
|
|
169
|
+
className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground
|
|
170
|
+
hover:!text-destructive transition-colors text-xs px-0.5"
|
|
171
|
+
title="Delete task"
|
|
172
|
+
>
|
|
173
|
+
×
|
|
174
|
+
</button>
|
|
162
175
|
</div>
|
|
163
176
|
))}
|
|
164
177
|
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
-
import type { ITaskConversation } from '@/types';
|
|
4
|
+
import type { ITaskConversation, TaskStatus } from '@/types';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
|
|
7
|
+
const POLL_INTERVAL = 3000; // Poll every 3s when task is testing
|
|
8
|
+
|
|
7
9
|
export default function TaskChat({
|
|
8
10
|
basePath,
|
|
11
|
+
taskStatus,
|
|
9
12
|
onApplyToPrompt,
|
|
10
13
|
}: {
|
|
11
14
|
basePath: string;
|
|
15
|
+
taskStatus?: TaskStatus;
|
|
12
16
|
onApplyToPrompt: (content: string) => void;
|
|
13
17
|
}) {
|
|
14
18
|
const [messages, setMessages] = useState<ITaskConversation[]>([]);
|
|
@@ -17,12 +21,26 @@ export default function TaskChat({
|
|
|
17
21
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
18
22
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
const fetchMessages = useCallback(() => {
|
|
21
25
|
fetch(`${basePath}/chat`)
|
|
22
26
|
.then(r => r.json())
|
|
23
|
-
.then(data =>
|
|
27
|
+
.then(data => {
|
|
28
|
+
if (Array.isArray(data)) setMessages(data);
|
|
29
|
+
});
|
|
24
30
|
}, [basePath]);
|
|
25
31
|
|
|
32
|
+
// Initial load
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
fetchMessages();
|
|
35
|
+
}, [fetchMessages]);
|
|
36
|
+
|
|
37
|
+
// Auto-poll when task is testing (watcher is running)
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (taskStatus !== 'testing') return;
|
|
40
|
+
const interval = setInterval(fetchMessages, POLL_INTERVAL);
|
|
41
|
+
return () => clearInterval(interval);
|
|
42
|
+
}, [taskStatus, fetchMessages]);
|
|
43
|
+
|
|
26
44
|
useEffect(() => {
|
|
27
45
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
28
46
|
}, [messages]);
|
|
@@ -71,6 +89,12 @@ export default function TaskChat({
|
|
|
71
89
|
<div className="flex flex-col h-full border-t border-border">
|
|
72
90
|
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
|
|
73
91
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">AI Chat</span>
|
|
92
|
+
{taskStatus === 'testing' && (
|
|
93
|
+
<span className="flex items-center gap-1.5 text-xs text-warning">
|
|
94
|
+
<span className="inline-block w-2 h-2 rounded-full bg-warning animate-pulse" />
|
|
95
|
+
Executing...
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
74
98
|
</div>
|
|
75
99
|
|
|
76
100
|
{/* Messages */}
|
|
@@ -28,11 +28,16 @@ export default function TaskDetail({
|
|
|
28
28
|
|
|
29
29
|
const basePath = `/api/projects/${projectId}/sub-projects/${subProjectId}/tasks/${task.id}`;
|
|
30
30
|
|
|
31
|
+
// Auto-show chat when task is being executed by watcher
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (task.status === 'testing') setShowChat(true);
|
|
34
|
+
}, [task.status]);
|
|
35
|
+
|
|
31
36
|
// Load prompt
|
|
32
37
|
useEffect(() => {
|
|
33
38
|
setTitle(task.title);
|
|
34
39
|
setDescription(task.description);
|
|
35
|
-
setShowChat(
|
|
40
|
+
setShowChat(task.status === 'testing');
|
|
36
41
|
fetch(`${basePath}/prompt`)
|
|
37
42
|
.then(r => r.json())
|
|
38
43
|
.then(data => setPromptContent(data.content || ''));
|
|
@@ -196,6 +201,7 @@ export default function TaskDetail({
|
|
|
196
201
|
<div className="h-[45%] flex-shrink-0">
|
|
197
202
|
<TaskChat
|
|
198
203
|
basePath={basePath}
|
|
204
|
+
taskStatus={task.status}
|
|
199
205
|
onApplyToPrompt={handleApplyToPrompt}
|
|
200
206
|
/>
|
|
201
207
|
</div>
|
|
@@ -17,6 +17,7 @@ export default function TaskList({
|
|
|
17
17
|
onCreate,
|
|
18
18
|
onStatusChange,
|
|
19
19
|
onTodayToggle,
|
|
20
|
+
onDelete,
|
|
20
21
|
}: {
|
|
21
22
|
tasks: ITask[];
|
|
22
23
|
selectedTaskId: string | null;
|
|
@@ -24,6 +25,7 @@ export default function TaskList({
|
|
|
24
25
|
onCreate: (title: string) => void;
|
|
25
26
|
onStatusChange: (taskId: string, status: TaskStatus) => void;
|
|
26
27
|
onTodayToggle: (taskId: string, isToday: boolean) => void;
|
|
28
|
+
onDelete: (taskId: string) => void;
|
|
27
29
|
}) {
|
|
28
30
|
const [newTitle, setNewTitle] = useState('');
|
|
29
31
|
const [adding, setAdding] = useState(false);
|
|
@@ -48,7 +50,7 @@ export default function TaskList({
|
|
|
48
50
|
<div
|
|
49
51
|
key={task.id}
|
|
50
52
|
onClick={() => onSelect(task.id)}
|
|
51
|
-
className={`flex items-center gap-2 px-3 py-2 cursor-pointer transition-colors text-sm border-l-2 ${
|
|
53
|
+
className={`group flex items-center gap-2 px-3 py-2 cursor-pointer transition-colors text-sm border-l-2 ${
|
|
52
54
|
selectedTaskId === task.id
|
|
53
55
|
? 'bg-card-hover border-l-primary'
|
|
54
56
|
: 'border-l-transparent hover:bg-card-hover/50'
|
|
@@ -77,6 +79,17 @@ export default function TaskList({
|
|
|
77
79
|
*
|
|
78
80
|
</button>
|
|
79
81
|
)}
|
|
82
|
+
<button
|
|
83
|
+
onClick={(e) => {
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
onDelete(task.id);
|
|
86
|
+
}}
|
|
87
|
+
className="flex-shrink-0 text-muted-foreground/0 group-hover:text-muted-foreground
|
|
88
|
+
hover:!text-destructive transition-colors text-xs px-0.5"
|
|
89
|
+
title="Delete task"
|
|
90
|
+
>
|
|
91
|
+
×
|
|
92
|
+
</button>
|
|
80
93
|
</div>
|
|
81
94
|
))}
|
|
82
95
|
</div>
|
|
@@ -2,14 +2,31 @@ import { getDb } from '../index';
|
|
|
2
2
|
import { generateId } from '../../utils/id';
|
|
3
3
|
import type { IProject } from '@/types';
|
|
4
4
|
|
|
5
|
+
interface ProjectRow {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
project_path: string | null;
|
|
10
|
+
ai_context: string;
|
|
11
|
+
watch_enabled: number;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rowToProject(row: ProjectRow): IProject {
|
|
17
|
+
return { ...row, watch_enabled: row.watch_enabled === 1 };
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
export function listProjects(): IProject[] {
|
|
6
21
|
const db = getDb();
|
|
7
|
-
|
|
22
|
+
const rows = db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as ProjectRow[];
|
|
23
|
+
return rows.map(rowToProject);
|
|
8
24
|
}
|
|
9
25
|
|
|
10
26
|
export function getProject(id: string): IProject | undefined {
|
|
11
27
|
const db = getDb();
|
|
12
|
-
|
|
28
|
+
const row = db.prepare('SELECT * FROM projects WHERE id = ?').get(id) as ProjectRow | undefined;
|
|
29
|
+
return row ? rowToProject(row) : undefined;
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
export function createProject(name: string, description: string = '', projectPath?: string): IProject {
|
|
@@ -30,7 +47,7 @@ export function createProject(name: string, description: string = '', projectPat
|
|
|
30
47
|
return getProject(id)!;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
export function updateProject(id: string, data: { name?: string; description?: string; project_path?: string | null; ai_context?: string }): IProject | undefined {
|
|
50
|
+
export function updateProject(id: string, data: { name?: string; description?: string; project_path?: string | null; ai_context?: string; watch_enabled?: boolean }): IProject | undefined {
|
|
34
51
|
const db = getDb();
|
|
35
52
|
const project = getProject(id);
|
|
36
53
|
if (!project) return undefined;
|
|
@@ -38,12 +55,13 @@ export function updateProject(id: string, data: { name?: string; description?: s
|
|
|
38
55
|
const now = new Date().toISOString();
|
|
39
56
|
|
|
40
57
|
db.prepare(
|
|
41
|
-
'UPDATE projects SET name = ?, description = ?, project_path = ?, ai_context = ?, updated_at = ? WHERE id = ?'
|
|
58
|
+
'UPDATE projects SET name = ?, description = ?, project_path = ?, ai_context = ?, watch_enabled = ?, updated_at = ? WHERE id = ?'
|
|
42
59
|
).run(
|
|
43
60
|
data.name ?? project.name,
|
|
44
61
|
data.description ?? project.description,
|
|
45
62
|
data.project_path !== undefined ? data.project_path : project.project_path,
|
|
46
63
|
data.ai_context !== undefined ? data.ai_context : (project.ai_context ?? ''),
|
|
64
|
+
data.watch_enabled !== undefined ? (data.watch_enabled ? 1 : 0) : (project.watch_enabled ? 1 : 0),
|
|
47
65
|
now,
|
|
48
66
|
id,
|
|
49
67
|
);
|
package/src/lib/db/schema.ts
CHANGED
|
@@ -30,6 +30,9 @@ export function initSchema(db: Database.Database): void {
|
|
|
30
30
|
if (!projCols.some(c => c.name === 'ai_context')) {
|
|
31
31
|
db.exec("ALTER TABLE projects ADD COLUMN ai_context TEXT NOT NULL DEFAULT ''");
|
|
32
32
|
}
|
|
33
|
+
if (!projCols.some(c => c.name === 'watch_enabled')) {
|
|
34
|
+
db.exec("ALTER TABLE projects ADD COLUMN watch_enabled INTEGER NOT NULL DEFAULT 0");
|
|
35
|
+
}
|
|
33
36
|
|
|
34
37
|
// v2 tables
|
|
35
38
|
db.exec(`
|
package/src/lib/watcher.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface WatcherOptions {
|
|
|
14
14
|
dryRun: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const PROGRESS_SAVE_INTERVAL = 5000; // Save streaming output to DB every 5s
|
|
18
|
+
|
|
17
19
|
function timestamp(): string {
|
|
18
20
|
return new Date().toLocaleTimeString('ko-KR', { hour12: false });
|
|
19
21
|
}
|
|
@@ -35,12 +37,10 @@ function formatDuration(ms: number): string {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function resolveCwd(task: ITask, project: IProject): string | null {
|
|
38
|
-
// 1. sub_project.folder_path
|
|
39
40
|
const subProject = getSubProject(task.sub_project_id);
|
|
40
41
|
if (subProject?.folder_path && fs.existsSync(subProject.folder_path)) {
|
|
41
42
|
return subProject.folder_path;
|
|
42
43
|
}
|
|
43
|
-
// 2. project.project_path
|
|
44
44
|
if (project.project_path && fs.existsSync(project.project_path)) {
|
|
45
45
|
return project.project_path;
|
|
46
46
|
}
|
|
@@ -60,7 +60,6 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
|
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// Re-check status (another watcher might have grabbed it)
|
|
64
63
|
const fresh = getTask(task.id);
|
|
65
64
|
if (!fresh || fresh.status !== 'submitted') {
|
|
66
65
|
logTask(`⚠ Skip "${task.title}" — status already changed`);
|
|
@@ -80,37 +79,90 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
|
|
|
80
79
|
// Transition: submitted → testing
|
|
81
80
|
updateTask(task.id, { status: 'testing' });
|
|
82
81
|
logTask(`submitted → testing`);
|
|
83
|
-
addTaskConversation(task.id, 'user', `[watch]
|
|
82
|
+
addTaskConversation(task.id, 'user', `[watch] 실행 시작`);
|
|
84
83
|
|
|
85
84
|
const startTime = Date.now();
|
|
86
85
|
|
|
87
|
-
// Build prompt with AI policy context
|
|
88
86
|
let fullPrompt = prompt.content;
|
|
89
87
|
if (project.ai_context) {
|
|
90
88
|
fullPrompt = `Project AI Policy:\n${project.ai_context}\n\n---\n\n${fullPrompt}`;
|
|
91
89
|
}
|
|
92
90
|
|
|
91
|
+
logTask('────────────────────────────────');
|
|
92
|
+
|
|
93
|
+
// Accumulate streaming output and periodically save to DB
|
|
94
|
+
let accumulated = '';
|
|
95
|
+
let progressMsgId: string | null = null;
|
|
96
|
+
let lastSaveTime = Date.now();
|
|
97
|
+
|
|
98
|
+
const saveProgress = () => {
|
|
99
|
+
if (!accumulated.trim()) return;
|
|
100
|
+
const content = `[진행 중]\n${accumulated}`;
|
|
101
|
+
if (progressMsgId) {
|
|
102
|
+
// Update existing progress message
|
|
103
|
+
const { getDb } = require('./db/index');
|
|
104
|
+
const db = getDb();
|
|
105
|
+
db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(content, progressMsgId);
|
|
106
|
+
} else {
|
|
107
|
+
const msg = addTaskConversation(task.id, 'assistant', content);
|
|
108
|
+
progressMsgId = msg.id;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
93
112
|
try {
|
|
94
|
-
const
|
|
113
|
+
const onText = (chunk: string) => {
|
|
114
|
+
process.stdout.write(chunk);
|
|
115
|
+
accumulated += chunk;
|
|
116
|
+
|
|
117
|
+
// Save to DB periodically
|
|
118
|
+
if (Date.now() - lastSaveTime > PROGRESS_SAVE_INTERVAL) {
|
|
119
|
+
saveProgress();
|
|
120
|
+
lastSaveTime = Date.now();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await runClaude(fullPrompt, onText, undefined, {
|
|
95
125
|
cwd,
|
|
96
126
|
timeoutMs: options.timeoutMs,
|
|
97
127
|
});
|
|
98
128
|
|
|
129
|
+
process.stdout.write('\n');
|
|
130
|
+
logTask('────────────────────────────────');
|
|
131
|
+
|
|
99
132
|
const duration = Date.now() - startTime;
|
|
100
133
|
updateTask(task.id, { status: 'done' });
|
|
101
|
-
|
|
134
|
+
|
|
135
|
+
// Replace progress message with final result
|
|
136
|
+
if (progressMsgId) {
|
|
137
|
+
const { getDb } = require('./db/index');
|
|
138
|
+
const db = getDb();
|
|
139
|
+
db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(result || '(no output)', progressMsgId);
|
|
140
|
+
} else {
|
|
141
|
+
addTaskConversation(task.id, 'assistant', result || '(no output)');
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
console.log(`[${timestamp()}] ✓ Done (${formatDuration(duration)})`);
|
|
103
145
|
} catch (err) {
|
|
104
146
|
const duration = Date.now() - startTime;
|
|
147
|
+
process.stdout.write('\n');
|
|
148
|
+
logTask('────────────────────────────────');
|
|
105
149
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
106
150
|
updateTask(task.id, { status: 'problem' });
|
|
107
|
-
|
|
151
|
+
|
|
152
|
+
// Replace progress message with error
|
|
153
|
+
if (progressMsgId) {
|
|
154
|
+
const { getDb } = require('./db/index');
|
|
155
|
+
const db = getDb();
|
|
156
|
+
db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(`[error] ${errorMsg}`, progressMsgId);
|
|
157
|
+
} else {
|
|
158
|
+
addTaskConversation(task.id, 'assistant', `[error] ${errorMsg}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
108
161
|
console.log(`[${timestamp()}] ✗ Failed (${formatDuration(duration)}): ${errorMsg}`);
|
|
109
162
|
}
|
|
110
163
|
}
|
|
111
164
|
|
|
112
165
|
export async function startWatcher(options: WatcherOptions): Promise<void> {
|
|
113
|
-
// Validate project if specified
|
|
114
166
|
if (options.projectId) {
|
|
115
167
|
const project = getProject(options.projectId);
|
|
116
168
|
if (!project) {
|
|
@@ -119,8 +171,7 @@ export async function startWatcher(options: WatcherOptions): Promise<void> {
|
|
|
119
171
|
}
|
|
120
172
|
log(`Watching project: "${project.name}" (${project.id})`);
|
|
121
173
|
} else {
|
|
122
|
-
|
|
123
|
-
log(`Watching all projects (${projects.length})`);
|
|
174
|
+
log(`Watching all watch-enabled projects`);
|
|
124
175
|
}
|
|
125
176
|
|
|
126
177
|
log(`Polling every ${options.intervalMs / 1000}s | Timeout: ${options.timeoutMs / 60000}m${options.dryRun ? ' | DRY RUN' : ''}`);
|
|
@@ -135,17 +186,15 @@ export async function startWatcher(options: WatcherOptions): Promise<void> {
|
|
|
135
186
|
isProcessing = true;
|
|
136
187
|
|
|
137
188
|
try {
|
|
138
|
-
//
|
|
139
|
-
const
|
|
140
|
-
? [options.projectId]
|
|
141
|
-
: listProjects().
|
|
189
|
+
// Only process watch-enabled projects
|
|
190
|
+
const projects = options.projectId
|
|
191
|
+
? [getProject(options.projectId)!].filter(p => p)
|
|
192
|
+
: listProjects().filter(p => p.watch_enabled);
|
|
142
193
|
|
|
143
194
|
const submittedTasks: { task: ITask; project: IProject }[] = [];
|
|
144
195
|
|
|
145
|
-
for (const
|
|
146
|
-
const
|
|
147
|
-
if (!project) continue;
|
|
148
|
-
const tasks = getTasksByProject(pid).filter(t => t.status === 'submitted');
|
|
196
|
+
for (const project of projects) {
|
|
197
|
+
const tasks = getTasksByProject(project.id).filter(t => t.status === 'submitted');
|
|
149
198
|
for (const task of tasks) {
|
|
150
199
|
submittedTasks.push({ task, project });
|
|
151
200
|
}
|
|
@@ -166,13 +215,11 @@ export async function startWatcher(options: WatcherOptions): Promise<void> {
|
|
|
166
215
|
}
|
|
167
216
|
};
|
|
168
217
|
|
|
169
|
-
// Graceful shutdown
|
|
170
218
|
const shutdown = () => {
|
|
171
219
|
if (shuttingDown) return;
|
|
172
220
|
shuttingDown = true;
|
|
173
221
|
console.log(`\n[IM Watch] Shutting down...${isProcessing ? ' (waiting for current task)' : ''}`);
|
|
174
222
|
if (!isProcessing) process.exit(0);
|
|
175
|
-
// If processing, the poll loop will exit after the current task
|
|
176
223
|
const checkDone = setInterval(() => {
|
|
177
224
|
if (!isProcessing) {
|
|
178
225
|
clearInterval(checkDone);
|
|
@@ -184,7 +231,6 @@ export async function startWatcher(options: WatcherOptions): Promise<void> {
|
|
|
184
231
|
process.on('SIGINT', shutdown);
|
|
185
232
|
process.on('SIGTERM', shutdown);
|
|
186
233
|
|
|
187
|
-
// Initial poll + recurring
|
|
188
234
|
await poll();
|
|
189
235
|
setInterval(poll, options.intervalMs);
|
|
190
236
|
}
|