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
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import AdvisorChat from './AdvisorChat';
5
+
6
+ export default function GlobalAdvisorLayer() {
7
+ const [open, setOpen] = useState(false);
8
+
9
+ useEffect(() => {
10
+ const onKey = (e: KeyboardEvent) => {
11
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'j') {
12
+ e.preventDefault();
13
+ setOpen(prev => !prev);
14
+ }
15
+ };
16
+ window.addEventListener('keydown', onKey);
17
+ return () => window.removeEventListener('keydown', onKey);
18
+ }, []);
19
+
20
+ if (!open) return null;
21
+
22
+ return (
23
+ <AdvisorChat
24
+ basePath="/api/global-advisor"
25
+ title="Global Advisor"
26
+ shortcutHint="⌘J"
27
+ placeholder="전체 워크스페이스에 대해 물어보세요…"
28
+ emptyIcon="🌐"
29
+ emptyHints={[
30
+ '모든 프로젝트를 조망하고 답합니다',
31
+ '"전체 진행 상황 요약해줘"\n"어떤 프로젝트가 제일 급해?"\n"이번 주 뭐 해야 돼?"',
32
+ ]}
33
+ activityType="global-advisor"
34
+ activityLabel="Global Advisor"
35
+ onClose={() => setOpen(false)}
36
+ />
37
+ );
38
+ }
@@ -87,6 +87,8 @@ export default function DashboardPanel() {
87
87
  useEffect(() => {
88
88
  fetchData();
89
89
  fetch('/api/global-memo').then(r => r.json()).then(d => setMemoContent(d.content || ''));
90
+ // Daily maintenance — clear done from today, auto-archive old done tasks
91
+ fetch('/api/maintenance', { method: 'POST' }).catch(() => {});
90
92
  // Restore localStorage state after mount
91
93
  const savedMemo = localStorage.getItem('im-memo-open');
92
94
  if (savedMemo === 'true') setMemoOpen(true);
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState, useCallback } from 'react';
4
+
5
+ export default function GlobalMemoLayer() {
6
+ const [open, setOpen] = useState(false);
7
+ const [content, setContent] = useState('');
8
+ const [loaded, setLoaded] = useState(false);
9
+ const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
10
+
11
+ useEffect(() => {
12
+ const onKey = (e: KeyboardEvent) => {
13
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'm') {
14
+ e.preventDefault();
15
+ setOpen(prev => !prev);
16
+ }
17
+ };
18
+ window.addEventListener('keydown', onKey);
19
+ return () => window.removeEventListener('keydown', onKey);
20
+ }, []);
21
+
22
+ useEffect(() => {
23
+ if (!open || loaded) return;
24
+ fetch('/api/global-memo')
25
+ .then(r => r.ok ? r.json() : { content: '' })
26
+ .then(d => { setContent(d.content || ''); setLoaded(true); })
27
+ .catch(() => setLoaded(true));
28
+ }, [open, loaded]);
29
+
30
+ const save = useCallback((value: string) => {
31
+ if (saveTimer.current) clearTimeout(saveTimer.current);
32
+ saveTimer.current = setTimeout(() => {
33
+ fetch('/api/global-memo', {
34
+ method: 'PUT',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ content: value }),
37
+ }).catch(() => {});
38
+ }, 600);
39
+ }, []);
40
+
41
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
42
+ const v = e.target.value;
43
+ setContent(v);
44
+ save(v);
45
+ };
46
+
47
+ if (!open) return null;
48
+
49
+ return (
50
+ <div
51
+ onClick={() => setOpen(false)}
52
+ className="fixed inset-0 z-[55] flex items-center justify-center"
53
+ style={{ background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(2px)' }}
54
+ >
55
+ <div
56
+ onClick={(e) => e.stopPropagation()}
57
+ className="bg-card border border-border rounded-xl shadow-2xl w-[520px] max-w-[90vw] h-[65vh] max-h-[550px] flex flex-col animate-dialog-in"
58
+ >
59
+ <div className="px-4 py-2.5 border-b border-border flex items-center justify-between flex-shrink-0">
60
+ <div className="flex items-center gap-2">
61
+ <span className="text-sm font-semibold text-foreground">Quick Memo</span>
62
+ <span className="text-[10px] text-muted-foreground/60">⌘M</span>
63
+ </div>
64
+ <button onClick={() => setOpen(false)} className="text-muted-foreground hover:text-foreground text-lg leading-none">×</button>
65
+ </div>
66
+ <div className="flex-1 min-h-0 p-1">
67
+ <textarea
68
+ value={content}
69
+ onChange={handleChange}
70
+ placeholder="자유롭게 메모하세요… 전역 스크래치패드입니다."
71
+ className="w-full h-full bg-transparent text-sm text-foreground resize-none focus:outline-none p-3 font-mono leading-relaxed"
72
+ autoFocus
73
+ />
74
+ </div>
75
+ <div className="px-4 py-1.5 border-t border-border text-[10px] text-muted-foreground/50 flex-shrink-0">
76
+ 자동 저장 · Esc로 닫기
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
@@ -3,6 +3,7 @@
3
3
  import { useTabContext } from './TabContext';
4
4
  import ThemePicker from '@/components/theme/ThemePicker';
5
5
  import UpdateButton from '@/components/update/UpdateButton';
6
+ import AiActivityIndicator from '@/components/ui/AiActivityIndicator';
6
7
 
7
8
  export default function TabBar() {
8
9
  const { state, setActiveTab, closeTab } = useTabContext();
@@ -44,6 +45,7 @@ export default function TabBar() {
44
45
  );
45
46
  })}
46
47
  <div className="tab-bar-spacer" />
48
+ <AiActivityIndicator />
47
49
  <UpdateButton />
48
50
  <ThemePicker />
49
51
  </div>
@@ -6,6 +6,9 @@ import DashboardPanel from '@/components/dashboard/DashboardPanel';
6
6
  import WorkspacePanel from '@/components/workspace/WorkspacePanel';
7
7
  import GlobalSearch from '@/components/search/GlobalSearch';
8
8
  import QuickCapture from '@/components/search/QuickCapture';
9
+ import ShortcutOverlay from '@/components/ui/ShortcutOverlay';
10
+ import GlobalMemoLayer from '@/components/memo/GlobalMemoLayer';
11
+ import GlobalAdvisorLayer from '@/components/advisor/GlobalAdvisorLayer';
9
12
 
10
13
  export default function TabShell() {
11
14
  const { state } = useTabContext();
@@ -15,6 +18,9 @@ export default function TabShell() {
15
18
  <TabBar />
16
19
  <GlobalSearch />
17
20
  <QuickCapture />
21
+ <ShortcutOverlay />
22
+ <GlobalMemoLayer />
23
+ <GlobalAdvisorLayer />
18
24
  <div className="flex-1 min-h-0 relative">
19
25
  {state.tabs.map((tab) => (
20
26
  <div
@@ -6,6 +6,7 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { EditorView, Decoration, keymap, ViewPlugin, WidgetType } from '@codemirror/view';
7
7
  import { EditorState, StateEffect, StateField, Prec } from '@codemirror/state';
8
8
  import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
9
+ import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
9
10
  import { tags as t } from '@lezer/highlight';
10
11
 
11
12
  // ─────────────────────────────────────────────────────────────
@@ -208,6 +209,73 @@ function dismissGhost(view: EditorView): boolean {
208
209
  return true;
209
210
  }
210
211
 
212
+ // ─────────────────────────────────────────────────────────────
213
+ // Checkbox toggle (⌘Enter): [ ] ↔ [x]
214
+ // ─────────────────────────────────────────────────────────────
215
+ function toggleCheckbox(view: EditorView): boolean {
216
+ const pos = view.state.selection.main.head;
217
+ const line = view.state.doc.lineAt(pos);
218
+ const unchecked = line.text.match(/^(\s*[-*+]\s)\[ \](.*)$/);
219
+ if (unchecked) {
220
+ const replacement = `${unchecked[1]}[x]${unchecked[2]}`;
221
+ view.dispatch({ changes: { from: line.from, to: line.to, insert: replacement } });
222
+ return true;
223
+ }
224
+ const checked = line.text.match(/^(\s*[-*+]\s)\[[xX]\](.*)$/);
225
+ if (checked) {
226
+ const replacement = `${checked[1]}[ ]${checked[2]}`;
227
+ view.dispatch({ changes: { from: line.from, to: line.to, insert: replacement } });
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+
233
+ // ─────────────────────────────────────────────────────────────
234
+ // Table commands: add row below, delete current row
235
+ // ─────────────────────────────────────────────────────────────
236
+ function isTableLine(text: string): boolean {
237
+ return /^\s*\|/.test(text);
238
+ }
239
+
240
+ function isSeparatorLine(text: string): boolean {
241
+ return /^\s*\|[\s:|-]+\|\s*$/.test(text);
242
+ }
243
+
244
+ function countColumns(text: string): number {
245
+ // Count | excluding escaped ones, minus 1 (fence)
246
+ const parts = text.split('|').filter(p => p !== undefined);
247
+ // first and last might be empty (leading/trailing |)
248
+ return Math.max(parts.length - 2, 1);
249
+ }
250
+
251
+ function tableAddRow(view: EditorView): boolean {
252
+ const pos = view.state.selection.main.head;
253
+ const line = view.state.doc.lineAt(pos);
254
+ if (!isTableLine(line.text)) return false;
255
+ const cols = countColumns(line.text);
256
+ const emptyRow = '|' + Array(cols).fill(' ').join('|') + '|';
257
+ view.dispatch({
258
+ changes: { from: line.to, insert: '\n' + emptyRow },
259
+ selection: { anchor: line.to + 2 }, // cursor inside first cell
260
+ });
261
+ return true;
262
+ }
263
+
264
+ function tableDeleteRow(view: EditorView): boolean {
265
+ const pos = view.state.selection.main.head;
266
+ const line = view.state.doc.lineAt(pos);
267
+ if (!isTableLine(line.text) || isSeparatorLine(line.text)) return false;
268
+ // Don't delete if it's the header line (first table line or line after which is separator)
269
+ const nextLine = line.to < view.state.doc.length ? view.state.doc.lineAt(line.to + 1) : null;
270
+ if (nextLine && isSeparatorLine(nextLine.text)) return false; // header row
271
+ const from = line.from > 0 ? line.from - 1 : line.from; // include preceding newline
272
+ view.dispatch({
273
+ changes: { from, to: line.to },
274
+ selection: { anchor: Math.min(from, view.state.doc.length) },
275
+ });
276
+ return true;
277
+ }
278
+
211
279
  // ─────────────────────────────────────────────────────────────
212
280
  // Markdown list / checkbox continuation on Enter.
213
281
  // ─────────────────────────────────────────────────────────────
@@ -259,6 +327,67 @@ function continueList(view: EditorView): boolean {
259
327
  return false;
260
328
  }
261
329
 
330
+ // ─────────────────────────────────────────────────────────────
331
+ // Slash commands — type / at line start to insert structures.
332
+ // ─────────────────────────────────────────────────────────────
333
+ const SLASH_COMMANDS: { label: string; detail: string; insert: string }[] = [
334
+ { label: '/todo', detail: '체크리스트', insert: '- [ ] ' },
335
+ { label: '/h1', detail: '제목 1', insert: '# ' },
336
+ { label: '/h2', detail: '제목 2', insert: '## ' },
337
+ { label: '/h3', detail: '제목 3', insert: '### ' },
338
+ { label: '/bullet', detail: '불릿 리스트', insert: '- ' },
339
+ { label: '/number', detail: '번호 리스트', insert: '1. ' },
340
+ { label: '/quote', detail: '인용', insert: '> ' },
341
+ { label: '/hr', detail: '구분선', insert: '---\n' },
342
+ { label: '/code', detail: '코드 블록', insert: '```\n\n```' },
343
+ { label: '/code ts', detail: 'TypeScript 코드', insert: '```ts\n\n```' },
344
+ { label: '/code py', detail: 'Python 코드', insert: '```python\n\n```' },
345
+ { label: '/code sql', detail: 'SQL 코드', insert: '```sql\n\n```' },
346
+ { label: '/code sh', detail: 'Shell 코드', insert: '```bash\n\n```' },
347
+ { label: '/table', detail: '3열 테이블', insert: '| 열1 | 열2 | 열3 |\n|-----|-----|-----|\n| | | |' },
348
+ { label: '/link', detail: '링크', insert: '[텍스트](url)' },
349
+ { label: '/bold', detail: '굵게', insert: '****' },
350
+ { label: '/details', detail: '접기/펼치기', insert: '<details>\n<summary>제목</summary>\n\n내용\n\n</details>' },
351
+ { label: '/addrow', detail: '테이블 행 추가 (⌘⇧↵)', insert: '' },
352
+ { label: '/delrow', detail: '테이블 행 삭제 (⌘⇧⌫)', insert: '' },
353
+ ];
354
+
355
+ function slashCompletion(context: CompletionContext): CompletionResult | null {
356
+ const line = context.state.doc.lineAt(context.pos);
357
+ const before = line.text.slice(0, context.pos - line.from);
358
+ // Only trigger when `/` is at line start (possibly with leading whitespace)
359
+ const m = before.match(/^(\s*)(\/\S*)$/);
360
+ if (!m) return null;
361
+ const from = line.from + (m[1]?.length ?? 0);
362
+ return {
363
+ from,
364
+ options: SLASH_COMMANDS.map(cmd => ({
365
+ label: cmd.label,
366
+ detail: cmd.detail,
367
+ apply: (view: EditorView, _completion: unknown, from: number, to: number) => {
368
+ // Special table commands — execute action instead of inserting text
369
+ if (cmd.label === '/addrow') {
370
+ view.dispatch({ changes: { from, to, insert: '' } });
371
+ tableAddRow(view);
372
+ return;
373
+ }
374
+ if (cmd.label === '/delrow') {
375
+ view.dispatch({ changes: { from, to, insert: '' } });
376
+ tableDeleteRow(view);
377
+ return;
378
+ }
379
+ // Place cursor inside code blocks (between the fences)
380
+ const cursorOffset = cmd.insert.includes('\n\n```') ? cmd.insert.indexOf('\n\n```') + 1 : cmd.insert.length;
381
+ view.dispatch({
382
+ changes: { from, to, insert: cmd.insert },
383
+ selection: { anchor: from + cursorOffset },
384
+ });
385
+ },
386
+ })),
387
+ filter: true,
388
+ };
389
+ }
390
+
262
391
  // ─────────────────────────────────────────────────────────────
263
392
  // Markdown syntax highlighting — explicit Lezer-tag mapping so list
264
393
  // marks, headings and inline code stand out clearly against plain
@@ -321,6 +450,11 @@ const NoteEditor = forwardRef<ReactCodeMirrorRef, NoteEditorProps>(function Note
321
450
  const extensions = useMemo(() => [
322
451
  markdown({ base: markdownLanguage }),
323
452
  syntaxHighlighting(mdHighlight),
453
+ autocompletion({
454
+ override: [slashCompletion],
455
+ defaultKeymap: true,
456
+ icons: false,
457
+ }),
324
458
  ghostField,
325
459
  ghostDecorations,
326
460
  createLocalCompletionPlugin(corpusRef),
@@ -328,6 +462,9 @@ const NoteEditor = forwardRef<ReactCodeMirrorRef, NoteEditorProps>(function Note
328
462
  { key: 'Tab', run: acceptGhost },
329
463
  { key: 'Escape', run: dismissGhost },
330
464
  { key: 'Enter', run: continueList },
465
+ { key: 'Mod-Enter', run: toggleCheckbox },
466
+ { key: 'Mod-Shift-Enter', run: tableAddRow },
467
+ { key: 'Mod-Shift-Backspace', run: tableDeleteRow },
331
468
  { key: 'Mod-k', run: () => { onOpenCommand?.(); return true; } },
332
469
  { key: 'Mod-Shift-t', run: () => { onPromoteLine?.(); return true; } },
333
470
  ])),
@@ -50,6 +50,7 @@ export default function ProjectTree({
50
50
  onTodayToggle,
51
51
  onDeleteTask,
52
52
  onReorderSubs,
53
+ onReorderTasks,
53
54
  onAutoDistribute,
54
55
  chatStates,
55
56
  }: {
@@ -67,6 +68,7 @@ export default function ProjectTree({
67
68
  onTodayToggle: (taskId: string, isToday: boolean) => void;
68
69
  onDeleteTask: (taskId: string) => void;
69
70
  onReorderSubs?: (orderedIds: string[]) => void;
71
+ onReorderTasks?: (orderedIds: string[]) => void;
70
72
  onAutoDistribute?: () => void;
71
73
  chatStates?: Record<string, 'idle' | 'loading' | 'done'>;
72
74
  }) {
@@ -172,6 +174,7 @@ export default function ProjectTree({
172
174
  onStatusChange={onStatusChange}
173
175
  onTodayToggle={onTodayToggle}
174
176
  onDeleteTask={onDeleteTask}
177
+ onReorderTasks={onReorderTasks}
175
178
  onAddTask={handleAddTask}
176
179
  onSetAddingTaskFor={setAddingTaskFor}
177
180
  onSetNewTaskTitle={setNewTaskTitle}
@@ -201,6 +204,7 @@ function SortableSubProject({
201
204
  onStatusChange,
202
205
  onTodayToggle,
203
206
  onDeleteTask,
207
+ onReorderTasks,
204
208
  onAddTask,
205
209
  onSetAddingTaskFor,
206
210
  onSetNewTaskTitle,
@@ -221,6 +225,7 @@ function SortableSubProject({
221
225
  onStatusChange: (taskId: string, status: TaskStatus) => void;
222
226
  onTodayToggle: (taskId: string, isToday: boolean) => void;
223
227
  onDeleteTask: (taskId: string) => void;
228
+ onReorderTasks?: (orderedIds: string[]) => void;
224
229
  onAddTask: (subId: string) => void;
225
230
  onSetAddingTaskFor: (subId: string | null) => void;
226
231
  onSetNewTaskTitle: (title: string) => void;
@@ -229,6 +234,23 @@ function SortableSubProject({
229
234
  const [editing, setEditing] = useState(false);
230
235
  const [editValue, setEditValue] = useState(sp.name);
231
236
 
237
+ const taskSensors = useSensors(
238
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
239
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
240
+ );
241
+
242
+ const handleTaskDragEnd = (event: DragEndEvent) => {
243
+ const { active, over } = event;
244
+ if (!over || active.id === over.id || !onReorderTasks) return;
245
+ const oldIndex = subTasks.findIndex(t => t.id === active.id);
246
+ const newIndex = subTasks.findIndex(t => t.id === over.id);
247
+ if (oldIndex === -1 || newIndex === -1) return;
248
+ const newOrder = [...subTasks];
249
+ const [moved] = newOrder.splice(oldIndex, 1);
250
+ newOrder.splice(newIndex, 0, moved);
251
+ onReorderTasks(newOrder.map(t => t.id));
252
+ };
253
+
232
254
  const {
233
255
  attributes,
234
256
  listeners,
@@ -348,63 +370,22 @@ function SortableSubProject({
348
370
  No tasks
349
371
  </div>
350
372
  )}
351
- {subTasks.map((task) => (
352
- <div
353
- key={task.id}
354
- onClick={() => onSelectTask(task.id)}
355
- 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 ${
356
- selectedTaskId === task.id
357
- ? 'bg-card-hover border-l-primary'
358
- : 'border-l-transparent hover:bg-card-hover/50'
359
- }`}
360
- >
361
- <button
362
- onClick={(e) => {
363
- e.stopPropagation();
364
- const nextStatus = getNextStatus(task.status);
365
- onStatusChange(task.id, nextStatus);
366
- }}
367
- className="flex-shrink-0 text-sm"
368
- title={`Status: ${task.status}`}
369
- >
370
- {statusIcon(task.status)}
371
- </button>
372
- <span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
373
- <span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
374
- {task.title}
375
- </span>
376
- {chatStates?.[task.id] === 'loading' && (
377
- <span className="flex-shrink-0 flex items-center gap-1 text-[10px] text-warning" title="AI 응답 대기 중">
378
- <span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
379
- AI...
380
- </span>
381
- )}
382
- {chatStates?.[task.id] === 'done' && (
383
- <span className="flex-shrink-0 text-[10px] text-success" title="AI 응답 완료">
384
-
385
- </span>
386
- )}
387
- {task.is_today && (
388
- <button
389
- onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }}
390
- className="text-xs flex-shrink-0 text-primary" title="Remove from today"
391
- >
392
- *
393
- </button>
394
- )}
395
- <button
396
- onClick={(e) => {
397
- e.stopPropagation();
398
- onDeleteTask(task.id);
399
- }}
400
- className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground
401
- hover:!text-destructive transition-colors text-xs px-0.5"
402
- title="Delete task"
403
- >
404
- ×
405
- </button>
406
- </div>
407
- ))}
373
+ <DndContext sensors={taskSensors} collisionDetection={closestCenter} onDragEnd={handleTaskDragEnd}>
374
+ <SortableContext items={subTasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
375
+ {subTasks.map((task) => (
376
+ <SortableTask
377
+ key={task.id}
378
+ task={task}
379
+ isSelected={selectedTaskId === task.id}
380
+ chatState={chatStates?.[task.id]}
381
+ onSelect={() => onSelectTask(task.id)}
382
+ onStatusChange={onStatusChange}
383
+ onTodayToggle={onTodayToggle}
384
+ onDelete={() => onDeleteTask(task.id)}
385
+ />
386
+ ))}
387
+ </SortableContext>
388
+ </DndContext>
408
389
 
409
390
  {/* Add task input */}
410
391
  {addingTaskFor === sp.id ? (
@@ -455,6 +436,73 @@ function SortableSubProject({
455
436
  );
456
437
  }
457
438
 
439
+ function SortableTask({
440
+ task,
441
+ isSelected,
442
+ chatState,
443
+ onSelect,
444
+ onStatusChange,
445
+ onTodayToggle,
446
+ onDelete,
447
+ }: {
448
+ task: ITask;
449
+ isSelected: boolean;
450
+ chatState?: 'idle' | 'loading' | 'done';
451
+ onSelect: () => void;
452
+ onStatusChange: (taskId: string, status: TaskStatus) => void;
453
+ onTodayToggle: (taskId: string, isToday: boolean) => void;
454
+ onDelete: () => void;
455
+ }) {
456
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id });
457
+ const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1 };
458
+
459
+ return (
460
+ <div
461
+ ref={setNodeRef}
462
+ style={style}
463
+ onClick={onSelect}
464
+ className={`group/task flex items-center gap-1 pl-2 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
465
+ isSelected ? 'bg-card-hover border-l-primary' : 'border-l-transparent hover:bg-card-hover/50'
466
+ }`}
467
+ >
468
+ <span
469
+ {...attributes}
470
+ {...listeners}
471
+ className="w-3 h-4 flex items-center justify-center text-[10px] text-muted-foreground/30 hover:text-muted-foreground cursor-grab active:cursor-grabbing flex-shrink-0"
472
+ onClick={(e) => e.stopPropagation()}
473
+ >
474
+
475
+ </span>
476
+ <button
477
+ onClick={(e) => { e.stopPropagation(); onStatusChange(task.id, getNextStatus(task.status)); }}
478
+ className="flex-shrink-0 text-sm"
479
+ title={`Status: ${task.status}`}
480
+ >
481
+ {statusIcon(task.status)}
482
+ </button>
483
+ <span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
484
+ <span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
485
+ {task.title}
486
+ </span>
487
+ {chatState === 'loading' && (
488
+ <span className="flex-shrink-0 flex items-center gap-1 text-[10px] text-warning">
489
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
490
+ </span>
491
+ )}
492
+ {chatState === 'done' && <span className="flex-shrink-0 text-[10px] text-success">✓</span>}
493
+ {task.is_today && (
494
+ <button onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }} className="text-xs flex-shrink-0 text-primary">*</button>
495
+ )}
496
+ <button
497
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
498
+ className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground hover:!text-destructive transition-colors text-xs px-0.5"
499
+ >
500
+ ×
501
+ </button>
502
+ </div>
503
+ );
504
+ }
505
+
458
506
  function getNextStatus(current: TaskStatus): TaskStatus {
459
507
  const flow: TaskStatus[] = ['idea', 'doing', 'done'];
460
508
  const idx = flow.indexOf(current);
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback } from 'react';
4
4
  import type { ITaskConversation, TaskStatus } from '@/types';
5
+ import { registerAiActivity, unregisterAiActivity } from '@/lib/ai-activity';
5
6
  import ReactMarkdown from 'react-markdown';
6
7
  import remarkGfm from 'remark-gfm';
7
8
 
@@ -82,9 +83,11 @@ export default function TaskChat({
82
83
  if (!text || loading) return;
83
84
 
84
85
  const sendPath = basePath;
86
+ const actId = `task-chat-${Date.now()}`;
85
87
  setInput('');
86
88
  setLoading(true);
87
89
  onChatStateChange?.('loading');
90
+ registerAiActivity({ id: actId, type: 'task-chat', label: 'Note Assistant', startedAt: Date.now() });
88
91
 
89
92
  // Optimistic user message
90
93
  const tempId = `temp-${Date.now()}`;
@@ -116,6 +119,7 @@ export default function TaskChat({
116
119
  }
117
120
  }
118
121
  } catch { /* silent */ }
122
+ unregisterAiActivity(actId);
119
123
  if (basePathRef.current === sendPath) {
120
124
  setLoading(false);
121
125
  inputRef.current?.focus();