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.
- package/.next/build-manifest.json +2 -2
- package/.next/routes-manifest.json +35 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/advisor-actions/route.js +15 -0
- package/.next/server/app/api/advisor-actions/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/archive/route.js +1 -122
- package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/global-advisor/route.js +37 -0
- package/.next/server/app/api/global-advisor/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/global-memo/route.js +8 -0
- package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/maintenance/route.js +130 -0
- package/.next/server/app/api/maintenance/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/projects/[id]/advisor/route.js +22 -11
- package/.next/server/app/api/projects/[id]/advisor/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/apply-distribute/route.js +2 -8
- package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/auto-distribute/route.js +126 -3
- package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/brainstorm/route.js +124 -1
- package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/git-sync/route.js +124 -1
- package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/route.js +124 -1
- package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +8 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.js +1 -7
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.js +8 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.js +2 -8
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.js +1 -122
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.js +124 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route.js +1 -122
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/route.js +8 -0
- package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route.js +124 -1
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/search/route.js +8 -0
- package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sync/route.js +8 -0
- package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/move/route.js +15 -0
- package/.next/server/app/api/tasks/[taskId]/move/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +12 -12
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +18 -13
- package/.next/server/chunks/{117.js → 697.js} +16 -2
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/static/KREG104cVn2mBTMPTDTvH/_buildManifest.js +1 -0
- package/.next/static/chunks/374-23189d7e246ad164.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/advisor-actions/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/archive/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/filesystem/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/filesystem/tree/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/global-advisor/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/global-memo/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/health/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/maintenance/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/advisor/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/search/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/sync/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/tasks/[taskId]/move/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/update/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/version/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/page-9117037f2947f4f6.js +28 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f051f234bea7bddd.js +1 -0
- package/.next/static/css/e9071b58a99b47e4.css +3 -0
- package/package.json +1 -1
- package/src/app/api/advisor-actions/route.ts +52 -0
- package/src/app/api/global-advisor/route.ts +50 -0
- package/src/app/api/maintenance/route.ts +36 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.ts +24 -0
- package/src/app/api/tasks/[taskId]/move/route.ts +30 -0
- package/src/components/advisor/ActionBlock.tsx +124 -0
- package/src/components/advisor/AdvisorChat.tsx +175 -0
- package/src/components/advisor/GlobalAdvisorLayer.tsx +38 -0
- package/src/components/dashboard/DashboardPanel.tsx +2 -0
- package/src/components/memo/GlobalMemoLayer.tsx +81 -0
- package/src/components/tabs/TabBar.tsx +2 -0
- package/src/components/tabs/TabShell.tsx +6 -0
- package/src/components/task/NoteEditor.tsx +137 -0
- package/src/components/task/ProjectTree.tsx +105 -57
- package/src/components/task/TaskChat.tsx +4 -0
- package/src/components/task/TaskDetail.tsx +182 -1
- package/src/components/ui/AiActivityIndicator.tsx +66 -0
- package/src/components/ui/ShortcutOverlay.tsx +108 -0
- package/src/components/workspace/ProjectAdvisor.tsx +17 -181
- package/src/components/workspace/WorkspacePanel.tsx +75 -3
- package/src/hooks/useAiActivity.ts +6 -0
- package/src/lib/advisor-actions/parse.ts +59 -0
- package/src/lib/ai/global-context.ts +114 -0
- package/src/lib/ai/project-context.ts +22 -2
- package/src/lib/ai-activity.ts +33 -0
- package/src/lib/db/queries/global-conversations.ts +31 -0
- package/src/lib/db/queries/tasks.ts +3 -1
- package/src/lib/db/schema.ts +8 -0
- package/src/types/advisor-actions.ts +25 -0
- package/.next/static/chunks/374-769431701aab500f.js +0 -1
- package/.next/static/chunks/app/_global-error/page-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/archive/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/filesystem/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/filesystem/tree/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/global-memo/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/health/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/advisor/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/search/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/sync/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/update/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/version/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/page-e935ee928da68ca2.js +0 -28
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/css/e4c7cd5a570312d9.css +0 -3
- package/.next/static/pxqzEiwniZAUDOUTb5SnX/_buildManifest.js +0 -1
- /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
|
-
{
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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();
|