idea-manager 1.9.0 → 2.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 (185) hide show
  1. package/.next/build-manifest.json +2 -2
  2. package/.next/routes-manifest.json +35 -0
  3. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  4. package/.next/server/app/_global-error.html +2 -2
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_not-found.html +2 -2
  14. package/.next/server/app/_not-found.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/api/advisor-actions/route.js +15 -0
  22. package/.next/server/app/api/advisor-actions/route_client-reference-manifest.js +1 -0
  23. package/.next/server/app/api/archive/route.js +1 -122
  24. package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/global-advisor/route.js +37 -0
  28. package/.next/server/app/api/global-advisor/route_client-reference-manifest.js +1 -0
  29. package/.next/server/app/api/global-memo/route.js +8 -0
  30. package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/maintenance/route.js +130 -0
  33. package/.next/server/app/api/maintenance/route_client-reference-manifest.js +1 -0
  34. package/.next/server/app/api/projects/[id]/advisor/route.js +22 -11
  35. package/.next/server/app/api/projects/[id]/advisor/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/projects/[id]/apply-distribute/route.js +2 -8
  37. package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/projects/[id]/auto-distribute/route.js +126 -3
  39. package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/projects/[id]/brainstorm/route.js +124 -1
  41. package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/projects/[id]/git-sync/route.js +124 -1
  43. package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/projects/[id]/route.js +124 -1
  45. package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +8 -0
  47. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.js +1 -7
  49. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.js +8 -0
  51. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
  52. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.js +2 -8
  53. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.js +1 -122
  55. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.js +124 -0
  57. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route_client-reference-manifest.js +1 -0
  58. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route.js +1 -122
  59. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/projects/[id]/sub-projects/route.js +8 -0
  61. package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/projects/route.js +124 -1
  63. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  64. package/.next/server/app/api/search/route.js +8 -0
  65. package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  66. package/.next/server/app/api/sync/route.js +8 -0
  67. package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/tasks/[taskId]/move/route.js +15 -0
  69. package/.next/server/app/api/tasks/[taskId]/move/route_client-reference-manifest.js +1 -0
  70. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/index.html +2 -2
  73. package/.next/server/app/index.rsc +3 -3
  74. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  75. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  76. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  77. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  78. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/page.js +12 -12
  80. package/.next/server/app/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app-paths-manifest.json +18 -13
  83. package/.next/server/chunks/{117.js → 697.js} +16 -2
  84. package/.next/server/pages/404.html +2 -2
  85. package/.next/server/pages/500.html +2 -2
  86. package/.next/static/KREG104cVn2mBTMPTDTvH/_buildManifest.js +1 -0
  87. package/.next/static/chunks/374-23189d7e246ad164.js +1 -0
  88. package/.next/static/chunks/app/_global-error/page-f051f234bea7bddd.js +1 -0
  89. package/.next/static/chunks/app/api/advisor-actions/route-f051f234bea7bddd.js +1 -0
  90. package/.next/static/chunks/app/api/archive/route-f051f234bea7bddd.js +1 -0
  91. package/.next/static/chunks/app/api/filesystem/route-f051f234bea7bddd.js +1 -0
  92. package/.next/static/chunks/app/api/filesystem/tree/route-f051f234bea7bddd.js +1 -0
  93. package/.next/static/chunks/app/api/global-advisor/route-f051f234bea7bddd.js +1 -0
  94. package/.next/static/chunks/app/api/global-memo/route-f051f234bea7bddd.js +1 -0
  95. package/.next/static/chunks/app/api/health/route-f051f234bea7bddd.js +1 -0
  96. package/.next/static/chunks/app/api/maintenance/route-f051f234bea7bddd.js +1 -0
  97. package/.next/static/chunks/app/api/projects/[id]/advisor/route-f051f234bea7bddd.js +1 -0
  98. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-f051f234bea7bddd.js +1 -0
  99. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-f051f234bea7bddd.js +1 -0
  100. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-f051f234bea7bddd.js +1 -0
  101. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-f051f234bea7bddd.js +1 -0
  102. package/.next/static/chunks/app/api/projects/[id]/route-f051f234bea7bddd.js +1 -0
  103. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-f051f234bea7bddd.js +1 -0
  104. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-f051f234bea7bddd.js +1 -0
  105. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-f051f234bea7bddd.js +1 -0
  106. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-f051f234bea7bddd.js +1 -0
  107. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-f051f234bea7bddd.js +1 -0
  108. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route-f051f234bea7bddd.js +1 -0
  109. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-f051f234bea7bddd.js +1 -0
  110. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-f051f234bea7bddd.js +1 -0
  111. package/.next/static/chunks/app/api/projects/route-f051f234bea7bddd.js +1 -0
  112. package/.next/static/chunks/app/api/search/route-f051f234bea7bddd.js +1 -0
  113. package/.next/static/chunks/app/api/sync/route-f051f234bea7bddd.js +1 -0
  114. package/.next/static/chunks/app/api/tasks/[taskId]/move/route-f051f234bea7bddd.js +1 -0
  115. package/.next/static/chunks/app/api/update/route-f051f234bea7bddd.js +1 -0
  116. package/.next/static/chunks/app/api/version/route-f051f234bea7bddd.js +1 -0
  117. package/.next/static/chunks/app/page-9117037f2947f4f6.js +28 -0
  118. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f051f234bea7bddd.js +1 -0
  119. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f051f234bea7bddd.js +1 -0
  120. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f051f234bea7bddd.js +1 -0
  121. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f051f234bea7bddd.js +1 -0
  122. package/.next/static/css/e9071b58a99b47e4.css +3 -0
  123. package/package.json +1 -1
  124. package/src/app/api/advisor-actions/route.ts +52 -0
  125. package/src/app/api/global-advisor/route.ts +50 -0
  126. package/src/app/api/maintenance/route.ts +36 -0
  127. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.ts +24 -0
  128. package/src/app/api/tasks/[taskId]/move/route.ts +30 -0
  129. package/src/components/advisor/ActionBlock.tsx +124 -0
  130. package/src/components/advisor/AdvisorChat.tsx +175 -0
  131. package/src/components/advisor/GlobalAdvisorLayer.tsx +38 -0
  132. package/src/components/dashboard/DashboardPanel.tsx +2 -0
  133. package/src/components/memo/GlobalMemoLayer.tsx +81 -0
  134. package/src/components/tabs/TabBar.tsx +2 -0
  135. package/src/components/tabs/TabShell.tsx +6 -0
  136. package/src/components/task/NoteEditor.tsx +137 -0
  137. package/src/components/task/ProjectTree.tsx +105 -57
  138. package/src/components/task/TaskChat.tsx +4 -0
  139. package/src/components/task/TaskDetail.tsx +182 -1
  140. package/src/components/ui/AiActivityIndicator.tsx +66 -0
  141. package/src/components/ui/ShortcutOverlay.tsx +108 -0
  142. package/src/components/workspace/ProjectAdvisor.tsx +17 -181
  143. package/src/components/workspace/WorkspacePanel.tsx +75 -3
  144. package/src/hooks/useAiActivity.ts +6 -0
  145. package/src/lib/advisor-actions/parse.ts +59 -0
  146. package/src/lib/ai/global-context.ts +114 -0
  147. package/src/lib/ai/project-context.ts +22 -2
  148. package/src/lib/ai-activity.ts +33 -0
  149. package/src/lib/db/queries/global-conversations.ts +31 -0
  150. package/src/lib/db/queries/tasks.ts +3 -1
  151. package/src/lib/db/schema.ts +8 -0
  152. package/src/types/advisor-actions.ts +25 -0
  153. package/.next/static/chunks/374-769431701aab500f.js +0 -1
  154. package/.next/static/chunks/app/_global-error/page-3ff8f59aaa75b8f8.js +0 -1
  155. package/.next/static/chunks/app/api/archive/route-3ff8f59aaa75b8f8.js +0 -1
  156. package/.next/static/chunks/app/api/filesystem/route-3ff8f59aaa75b8f8.js +0 -1
  157. package/.next/static/chunks/app/api/filesystem/tree/route-3ff8f59aaa75b8f8.js +0 -1
  158. package/.next/static/chunks/app/api/global-memo/route-3ff8f59aaa75b8f8.js +0 -1
  159. package/.next/static/chunks/app/api/health/route-3ff8f59aaa75b8f8.js +0 -1
  160. package/.next/static/chunks/app/api/projects/[id]/advisor/route-3ff8f59aaa75b8f8.js +0 -1
  161. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-3ff8f59aaa75b8f8.js +0 -1
  162. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-3ff8f59aaa75b8f8.js +0 -1
  163. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-3ff8f59aaa75b8f8.js +0 -1
  164. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-3ff8f59aaa75b8f8.js +0 -1
  165. package/.next/static/chunks/app/api/projects/[id]/route-3ff8f59aaa75b8f8.js +0 -1
  166. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-3ff8f59aaa75b8f8.js +0 -1
  167. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-3ff8f59aaa75b8f8.js +0 -1
  168. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-3ff8f59aaa75b8f8.js +0 -1
  169. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-3ff8f59aaa75b8f8.js +0 -1
  170. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-3ff8f59aaa75b8f8.js +0 -1
  171. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-3ff8f59aaa75b8f8.js +0 -1
  172. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-3ff8f59aaa75b8f8.js +0 -1
  173. package/.next/static/chunks/app/api/projects/route-3ff8f59aaa75b8f8.js +0 -1
  174. package/.next/static/chunks/app/api/search/route-3ff8f59aaa75b8f8.js +0 -1
  175. package/.next/static/chunks/app/api/sync/route-3ff8f59aaa75b8f8.js +0 -1
  176. package/.next/static/chunks/app/api/update/route-3ff8f59aaa75b8f8.js +0 -1
  177. package/.next/static/chunks/app/api/version/route-3ff8f59aaa75b8f8.js +0 -1
  178. package/.next/static/chunks/app/page-e935ee928da68ca2.js +0 -28
  179. package/.next/static/chunks/next/dist/client/components/builtin/app-error-3ff8f59aaa75b8f8.js +0 -1
  180. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-3ff8f59aaa75b8f8.js +0 -1
  181. package/.next/static/chunks/next/dist/client/components/builtin/not-found-3ff8f59aaa75b8f8.js +0 -1
  182. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-3ff8f59aaa75b8f8.js +0 -1
  183. package/.next/static/css/e4c7cd5a570312d9.css +0 -3
  184. package/.next/static/pxqzEiwniZAUDOUTb5SnX/_buildManifest.js +0 -1
  185. /package/.next/static/{pxqzEiwniZAUDOUTb5SnX → KREG104cVn2mBTMPTDTvH}/_ssgManifest.js +0 -0
@@ -70,6 +70,7 @@ export default function WorkspacePanel({
70
70
  const [showFileTree, setShowFileTree] = useState(false);
71
71
  const [showAutoDistribute, setShowAutoDistribute] = useState(false);
72
72
  const [chatStates, setChatStates] = useState<Record<string, 'idle' | 'loading' | 'done'>>({});
73
+ const [deleteToast, setDeleteToast] = useState<{ taskId: string; title: string; timer: ReturnType<typeof setTimeout> } | null>(null);
73
74
  const syncingRef = useRef(false);
74
75
 
75
76
  // Resizable panel widths
@@ -156,6 +157,16 @@ export default function WorkspacePanel({
156
157
 
157
158
  const selectedTask = tasks.find(t => t.id === selectedTaskId) ?? null;
158
159
 
160
+ // Refresh when advisor actions are applied (from any advisor — project or global)
161
+ useEffect(() => {
162
+ const refresh = () => { loadSubProjects(); if (selectedSubId) {
163
+ fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`)
164
+ .then(r => r.json()).then(setTasks).catch(() => {});
165
+ }};
166
+ window.addEventListener('advisor-action-applied', refresh);
167
+ return () => window.removeEventListener('advisor-action-applied', refresh);
168
+ }, [id, selectedSubId, loadSubProjects]);
169
+
159
170
  const handleCreateSubProject = async () => {
160
171
  if (!newSubName.trim()) return;
161
172
  const res = await fetch(`/api/projects/${id}/sub-projects`, {
@@ -244,10 +255,34 @@ export default function WorkspacePanel({
244
255
  });
245
256
  };
246
257
 
247
- const handleTaskDelete = (taskId?: string) => {
258
+ const handleTaskDelete = async (taskId?: string) => {
248
259
  const tid = taskId || selectedTaskId;
249
- if (!tid) return;
250
- setConfirmAction({ type: 'delete-task', id: tid });
260
+ if (!tid || !selectedSubId) return;
261
+ const task = tasks.find(t => t.id === tid);
262
+ if (!task) return;
263
+
264
+ // Immediate archive (soft delete) + undo toast
265
+ await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${tid}?mode=archive`, { method: 'DELETE' });
266
+ setTasks(prev => prev.filter(t => t.id !== tid));
267
+ if (selectedTaskId === tid) setSelectedTaskId(null);
268
+ loadSubProjects();
269
+
270
+ if (deleteToast?.timer) clearTimeout(deleteToast.timer);
271
+ const timer = setTimeout(() => setDeleteToast(null), 30000);
272
+ setDeleteToast({ taskId: tid, title: task.title, timer });
273
+ };
274
+
275
+ const undoTaskDelete = async () => {
276
+ if (!deleteToast) return;
277
+ clearTimeout(deleteToast.timer);
278
+ await fetch(`/api/archive?action=restore&taskId=${deleteToast.taskId}`, { method: 'POST' });
279
+ // Re-fetch tasks to get the restored one
280
+ if (selectedSubId) {
281
+ const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`);
282
+ if (res.ok) setTasks(await res.json());
283
+ }
284
+ loadSubProjects();
285
+ setDeleteToast(null);
251
286
  };
252
287
 
253
288
  const handleConfirmAction = async (mode?: 'archive' | 'permanent') => {
@@ -540,6 +575,18 @@ export default function WorkspacePanel({
540
575
  onCreateTask={handleCreateTask} onStatusChange={handleTaskStatusChange}
541
576
  onTodayToggle={handleTaskTodayToggle} onDeleteTask={handleTaskDelete}
542
577
  onReorderSubs={handleReorderSubs}
578
+ onReorderTasks={async (orderedIds) => {
579
+ if (!selectedSubId) return;
580
+ setTasks(prev => {
581
+ const map = new Map(prev.map(t => [t.id, t]));
582
+ return orderedIds.map(id => map.get(id)!).filter(Boolean);
583
+ });
584
+ await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/reorder`, {
585
+ method: 'POST',
586
+ headers: { 'Content-Type': 'application/json' },
587
+ body: JSON.stringify({ orderedIds }),
588
+ });
589
+ }}
543
590
  onAutoDistribute={() => setShowAutoDistribute(true)}
544
591
  chatStates={chatStates} />
545
592
  </div>
@@ -554,6 +601,11 @@ export default function WorkspacePanel({
554
601
  siblingTasks={tasks}
555
602
  onUpdate={handleTaskUpdate} onDelete={handleTaskDelete}
556
603
  onTaskPromoted={(newTask) => setTasks(prev => [...prev, newTask])}
604
+ onTaskMoved={() => {
605
+ // Task moved away — remove from current list and deselect
606
+ setTasks(prev => prev.filter(t => t.id !== selectedTaskId));
607
+ setSelectedTaskId(null);
608
+ }}
557
609
  onChatStateChange={(taskId, state) => {
558
610
  setChatStates(prev => ({ ...prev, [taskId]: state }));
559
611
  }} />
@@ -623,9 +675,29 @@ export default function WorkspacePanel({
623
675
  {showAdvisor && (
624
676
  <ProjectAdvisor
625
677
  projectId={id}
678
+ projectName={project.name}
626
679
  onClose={() => setShowAdvisor(false)}
627
680
  />
628
681
  )}
682
+ {deleteToast && (
683
+ <div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 bg-card border border-border rounded-lg shadow-xl px-4 py-2.5 flex items-center gap-3 animate-dialog-in">
684
+ <span className="text-xs text-muted-foreground truncate max-w-[200px]">
685
+ &quot;{deleteToast.title}&quot; 삭제됨
686
+ </span>
687
+ <button
688
+ onClick={undoTaskDelete}
689
+ className="text-xs px-2 py-1 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
690
+ >
691
+ 되돌리기
692
+ </button>
693
+ <button
694
+ onClick={() => { clearTimeout(deleteToast.timer); setDeleteToast(null); }}
695
+ className="text-xs text-muted-foreground hover:text-foreground"
696
+ >
697
+ ×
698
+ </button>
699
+ </div>
700
+ )}
629
701
  </div>
630
702
  );
631
703
  }
@@ -0,0 +1,6 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { getAiActivities, subscribeAiActivity, type AiActivity } from '@/lib/ai-activity';
3
+
4
+ export function useAiActivity(): AiActivity[] {
5
+ return useSyncExternalStore(subscribeAiActivity, getAiActivities, getAiActivities);
6
+ }
@@ -0,0 +1,59 @@
1
+ import type { AdvisorAction } from '@/types/advisor-actions';
2
+
3
+ const ACTION_BLOCK_RE = /```action\s*\n([\s\S]*?)```/g;
4
+
5
+ export interface ParsedContent {
6
+ segments: ({ type: 'markdown'; text: string } | { type: 'actions'; actions: AdvisorAction[] })[];
7
+ }
8
+
9
+ function validateAction(obj: unknown): AdvisorAction | null {
10
+ if (!obj || typeof obj !== 'object') return null;
11
+ const a = obj as Record<string, unknown>;
12
+ if (a.type === 'create_task') {
13
+ if (typeof a.subProjectId !== 'string' || typeof a.title !== 'string') return null;
14
+ return {
15
+ type: 'create_task',
16
+ subProjectId: a.subProjectId,
17
+ projectId: typeof a.projectId === 'string' ? a.projectId : undefined,
18
+ title: a.title,
19
+ description: typeof a.description === 'string' ? a.description : undefined,
20
+ priority: ['high', 'medium', 'low'].includes(a.priority as string) ? a.priority as 'high' | 'medium' | 'low' : undefined,
21
+ status: typeof a.status === 'string' ? (a.status as string) : undefined,
22
+ } as AdvisorAction;
23
+ }
24
+ if (a.type === 'update_task') {
25
+ if (typeof a.taskId !== 'string' || !a.changes || typeof a.changes !== 'object') return null;
26
+ return { type: 'update_task', taskId: a.taskId, changes: a.changes as Record<string, unknown> } as AdvisorAction;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ export function parseAdvisorContent(content: string): ParsedContent {
32
+ const segments: ParsedContent['segments'] = [];
33
+ let lastIndex = 0;
34
+
35
+ for (const match of content.matchAll(ACTION_BLOCK_RE)) {
36
+ const before = content.slice(lastIndex, match.index);
37
+ if (before.trim()) segments.push({ type: 'markdown', text: before });
38
+
39
+ try {
40
+ const raw = JSON.parse(match[1]);
41
+ const arr = Array.isArray(raw) ? raw : [raw];
42
+ const valid = arr.map(validateAction).filter((a): a is AdvisorAction => a !== null);
43
+ if (valid.length > 0) {
44
+ segments.push({ type: 'actions', actions: valid });
45
+ } else {
46
+ segments.push({ type: 'markdown', text: match[0] });
47
+ }
48
+ } catch {
49
+ segments.push({ type: 'markdown', text: match[0] });
50
+ }
51
+
52
+ lastIndex = (match.index ?? 0) + match[0].length;
53
+ }
54
+
55
+ const remaining = content.slice(lastIndex);
56
+ if (remaining.trim()) segments.push({ type: 'markdown', text: remaining });
57
+
58
+ return { segments };
59
+ }
@@ -0,0 +1,114 @@
1
+ import { listProjects } from '../db/queries/projects';
2
+ import { getSubProjects } from '../db/queries/sub-projects';
3
+ import { getTasksByProject } from '../db/queries/tasks';
4
+ import { getBrainstorm } from '../db/queries/brainstorms';
5
+ import type { ITask, TaskStatus } from '../../types';
6
+
7
+ const ACTION_INSTRUCTIONS = `
8
+ ## Actions
9
+ 사용자가 요청하면 태스크 생성/수정을 제안할 수 있습니다. \`\`\`action 블록을 사용하세요.
10
+ 사용자가 명시적으로 요청하지 않으면 action 블록을 넣지 마세요.
11
+
12
+ 형식:
13
+ \`\`\`action
14
+ [
15
+ {"type":"create_task","subProjectId":"<sub_id값>","projectId":"<project_id값>","title":"제목","priority":"high|medium|low"},
16
+ {"type":"update_task","taskId":"<task_id값>","changes":{"status":"done"}}
17
+ ]
18
+ \`\`\`
19
+
20
+ 규칙:
21
+ - subProjectId, taskId, projectId는 아래 컨텍스트의 [sub_id:...], [task_id:...], [project_id:...] 값을 정확히 사용
22
+ - action 블록 앞에 항상 무엇을 제안하는지 설명
23
+ - 사용자가 승인해야 실행됨
24
+ `;
25
+
26
+ const MAX_BRAINSTORM = 1500;
27
+ const NOTE_LIMIT = 120;
28
+ const MAX_HISTORY_MESSAGES = 20;
29
+
30
+ function truncate(s: string | null | undefined, max: number): string {
31
+ if (!s) return '';
32
+ if (s.length <= max) return s;
33
+ return s.slice(0, max) + '…';
34
+ }
35
+
36
+ const STATUS_ICON: Record<string, string> = {
37
+ idea: 'idea', doing: 'DOING', writing: 'writing', submitted: 'submitted',
38
+ testing: 'testing', done: 'done', problem: 'PROBLEM',
39
+ };
40
+
41
+ export function buildGlobalAdvisorPrompt(): string {
42
+ const projects = listProjects();
43
+
44
+ const parts: string[] = [];
45
+ parts.push('당신은 사용자의 전체 워크스페이스를 조망하는 AI 어드바이저입니다.');
46
+ parts.push('여러 프로젝트의 현황을 파악하고, 우선순위·방향·빠진 부분·크로스-프로젝트 이슈 등을 논의합니다.');
47
+ parts.push('한국어로 간결하게 답하세요. 긴 설교 금지.');
48
+ parts.push(ACTION_INSTRUCTIONS);
49
+
50
+ parts.push('=== ALL WORKSPACES ===\n');
51
+
52
+ let totalTasks = 0;
53
+ let totalDone = 0;
54
+ let totalProblem = 0;
55
+
56
+ for (const project of projects) {
57
+ const subs = getSubProjects(project.id);
58
+ const allTasks = getTasksByProject(project.id);
59
+ const brainstorm = getBrainstorm(project.id);
60
+
61
+ totalTasks += allTasks.length;
62
+ totalDone += allTasks.filter(t => t.status === 'done').length;
63
+ totalProblem += allTasks.filter(t => t.status === 'problem').length;
64
+
65
+ const counts: Record<string, number> = {};
66
+ const todayTasks: string[] = [];
67
+ const problemTasks: string[] = [];
68
+ for (const t of allTasks) {
69
+ counts[t.status] = (counts[t.status] ?? 0) + 1;
70
+ if (t.is_today) todayTasks.push(t.title);
71
+ if (t.status === 'problem') problemTasks.push(t.title);
72
+ }
73
+
74
+ const lines: string[] = [];
75
+ lines.push(`## ${project.name} [project_id:${project.id}]`);
76
+ if (project.description) lines.push(project.description);
77
+ const statsStr = Object.entries(counts).map(([k, v]) => `${k}:${v}`).join(' / ');
78
+ lines.push(`태스크 ${allTasks.length}개 (${statsStr})`);
79
+ if (todayTasks.length) lines.push(`Today: ${todayTasks.join(', ')}`);
80
+ if (problemTasks.length) lines.push(`문제: ${problemTasks.join(', ')}`);
81
+
82
+ if (brainstorm?.content) {
83
+ lines.push(`\n브레인스토밍 요약: ${truncate(brainstorm.content, MAX_BRAINSTORM)}`);
84
+ }
85
+
86
+ // Show active (non-done, non-archived) tasks by sub-project
87
+ for (const sub of subs) {
88
+ const subTasks = allTasks.filter(t => t.sub_project_id === sub.id && t.status !== 'done');
89
+ if (!subTasks.length) continue;
90
+ lines.push(`\n### ${sub.name} [sub_id:${sub.id}]`);
91
+ for (const t of subTasks) {
92
+ const note = truncate(t.description, NOTE_LIMIT);
93
+ const flags = [t.priority === 'high' ? 'HIGH' : null, t.is_today ? 'today' : null].filter(Boolean).join(', ');
94
+ const flagStr = flags ? ` (${flags})` : '';
95
+ lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}** [task_id:${t.id}]${flagStr}${note ? ' — ' + note : ''}`);
96
+ }
97
+ }
98
+
99
+ parts.push(lines.join('\n'));
100
+ parts.push('');
101
+ }
102
+
103
+ parts.push('---');
104
+ parts.push(`전체: ${projects.length}개 워크스페이스, ${totalTasks}개 태스크 (완료 ${totalDone}, 문제 ${totalProblem})`);
105
+
106
+ return parts.join('\n');
107
+ }
108
+
109
+ export function trimHistory(
110
+ messages: { role: string; content: string }[],
111
+ ): { role: string; content: string }[] {
112
+ if (messages.length <= MAX_HISTORY_MESSAGES) return messages;
113
+ return messages.slice(-MAX_HISTORY_MESSAGES);
114
+ }
@@ -4,6 +4,25 @@ import { getTasksByProject } from '../db/queries/tasks';
4
4
  import { getBrainstorm } from '../db/queries/brainstorms';
5
5
  import type { ITask, TaskStatus } from '../../types';
6
6
 
7
+ const ACTION_INSTRUCTIONS = `
8
+ ## Actions
9
+ 사용자가 요청하면 태스크 생성/수정을 제안할 수 있습니다. \`\`\`action 블록을 사용하세요.
10
+ 사용자가 명시적으로 요청하지 않으면 action 블록을 넣지 마세요.
11
+
12
+ 형식:
13
+ \`\`\`action
14
+ [
15
+ {"type":"create_task","subProjectId":"<sub_id값>","title":"제목","priority":"high|medium|low","status":"idea"},
16
+ {"type":"update_task","taskId":"<task_id값>","changes":{"status":"done"}}
17
+ ]
18
+ \`\`\`
19
+
20
+ 규칙:
21
+ - subProjectId, taskId는 아래 컨텍스트의 [sub_id:...], [task_id:...] 값을 정확히 사용
22
+ - action 블록 앞에 항상 무엇을 제안하는지 설명
23
+ - 사용자가 승인해야 실행됨 (자동 실행 아님)
24
+ `;
25
+
7
26
  const MAX_BRAINSTORM = 4000;
8
27
  const NOTE_LIMIT_ACTIVE = 500;
9
28
  const NOTE_LIMIT_DEFAULT = 200;
@@ -49,7 +68,7 @@ export function buildProjectAdvisorPrompt(projectId: string): string {
49
68
  continue;
50
69
  }
51
70
  const lines: string[] = [];
52
- lines.push(`### ${sub.name}`);
71
+ lines.push(`### ${sub.name} [sub_id:${sub.id}]`);
53
72
  if (sub.description) lines.push(sub.description);
54
73
  lines.push(`태스크 ${tasks.length}개:`);
55
74
  for (const t of tasks) {
@@ -58,7 +77,7 @@ export function buildProjectAdvisorPrompt(projectId: string): string {
58
77
  const flags = [t.priority === 'high' ? 'HIGH' : null, t.is_today ? 'today' : null].filter(Boolean).join(', ');
59
78
  const flagStr = flags ? ` (${flags})` : '';
60
79
  const noteStr = note ? ` — ${note}` : '';
61
- lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}**${flagStr}${noteStr}`);
80
+ lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}** [task_id:${t.id}]${flagStr}${noteStr}`);
62
81
  }
63
82
  subSections.push(lines.join('\n'));
64
83
  }
@@ -86,6 +105,7 @@ export function buildProjectAdvisorPrompt(projectId: string): string {
86
105
  parts.push(`당신은 프로젝트 "${project.name}"의 어드바이저입니다.`);
87
106
  parts.push(`사용자가 프로젝트 방향, 우선순위, 빠진 부분, 다음 단계 등을 논의하면 프로젝트 전체 맥락을 바탕으로 간결하게 답합니다.`);
88
107
  parts.push(`태스크를 언급할 때는 정확한 제목을 쓰세요. 한국어로 답하세요. 긴 설교는 금지.`);
108
+ parts.push(ACTION_INSTRUCTIONS);
89
109
 
90
110
  if (project.ai_context) {
91
111
  parts.push(`\nProject AI Policy:\n${project.ai_context}`);
@@ -0,0 +1,33 @@
1
+ export interface AiActivity {
2
+ id: string;
3
+ type: 'refine' | 'task-chat' | 'project-advisor' | 'global-advisor' | 'watch';
4
+ label: string;
5
+ startedAt: number;
6
+ }
7
+
8
+ // Module-level store — survives React component unmounts.
9
+ let activities: AiActivity[] = [];
10
+ let listeners: Set<() => void> = new Set();
11
+
12
+ function notify() {
13
+ for (const fn of listeners) fn();
14
+ }
15
+
16
+ export function registerAiActivity(activity: AiActivity) {
17
+ activities = [...activities, activity];
18
+ notify();
19
+ }
20
+
21
+ export function unregisterAiActivity(id: string) {
22
+ activities = activities.filter(a => a.id !== id);
23
+ notify();
24
+ }
25
+
26
+ export function getAiActivities(): AiActivity[] {
27
+ return activities;
28
+ }
29
+
30
+ export function subscribeAiActivity(listener: () => void): () => void {
31
+ listeners.add(listener);
32
+ return () => { listeners.delete(listener); };
33
+ }
@@ -0,0 +1,31 @@
1
+ import { getDb } from '../index';
2
+ import { generateId } from '../../utils/id';
3
+ import type { IProjectConversation } from '../../../types';
4
+
5
+ // Reuses IProjectConversation shape — project_id is always '__global__'.
6
+ const GLOBAL_ID = '__global__';
7
+
8
+ export function getGlobalConversations(limit = 50): IProjectConversation[] {
9
+ const db = getDb();
10
+ return db.prepare(
11
+ `SELECT * FROM global_conversations ORDER BY created_at DESC LIMIT ?`
12
+ ).all(limit).reverse() as IProjectConversation[];
13
+ }
14
+
15
+ export function addGlobalConversation(
16
+ role: 'assistant' | 'user' | 'system',
17
+ content: string,
18
+ ): IProjectConversation {
19
+ const db = getDb();
20
+ const id = generateId();
21
+ const now = new Date().toISOString();
22
+ db.prepare(
23
+ 'INSERT INTO global_conversations (id, project_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)'
24
+ ).run(id, GLOBAL_ID, role, content, now);
25
+ return db.prepare('SELECT * FROM global_conversations WHERE id = ?').get(id) as IProjectConversation;
26
+ }
27
+
28
+ export function clearGlobalConversations(): void {
29
+ const db = getDb();
30
+ db.prepare('DELETE FROM global_conversations').run();
31
+ }
@@ -116,6 +116,7 @@ export function updateTask(id: string, data: {
116
116
  priority?: ItemPriority;
117
117
  is_today?: boolean;
118
118
  sort_order?: number;
119
+ project_id?: string;
119
120
  sub_project_id?: string;
120
121
  tags?: string[];
121
122
  }): ITask | undefined {
@@ -127,7 +128,7 @@ export function updateTask(id: string, data: {
127
128
  db.prepare(`
128
129
  UPDATE tasks SET
129
130
  title = ?, description = ?, status = ?, priority = ?,
130
- is_today = ?, sort_order = ?, sub_project_id = ?, tags = ?, updated_at = ?
131
+ is_today = ?, sort_order = ?, project_id = ?, sub_project_id = ?, tags = ?, updated_at = ?
131
132
  WHERE id = ?
132
133
  `).run(
133
134
  data.title ?? row.title,
@@ -136,6 +137,7 @@ export function updateTask(id: string, data: {
136
137
  data.priority ?? row.priority,
137
138
  data.is_today !== undefined ? (data.is_today ? 1 : 0) : row.is_today,
138
139
  data.sort_order ?? row.sort_order,
140
+ data.project_id ?? row.project_id,
139
141
  data.sub_project_id ?? row.sub_project_id,
140
142
  data.tags ? JSON.stringify(data.tags) : row.tags,
141
143
  now,
@@ -96,6 +96,14 @@ export function initSchema(db: any): void {
96
96
  `);
97
97
 
98
98
  db.exec(`
99
+ CREATE TABLE IF NOT EXISTS global_conversations (
100
+ id TEXT PRIMARY KEY,
101
+ project_id TEXT NOT NULL DEFAULT '__global__',
102
+ role TEXT NOT NULL CHECK(role IN ('assistant','user','system')),
103
+ content TEXT NOT NULL,
104
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
105
+ );
106
+
99
107
  CREATE TABLE IF NOT EXISTS project_conversations (
100
108
  id TEXT PRIMARY KEY,
101
109
  project_id TEXT NOT NULL,
@@ -0,0 +1,25 @@
1
+ import type { TaskStatus } from './index';
2
+
3
+ export interface CreateTaskAction {
4
+ type: 'create_task';
5
+ subProjectId: string;
6
+ projectId?: string;
7
+ title: string;
8
+ description?: string;
9
+ priority?: 'high' | 'medium' | 'low';
10
+ status?: TaskStatus;
11
+ }
12
+
13
+ export interface UpdateTaskAction {
14
+ type: 'update_task';
15
+ taskId: string;
16
+ changes: {
17
+ status?: TaskStatus;
18
+ priority?: 'high' | 'medium' | 'low';
19
+ description?: string;
20
+ title?: string;
21
+ is_today?: boolean;
22
+ };
23
+ }
24
+
25
+ export type AdvisorAction = CreateTaskAction | UpdateTaskAction;