idea-manager 0.6.0 → 0.7.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/README.md +2 -0
- package/package.json +1 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +27 -0
- package/public/sw.js +27 -0
- package/src/app/globals.css +58 -0
- package/src/app/layout.tsx +18 -1
- package/src/app/page.tsx +6 -341
- package/src/app/projects/[id]/page.tsx +9 -448
- package/src/components/dashboard/DashboardPanel.tsx +291 -0
- package/src/components/tabs/TabBar.tsx +46 -0
- package/src/components/tabs/TabContext.tsx +201 -0
- package/src/components/tabs/TabShell.tsx +35 -0
- 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/components/workspace/WorkspacePanel.tsx +440 -0
- 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
|
@@ -1,460 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useRouter
|
|
5
|
-
import Editor from '@/components/brainstorm/Editor';
|
|
6
|
-
import ProjectTree from '@/components/task/ProjectTree';
|
|
7
|
-
import TaskDetail from '@/components/task/TaskDetail';
|
|
8
|
-
import DirectoryPicker from '@/components/DirectoryPicker';
|
|
9
|
-
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
10
|
-
import AiPolicyModal from '@/components/ui/AiPolicyModal';
|
|
11
|
-
import type { ISubProject, ITask, TaskStatus, ISubProjectWithStats } from '@/types';
|
|
3
|
+
import { useEffect, use } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
id
|
|
15
|
-
name: string;
|
|
16
|
-
description: string;
|
|
17
|
-
project_path: string | null;
|
|
18
|
-
ai_context: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function WorkspaceInner({ id }: { id: string }) {
|
|
6
|
+
export default function ProjectRedirect({ params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = use(params);
|
|
22
8
|
const router = useRouter();
|
|
23
|
-
const searchParams = useSearchParams();
|
|
24
|
-
const initialUrlSub = useRef(searchParams.get('sub'));
|
|
25
|
-
const initialUrlTask = useRef(searchParams.get('task'));
|
|
26
|
-
|
|
27
|
-
const [project, setProject] = useState<IProject | null>(null);
|
|
28
|
-
const [subProjects, setSubProjects] = useState<ISubProjectWithStats[]>([]);
|
|
29
|
-
const [selectedSubId, setSelectedSubId] = useState<string | null>(null);
|
|
30
|
-
const [tasks, setTasks] = useState<ITask[]>([]);
|
|
31
|
-
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
|
32
|
-
const [showDirPicker, setShowDirPicker] = useState(false);
|
|
33
|
-
const [confirmAction, setConfirmAction] = useState<{ type: 'delete-sub' | 'delete-task'; id: string } | null>(null);
|
|
34
|
-
const [showAddSub, setShowAddSub] = useState(false);
|
|
35
|
-
const [showBrainstorm, setShowBrainstorm] = useState(true);
|
|
36
|
-
const [newSubName, setNewSubName] = useState('');
|
|
37
|
-
const [showAiPolicy, setShowAiPolicy] = useState(false);
|
|
38
|
-
|
|
39
|
-
// Resizable panel widths
|
|
40
|
-
const [leftWidth, setLeftWidth] = useState(500);
|
|
41
|
-
const [centerWidth, setCenterWidth] = useState(500);
|
|
42
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
43
|
-
const draggingRef = useRef<'left' | 'center' | null>(null);
|
|
44
|
-
const startXRef = useRef(0);
|
|
45
|
-
const startWidthRef = useRef(0);
|
|
46
|
-
|
|
47
|
-
const handleMouseDown = useCallback((panel: 'left' | 'center', e: React.MouseEvent) => {
|
|
48
|
-
e.preventDefault();
|
|
49
|
-
draggingRef.current = panel;
|
|
50
|
-
startXRef.current = e.clientX;
|
|
51
|
-
startWidthRef.current = panel === 'left' ? leftWidth : centerWidth;
|
|
52
|
-
}, [leftWidth, centerWidth]);
|
|
53
9
|
|
|
54
10
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const newWidth = Math.max(180, Math.min(500, startWidthRef.current + delta));
|
|
59
|
-
if (draggingRef.current === 'left') setLeftWidth(newWidth);
|
|
60
|
-
else setCenterWidth(newWidth);
|
|
61
|
-
};
|
|
62
|
-
const handleMouseUp = () => { draggingRef.current = null; };
|
|
63
|
-
window.addEventListener('mousemove', handleMouseMove);
|
|
64
|
-
window.addEventListener('mouseup', handleMouseUp);
|
|
65
|
-
return () => {
|
|
66
|
-
window.removeEventListener('mousemove', handleMouseMove);
|
|
67
|
-
window.removeEventListener('mouseup', handleMouseUp);
|
|
68
|
-
};
|
|
69
|
-
}, []);
|
|
70
|
-
|
|
71
|
-
// Load project
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
fetch(`/api/projects/${id}`)
|
|
74
|
-
.then(r => { if (!r.ok) { router.push('/'); return null; } return r.json(); })
|
|
75
|
-
.then(data => data && setProject(data));
|
|
11
|
+
// Store project ID for TabProvider to pick up, then redirect to root
|
|
12
|
+
sessionStorage.setItem('im-open-project', id);
|
|
13
|
+
router.replace('/');
|
|
76
14
|
}, [id, router]);
|
|
77
15
|
|
|
78
|
-
// Load sub-projects (stable callback, no deps on selection state)
|
|
79
|
-
const loadSubProjects = useCallback(async () => {
|
|
80
|
-
const res = await fetch(`/api/projects/${id}/sub-projects`);
|
|
81
|
-
if (!res.ok) return;
|
|
82
|
-
const data: ISubProjectWithStats[] = await res.json();
|
|
83
|
-
setSubProjects(data);
|
|
84
|
-
return data;
|
|
85
|
-
}, [id]);
|
|
86
|
-
|
|
87
|
-
// Initial load: sub-projects + auto-select from URL
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
loadSubProjects().then(data => {
|
|
90
|
-
if (!data || data.length === 0) return;
|
|
91
|
-
const urlSub = initialUrlSub.current;
|
|
92
|
-
if (urlSub && data.some(s => s.id === urlSub)) {
|
|
93
|
-
setSelectedSubId(urlSub);
|
|
94
|
-
} else {
|
|
95
|
-
setSelectedSubId(data[0].id);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}, [loadSubProjects]);
|
|
99
|
-
|
|
100
|
-
// Load tasks when sub-project changes
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (!selectedSubId) { setTasks([]); return; }
|
|
103
|
-
fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`)
|
|
104
|
-
.then(r => r.json())
|
|
105
|
-
.then((data: ITask[]) => {
|
|
106
|
-
setTasks(data);
|
|
107
|
-
// Auto-select from URL on first load only
|
|
108
|
-
const urlTask = initialUrlTask.current;
|
|
109
|
-
if (urlTask && data.some(t => t.id === urlTask)) {
|
|
110
|
-
setSelectedTaskId(urlTask);
|
|
111
|
-
initialUrlTask.current = null; // consume once
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}, [id, selectedSubId]);
|
|
115
|
-
|
|
116
|
-
const selectedTask = tasks.find(t => t.id === selectedTaskId) ?? null;
|
|
117
|
-
|
|
118
|
-
const handleCreateSubProject = async () => {
|
|
119
|
-
if (!newSubName.trim()) return;
|
|
120
|
-
const res = await fetch(`/api/projects/${id}/sub-projects`, {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: { 'Content-Type': 'application/json' },
|
|
123
|
-
body: JSON.stringify({ name: newSubName.trim() }),
|
|
124
|
-
});
|
|
125
|
-
if (res.ok) {
|
|
126
|
-
const sp: ISubProject = await res.json();
|
|
127
|
-
setNewSubName('');
|
|
128
|
-
setShowAddSub(false);
|
|
129
|
-
await loadSubProjects();
|
|
130
|
-
setSelectedSubId(sp.id);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const handleDeleteSubProject = (subId: string) => {
|
|
135
|
-
setConfirmAction({ type: 'delete-sub', id: subId });
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const handleCreateTask = async (title: string) => {
|
|
139
|
-
if (!selectedSubId) return;
|
|
140
|
-
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`, {
|
|
141
|
-
method: 'POST',
|
|
142
|
-
headers: { 'Content-Type': 'application/json' },
|
|
143
|
-
body: JSON.stringify({ title }),
|
|
144
|
-
});
|
|
145
|
-
if (res.ok) {
|
|
146
|
-
const task: ITask = await res.json();
|
|
147
|
-
setTasks(prev => [...prev, task]);
|
|
148
|
-
setSelectedTaskId(task.id);
|
|
149
|
-
loadSubProjects();
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const handleTaskStatusChange = async (taskId: string, status: TaskStatus) => {
|
|
154
|
-
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${taskId}`, {
|
|
155
|
-
method: 'PUT',
|
|
156
|
-
headers: { 'Content-Type': 'application/json' },
|
|
157
|
-
body: JSON.stringify({ status }),
|
|
158
|
-
});
|
|
159
|
-
if (res.ok) {
|
|
160
|
-
const updated: ITask = await res.json();
|
|
161
|
-
setTasks(prev => prev.map(t => t.id === taskId ? updated : t));
|
|
162
|
-
loadSubProjects();
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const handleTaskTodayToggle = async (taskId: string, isToday: boolean) => {
|
|
167
|
-
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${taskId}`, {
|
|
168
|
-
method: 'PUT',
|
|
169
|
-
headers: { 'Content-Type': 'application/json' },
|
|
170
|
-
body: JSON.stringify({ is_today: isToday }),
|
|
171
|
-
});
|
|
172
|
-
if (res.ok) {
|
|
173
|
-
const updated: ITask = await res.json();
|
|
174
|
-
setTasks(prev => prev.map(t => t.id === taskId ? updated : t));
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const handleTaskUpdate = async (data: Partial<ITask>) => {
|
|
179
|
-
if (!selectedTaskId || !selectedSubId) return;
|
|
180
|
-
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${selectedTaskId}`, {
|
|
181
|
-
method: 'PUT',
|
|
182
|
-
headers: { 'Content-Type': 'application/json' },
|
|
183
|
-
body: JSON.stringify(data),
|
|
184
|
-
});
|
|
185
|
-
if (res.ok) {
|
|
186
|
-
const updated: ITask = await res.json();
|
|
187
|
-
setTasks(prev => prev.map(t => t.id === selectedTaskId ? updated : t));
|
|
188
|
-
loadSubProjects();
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const handleTaskDelete = () => {
|
|
193
|
-
if (!selectedTaskId) return;
|
|
194
|
-
setConfirmAction({ type: 'delete-task', id: selectedTaskId });
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const handleConfirmAction = async () => {
|
|
198
|
-
if (!confirmAction) return;
|
|
199
|
-
if (confirmAction.type === 'delete-sub') {
|
|
200
|
-
await fetch(`/api/projects/${id}/sub-projects/${confirmAction.id}`, { method: 'DELETE' });
|
|
201
|
-
if (selectedSubId === confirmAction.id) {
|
|
202
|
-
setSelectedSubId(null);
|
|
203
|
-
setSelectedTaskId(null);
|
|
204
|
-
}
|
|
205
|
-
loadSubProjects();
|
|
206
|
-
} else if (confirmAction.type === 'delete-task') {
|
|
207
|
-
await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${confirmAction.id}`, { method: 'DELETE' });
|
|
208
|
-
setTasks(prev => prev.filter(t => t.id !== confirmAction.id));
|
|
209
|
-
if (selectedTaskId === confirmAction.id) setSelectedTaskId(null);
|
|
210
|
-
loadSubProjects();
|
|
211
|
-
}
|
|
212
|
-
setConfirmAction(null);
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const handleSetPath = async (selectedPath: string) => {
|
|
216
|
-
const res = await fetch(`/api/projects/${id}`, {
|
|
217
|
-
method: 'PUT',
|
|
218
|
-
headers: { 'Content-Type': 'application/json' },
|
|
219
|
-
body: JSON.stringify({ project_path: selectedPath }),
|
|
220
|
-
});
|
|
221
|
-
if (res.ok) {
|
|
222
|
-
setProject(await res.json());
|
|
223
|
-
setShowDirPicker(false);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const handleSaveAiPolicy = async (aiContext: string) => {
|
|
228
|
-
const res = await fetch(`/api/projects/${id}`, {
|
|
229
|
-
method: 'PUT',
|
|
230
|
-
headers: { 'Content-Type': 'application/json' },
|
|
231
|
-
body: JSON.stringify({ ai_context: aiContext }),
|
|
232
|
-
});
|
|
233
|
-
if (res.ok) {
|
|
234
|
-
setProject(await res.json());
|
|
235
|
-
setShowAiPolicy(false);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
// Keyboard shortcuts (use e.code for Korean IME compatibility)
|
|
240
|
-
useEffect(() => {
|
|
241
|
-
const handler = (e: KeyboardEvent) => {
|
|
242
|
-
const isInput = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement;
|
|
243
|
-
|
|
244
|
-
// B — toggle brainstorming panel
|
|
245
|
-
if (!isInput && e.code === 'KeyB' && !e.metaKey && !e.ctrlKey) {
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
setShowBrainstorm(prev => !prev);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// N — new sub-project (when not in input)
|
|
252
|
-
if (!isInput && e.code === 'KeyN' && !e.metaKey && !e.ctrlKey) {
|
|
253
|
-
e.preventDefault();
|
|
254
|
-
setShowAddSub(true);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// T — new task (when sub-project selected, not in input)
|
|
259
|
-
if (!isInput && e.code === 'KeyT' && !e.metaKey && !e.ctrlKey && selectedSubId) {
|
|
260
|
-
e.preventDefault();
|
|
261
|
-
const addBtn = document.querySelector('[data-add-task]') as HTMLButtonElement;
|
|
262
|
-
addBtn?.click();
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Cmd+1~6 — status change
|
|
267
|
-
if (selectedTaskId && selectedSubId && !isInput) {
|
|
268
|
-
const statusMap: Record<string, TaskStatus> = {
|
|
269
|
-
'Digit1': 'idea', 'Digit2': 'writing', 'Digit3': 'submitted',
|
|
270
|
-
'Digit4': 'testing', 'Digit5': 'done', 'Digit6': 'problem',
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
if ((e.metaKey || e.ctrlKey) && statusMap[e.code]) {
|
|
274
|
-
e.preventDefault();
|
|
275
|
-
handleTaskStatusChange(selectedTaskId, statusMap[e.code]);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
window.addEventListener('keydown', handler);
|
|
280
|
-
return () => window.removeEventListener('keydown', handler);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
if (!project) {
|
|
284
|
-
return <div className="min-h-screen flex items-center justify-center text-muted-foreground">Loading...</div>;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
16
|
return (
|
|
288
|
-
<div className="h-screen flex
|
|
289
|
-
|
|
290
|
-
<header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card flex-shrink-0">
|
|
291
|
-
<div className="flex items-center gap-3">
|
|
292
|
-
<button
|
|
293
|
-
onClick={() => router.push('/')}
|
|
294
|
-
className="text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-sm px-2 py-1 rounded-md"
|
|
295
|
-
>
|
|
296
|
-
← Back
|
|
297
|
-
</button>
|
|
298
|
-
<span className="text-border">|</span>
|
|
299
|
-
<h1 className="text-sm font-semibold">{project.name}</h1>
|
|
300
|
-
{project.project_path && (
|
|
301
|
-
<span className="text-xs text-muted-foreground font-mono truncate max-w-48" title={project.project_path}>
|
|
302
|
-
{project.project_path}
|
|
303
|
-
</span>
|
|
304
|
-
)}
|
|
305
|
-
</div>
|
|
306
|
-
<div className="flex items-center gap-2">
|
|
307
|
-
<button
|
|
308
|
-
onClick={() => setShowAiPolicy(true)}
|
|
309
|
-
className={`px-3 py-1.5 text-xs border rounded-md transition-colors ${
|
|
310
|
-
project.ai_context
|
|
311
|
-
? 'bg-accent/15 text-accent border-accent/30 hover:bg-accent/25'
|
|
312
|
-
: 'bg-muted hover:bg-card-hover text-muted-foreground border-border'
|
|
313
|
-
}`}
|
|
314
|
-
>
|
|
315
|
-
AI Policy{project.ai_context ? ' *' : ''}
|
|
316
|
-
</button>
|
|
317
|
-
{!project.project_path && (
|
|
318
|
-
<button
|
|
319
|
-
onClick={() => setShowDirPicker(true)}
|
|
320
|
-
className="px-3 py-1.5 text-xs bg-muted hover:bg-card-hover text-foreground
|
|
321
|
-
border border-border rounded-md transition-colors"
|
|
322
|
-
>
|
|
323
|
-
Link folder
|
|
324
|
-
</button>
|
|
325
|
-
)}
|
|
326
|
-
</div>
|
|
327
|
-
</header>
|
|
328
|
-
|
|
329
|
-
{/* 3-Panel Layout with resize handles */}
|
|
330
|
-
<div ref={containerRef} className="flex-1 flex overflow-hidden">
|
|
331
|
-
{/* Left: Brainstorming (collapsible) */}
|
|
332
|
-
{showBrainstorm ? (
|
|
333
|
-
<>
|
|
334
|
-
<div style={{ width: leftWidth }} className="border-r border-border flex flex-col flex-shrink-0">
|
|
335
|
-
<Editor
|
|
336
|
-
projectId={id}
|
|
337
|
-
onCollapse={() => setShowBrainstorm(false)}
|
|
338
|
-
/>
|
|
339
|
-
</div>
|
|
340
|
-
{/* Resize handle: left */}
|
|
341
|
-
<div
|
|
342
|
-
className="panel-resize-handle"
|
|
343
|
-
onMouseDown={(e) => handleMouseDown('left', e)}
|
|
344
|
-
>
|
|
345
|
-
<div className="panel-resize-handle-bar" />
|
|
346
|
-
</div>
|
|
347
|
-
</>
|
|
348
|
-
) : (
|
|
349
|
-
<button
|
|
350
|
-
onClick={() => setShowBrainstorm(true)}
|
|
351
|
-
className="w-8 border-r border-border flex-shrink-0 flex items-center justify-center
|
|
352
|
-
text-muted-foreground hover:text-foreground hover:bg-card-hover transition-colors
|
|
353
|
-
text-xs"
|
|
354
|
-
title="Show brainstorming (B)"
|
|
355
|
-
style={{ writingMode: 'vertical-rl' }}
|
|
356
|
-
>
|
|
357
|
-
Brainstorm
|
|
358
|
-
</button>
|
|
359
|
-
)}
|
|
360
|
-
|
|
361
|
-
{/* Center: Tree (Sub-projects + Tasks) */}
|
|
362
|
-
<div style={{ width: centerWidth }} className="border-r border-border flex flex-col flex-shrink-0">
|
|
363
|
-
{/* Add sub-project input */}
|
|
364
|
-
{showAddSub && (
|
|
365
|
-
<div className="px-3 py-2 border-b border-border">
|
|
366
|
-
<input
|
|
367
|
-
type="text"
|
|
368
|
-
value={newSubName}
|
|
369
|
-
onChange={(e) => setNewSubName(e.target.value)}
|
|
370
|
-
onKeyDown={(e) => {
|
|
371
|
-
if (e.key === 'Enter') handleCreateSubProject();
|
|
372
|
-
if (e.key === 'Escape') { setNewSubName(''); setShowAddSub(false); }
|
|
373
|
-
}}
|
|
374
|
-
placeholder="Sub-project name..."
|
|
375
|
-
className="w-full bg-input border border-border rounded px-2 py-1 text-xs
|
|
376
|
-
focus:border-primary focus:outline-none text-foreground"
|
|
377
|
-
autoFocus
|
|
378
|
-
/>
|
|
379
|
-
</div>
|
|
380
|
-
)}
|
|
381
|
-
|
|
382
|
-
<ProjectTree
|
|
383
|
-
subProjects={subProjects}
|
|
384
|
-
tasks={tasks}
|
|
385
|
-
selectedSubId={selectedSubId}
|
|
386
|
-
selectedTaskId={selectedTaskId}
|
|
387
|
-
onSelectSub={(subId) => { setSelectedSubId(subId); setSelectedTaskId(null); }}
|
|
388
|
-
onSelectTask={setSelectedTaskId}
|
|
389
|
-
onCreateSub={() => setShowAddSub(true)}
|
|
390
|
-
onDeleteSub={handleDeleteSubProject}
|
|
391
|
-
onCreateTask={handleCreateTask}
|
|
392
|
-
onStatusChange={handleTaskStatusChange}
|
|
393
|
-
onTodayToggle={handleTaskTodayToggle}
|
|
394
|
-
/>
|
|
395
|
-
</div>
|
|
396
|
-
|
|
397
|
-
{/* Resize handle: center */}
|
|
398
|
-
<div
|
|
399
|
-
className="panel-resize-handle"
|
|
400
|
-
onMouseDown={(e) => handleMouseDown('center', e)}
|
|
401
|
-
>
|
|
402
|
-
<div className="panel-resize-handle-bar" />
|
|
403
|
-
</div>
|
|
404
|
-
|
|
405
|
-
{/* Right: Task Detail */}
|
|
406
|
-
<div className="flex-1 min-w-0">
|
|
407
|
-
{selectedTask ? (
|
|
408
|
-
<TaskDetail
|
|
409
|
-
task={selectedTask}
|
|
410
|
-
projectId={id}
|
|
411
|
-
subProjectId={selectedSubId!}
|
|
412
|
-
onUpdate={handleTaskUpdate}
|
|
413
|
-
onDelete={handleTaskDelete}
|
|
414
|
-
/>
|
|
415
|
-
) : (
|
|
416
|
-
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
417
|
-
{tasks.length > 0 ? 'Select a task' : selectedSubId ? 'Create a task to get started' : 'Select a sub-project'}
|
|
418
|
-
</div>
|
|
419
|
-
)}
|
|
420
|
-
</div>
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
{showDirPicker && (
|
|
424
|
-
<DirectoryPicker
|
|
425
|
-
onSelect={handleSetPath}
|
|
426
|
-
onCancel={() => setShowDirPicker(false)}
|
|
427
|
-
initialPath={project.project_path || undefined}
|
|
428
|
-
/>
|
|
429
|
-
)}
|
|
430
|
-
|
|
431
|
-
<ConfirmDialog
|
|
432
|
-
open={!!confirmAction}
|
|
433
|
-
title={confirmAction?.type === 'delete-sub' ? 'Delete sub-project?' : 'Delete task?'}
|
|
434
|
-
description={confirmAction?.type === 'delete-sub'
|
|
435
|
-
? 'This will delete the sub-project and all its tasks.'
|
|
436
|
-
: 'This task will be permanently deleted.'}
|
|
437
|
-
confirmLabel="Delete"
|
|
438
|
-
variant="danger"
|
|
439
|
-
onConfirm={handleConfirmAction}
|
|
440
|
-
onCancel={() => setConfirmAction(null)}
|
|
441
|
-
/>
|
|
442
|
-
|
|
443
|
-
<AiPolicyModal
|
|
444
|
-
open={showAiPolicy}
|
|
445
|
-
content={project.ai_context || ''}
|
|
446
|
-
onSave={handleSaveAiPolicy}
|
|
447
|
-
onClose={() => setShowAiPolicy(false)}
|
|
448
|
-
/>
|
|
17
|
+
<div className="min-h-screen flex items-center justify-center text-muted-foreground">
|
|
18
|
+
Loading...
|
|
449
19
|
</div>
|
|
450
20
|
);
|
|
451
21
|
}
|
|
452
|
-
|
|
453
|
-
export default function ProjectWorkspace({ params }: { params: Promise<{ id: string }> }) {
|
|
454
|
-
const { id } = use(params);
|
|
455
|
-
return (
|
|
456
|
-
<Suspense fallback={<div className="min-h-screen flex items-center justify-center text-muted-foreground">Loading...</div>}>
|
|
457
|
-
<WorkspaceInner id={id} />
|
|
458
|
-
</Suspense>
|
|
459
|
-
);
|
|
460
|
-
}
|