idea-manager 1.5.1 → 1.6.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 (151) hide show
  1. package/.next/build-manifest.json +2 -2
  2. package/.next/prerender-manifest.json +3 -3
  3. package/.next/required-server-files.js +5 -0
  4. package/.next/required-server-files.json +5 -0
  5. package/.next/routes-manifest.json +10 -0
  6. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  7. package/.next/server/app/_global-error.html +2 -2
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +2 -2
  17. package/.next/server/app/_not-found.rsc +2 -2
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/server/app/api/archive/route.js +34 -4
  25. package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/global-memo/route.js +34 -4
  29. package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/projects/[id]/apply-distribute/route.js +6 -82
  32. package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/projects/[id]/auto-distribute/route.js +6 -6
  34. package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/projects/[id]/brainstorm/route.js +1 -77
  36. package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/projects/[id]/git-sync/route.js +1 -77
  38. package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/projects/[id]/route.js +1 -77
  40. package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
  41. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +38 -8
  42. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.js +15 -10
  44. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.js +34 -4
  46. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.js +26 -0
  48. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -0
  49. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.js +34 -4
  50. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route.js +34 -4
  52. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/projects/[id]/sub-projects/route.js +38 -8
  54. package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
  55. package/.next/server/app/api/projects/route.js +1 -77
  56. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/sync/route.js +34 -4
  58. package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/index.html +2 -2
  60. package/.next/server/app/index.rsc +3 -3
  61. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  62. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  63. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  64. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  65. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  66. package/.next/server/app/page.js +15 -6
  67. package/.next/server/app/page_client-reference-manifest.js +1 -1
  68. package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  69. package/.next/server/app-paths-manifest.json +9 -8
  70. package/.next/server/chunks/117.js +107 -0
  71. package/.next/server/pages/404.html +2 -2
  72. package/.next/server/pages/500.html +2 -2
  73. package/.next/server/server-reference-manifest.json +1 -1
  74. package/.next/static/chunks/363642f4-9eb39e0bc542c65b.js +1 -0
  75. package/.next/static/chunks/374-23189d7e246ad164.js +1 -0
  76. package/.next/static/chunks/app/_global-error/page-6ec0e723e471f87a.js +1 -0
  77. package/.next/static/chunks/app/api/archive/route-6ec0e723e471f87a.js +1 -0
  78. package/.next/static/chunks/app/api/filesystem/route-6ec0e723e471f87a.js +1 -0
  79. package/.next/static/chunks/app/api/filesystem/tree/route-6ec0e723e471f87a.js +1 -0
  80. package/.next/static/chunks/app/api/global-memo/route-6ec0e723e471f87a.js +1 -0
  81. package/.next/static/chunks/app/api/health/route-6ec0e723e471f87a.js +1 -0
  82. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-6ec0e723e471f87a.js +1 -0
  83. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-6ec0e723e471f87a.js +1 -0
  84. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-6ec0e723e471f87a.js +1 -0
  85. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-6ec0e723e471f87a.js +1 -0
  86. package/.next/static/chunks/app/api/projects/[id]/route-6ec0e723e471f87a.js +1 -0
  87. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-6ec0e723e471f87a.js +1 -0
  88. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-6ec0e723e471f87a.js +1 -0
  89. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-6ec0e723e471f87a.js +1 -0
  90. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-6ec0e723e471f87a.js +1 -0
  91. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-6ec0e723e471f87a.js +1 -0
  92. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-6ec0e723e471f87a.js +1 -0
  93. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-6ec0e723e471f87a.js +1 -0
  94. package/.next/static/chunks/app/api/projects/route-6ec0e723e471f87a.js +1 -0
  95. package/.next/static/chunks/app/api/sync/route-6ec0e723e471f87a.js +1 -0
  96. package/.next/static/chunks/app/page-6a511af64da7531f.js +28 -0
  97. package/.next/static/chunks/next/dist/client/components/builtin/app-error-6ec0e723e471f87a.js +1 -0
  98. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-6ec0e723e471f87a.js +1 -0
  99. package/.next/static/chunks/next/dist/client/components/builtin/not-found-6ec0e723e471f87a.js +1 -0
  100. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-6ec0e723e471f87a.js +1 -0
  101. package/.next/static/css/cc32379d0efa7d1d.css +3 -0
  102. package/next.config.mjs +3 -0
  103. package/package.json +11 -6
  104. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +9 -5
  105. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.ts +76 -0
  106. package/src/components/dashboard/DashboardPanel.tsx +1 -1
  107. package/src/components/dashboard/SubProjectCard.tsx +1 -0
  108. package/src/components/task/CommandPalette.tsx +137 -0
  109. package/src/components/task/NoteEditor.tsx +411 -0
  110. package/src/components/task/ProjectTree.tsx +1 -1
  111. package/src/components/task/StatusFlow.tsx +43 -20
  112. package/src/components/task/TaskChat.tsx +7 -7
  113. package/src/components/task/TaskDetail.tsx +270 -89
  114. package/src/components/task/TaskList.tsx +1 -1
  115. package/src/components/workspace/WorkspacePanel.tsx +8 -3
  116. package/src/lib/ai/agents.ts +3 -3
  117. package/src/lib/ai/client.ts +3 -1
  118. package/src/lib/db/index.ts +4 -1
  119. package/src/lib/db/queries/sub-projects.ts +3 -3
  120. package/src/lib/db/queries/tasks.ts +1 -1
  121. package/src/lib/db/schema.ts +60 -1
  122. package/src/types/index.ts +3 -1
  123. package/.next/server/chunks/806.js +0 -77
  124. package/.next/static/chunks/151-332d463cd8bd4db6.js +0 -1
  125. package/.next/static/chunks/app/_global-error/page-fd75b71b49e9729e.js +0 -1
  126. package/.next/static/chunks/app/api/archive/route-fd75b71b49e9729e.js +0 -1
  127. package/.next/static/chunks/app/api/filesystem/route-fd75b71b49e9729e.js +0 -1
  128. package/.next/static/chunks/app/api/filesystem/tree/route-fd75b71b49e9729e.js +0 -1
  129. package/.next/static/chunks/app/api/global-memo/route-fd75b71b49e9729e.js +0 -1
  130. package/.next/static/chunks/app/api/health/route-fd75b71b49e9729e.js +0 -1
  131. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-fd75b71b49e9729e.js +0 -1
  132. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-fd75b71b49e9729e.js +0 -1
  133. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-fd75b71b49e9729e.js +0 -1
  134. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-fd75b71b49e9729e.js +0 -1
  135. package/.next/static/chunks/app/api/projects/[id]/route-fd75b71b49e9729e.js +0 -1
  136. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-fd75b71b49e9729e.js +0 -1
  137. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-fd75b71b49e9729e.js +0 -1
  138. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-fd75b71b49e9729e.js +0 -1
  139. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-fd75b71b49e9729e.js +0 -1
  140. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-fd75b71b49e9729e.js +0 -1
  141. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-fd75b71b49e9729e.js +0 -1
  142. package/.next/static/chunks/app/api/projects/route-fd75b71b49e9729e.js +0 -1
  143. package/.next/static/chunks/app/api/sync/route-fd75b71b49e9729e.js +0 -1
  144. package/.next/static/chunks/app/page-d0d563bda0034c18.js +0 -19
  145. package/.next/static/chunks/next/dist/client/components/builtin/app-error-fd75b71b49e9729e.js +0 -1
  146. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-fd75b71b49e9729e.js +0 -1
  147. package/.next/static/chunks/next/dist/client/components/builtin/not-found-fd75b71b49e9729e.js +0 -1
  148. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-fd75b71b49e9729e.js +0 -1
  149. package/.next/static/css/22a3bf63fb41db4f.css +0 -3
  150. /package/.next/static/{3dIOxF31xgLe9pGE0yrsa → 63zinfEtSLCdG9nUZ3W-E}/_buildManifest.js +0 -0
  151. /package/.next/static/{3dIOxF31xgLe9pGE0yrsa → 63zinfEtSLCdG9nUZ3W-E}/_ssgManifest.js +0 -0
@@ -456,7 +456,7 @@ function SortableSubProject({
456
456
  }
457
457
 
458
458
  function getNextStatus(current: TaskStatus): TaskStatus {
459
- const flow: TaskStatus[] = ['idea', 'writing', 'submitted', 'testing', 'done'];
459
+ const flow: TaskStatus[] = ['idea', 'doing', 'done'];
460
460
  const idx = flow.indexOf(current);
461
461
  if (idx === -1) return 'idea';
462
462
  return flow[(idx + 1) % flow.length];
@@ -1,16 +1,24 @@
1
1
  'use client';
2
2
 
3
3
  import type { TaskStatus } from '@/types';
4
+ import { ACTIVE_STATUSES, LEGACY_STATUSES } from '@/types';
4
5
 
5
- const STATUSES: { key: TaskStatus; label: string; icon: string; color: string }[] = [
6
- { key: 'idea', label: 'Idea', icon: '\u{1F4A1}', color: 'text-muted-foreground' },
7
- { key: 'writing', label: 'Writing', icon: '\u{270F}\u{FE0F}', color: 'text-warning' },
6
+ type StatusMeta = { key: TaskStatus; label: string; icon: string; color: string };
7
+
8
+ const ALL: StatusMeta[] = [
9
+ { key: 'idea', label: 'Idea', icon: '\u{1F4A1}', color: 'text-muted-foreground' },
10
+ { key: 'doing', label: 'Doing', icon: '\u{1F525}', color: 'text-primary' },
11
+ { key: 'writing', label: 'Writing', icon: '\u{270F}\u{FE0F}', color: 'text-warning' },
8
12
  { key: 'submitted', label: 'Submitted', icon: '\u{1F680}', color: 'text-primary' },
9
- { key: 'testing', label: 'Testing', icon: '\u{1F9EA}', color: 'text-accent' },
10
- { key: 'done', label: 'Done', icon: '\u{2705}', color: 'text-success' },
11
- { key: 'problem', label: 'Problem', icon: '\u{1F534}', color: 'text-destructive' },
13
+ { key: 'testing', label: 'Testing', icon: '\u{1F9EA}', color: 'text-accent' },
14
+ { key: 'done', label: 'Done', icon: '\u{2705}', color: 'text-success' },
15
+ { key: 'problem', label: 'Problem', icon: '\u{1F534}', color: 'text-destructive' },
12
16
  ];
13
17
 
18
+ function meta(key: TaskStatus): StatusMeta {
19
+ return ALL.find(s => s.key === key) ?? ALL[0];
20
+ }
21
+
14
22
  export default function StatusFlow({
15
23
  status,
16
24
  onChange,
@@ -18,26 +26,41 @@ export default function StatusFlow({
18
26
  status: TaskStatus;
19
27
  onChange: (status: TaskStatus) => void;
20
28
  }) {
29
+ const isLegacy = LEGACY_STATUSES.includes(status);
30
+ const current = meta(status);
31
+
21
32
  return (
22
33
  <div className="flex items-center gap-1">
23
- {STATUSES.map((s) => (
24
- <button
25
- key={s.key}
26
- onClick={() => onChange(s.key)}
27
- title={s.label}
28
- className={`px-2 py-1 rounded text-base transition-all ${
29
- status === s.key
30
- ? `${s.color} bg-muted scale-110`
31
- : 'opacity-40 hover:opacity-80'
32
- }`}
34
+ {isLegacy && (
35
+ <span
36
+ title={`Legacy: ${current.label} (클릭해서 새 상태로 이동)`}
37
+ className={`px-2 py-1 rounded text-xs ${current.color} bg-muted/50 border border-dashed border-muted-foreground/30 mr-1`}
33
38
  >
34
- {s.icon}
35
- </button>
36
- ))}
39
+ {current.icon} {current.label}
40
+ </span>
41
+ )}
42
+ {ACTIVE_STATUSES.map((key) => {
43
+ const s = meta(key);
44
+ const active = status === key;
45
+ return (
46
+ <button
47
+ key={key}
48
+ onClick={() => onChange(key)}
49
+ title={s.label}
50
+ className={`px-2 py-1 rounded text-base transition-all ${
51
+ active
52
+ ? `${s.color} bg-muted scale-110`
53
+ : 'opacity-40 hover:opacity-80'
54
+ }`}
55
+ >
56
+ {s.icon}
57
+ </button>
58
+ );
59
+ })}
37
60
  </div>
38
61
  );
39
62
  }
40
63
 
41
64
  export function statusIcon(status: TaskStatus): string {
42
- return STATUSES.find(s => s.key === status)?.icon ?? '';
65
+ return meta(status).icon;
43
66
  }
@@ -22,12 +22,12 @@ function notifyAiResponse(preview: string) {
22
22
  export default function TaskChat({
23
23
  basePath,
24
24
  taskStatus,
25
- onApplyToPrompt,
25
+ onInsertToNote,
26
26
  onChatStateChange,
27
27
  }: {
28
28
  basePath: string;
29
29
  taskStatus?: TaskStatus;
30
- onApplyToPrompt: (content: string) => void;
30
+ onInsertToNote: (content: string) => void;
31
31
  onChatStateChange?: (state: 'idle' | 'loading' | 'done') => void;
32
32
  }) {
33
33
  const [messages, setMessages] = useState<ITaskConversation[]>([]);
@@ -131,9 +131,9 @@ export default function TaskChat({
131
131
  };
132
132
 
133
133
  return (
134
- <div className="flex flex-col h-full border-t border-border">
134
+ <div className="flex flex-col h-full">
135
135
  <div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
136
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">AI Chat</span>
136
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Note Assistant</span>
137
137
  {taskStatus === 'testing' && (
138
138
  <span className="flex items-center gap-1.5 text-xs text-warning">
139
139
  <span className="inline-block w-2 h-2 rounded-full bg-warning animate-pulse" />
@@ -146,7 +146,7 @@ export default function TaskChat({
146
146
  <div className="flex-1 overflow-y-auto px-3 py-2 space-y-2 min-h-0">
147
147
  {messages.length === 0 && !loading && (
148
148
  <div className="text-sm text-muted-foreground text-center py-4">
149
- Ask AI to help refine your task or prompt
149
+ 노트 작성을 도와드립니다. 질문하거나 &quot;이 부분 정리해줘&quot; 같이 요청해보세요
150
150
  </div>
151
151
  )}
152
152
  {messages.filter(msg => msg.content).map((msg) => (
@@ -162,10 +162,10 @@ export default function TaskChat({
162
162
  </div>
163
163
  {msg.role === 'assistant' && (
164
164
  <button
165
- onClick={() => onApplyToPrompt(msg.content)}
165
+ onClick={() => onInsertToNote(msg.content)}
166
166
  className="text-xs text-muted-foreground hover:text-primary mt-0.5 px-1 transition-colors"
167
167
  >
168
- Apply to prompt
168
+ 노트에 삽입
169
169
  </button>
170
170
  )}
171
171
  </div>
@@ -1,15 +1,18 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
4
4
  import type { ITask, TaskStatus, ItemPriority } from '@/types';
5
5
  import StatusFlow from './StatusFlow';
6
- import PromptEditor from './PromptEditor';
7
6
  import TaskChat from './TaskChat';
7
+ import NoteEditor from './NoteEditor';
8
+ import CommandPalette, { type RefineCommand } from './CommandPalette';
9
+ import type { ReactCodeMirrorRef } from '@uiw/react-codemirror';
8
10
 
9
11
  export default function TaskDetail({
10
12
  task,
11
13
  projectId,
12
14
  subProjectId,
15
+ siblingTasks,
13
16
  onUpdate,
14
17
  onDelete,
15
18
  onChatStateChange,
@@ -17,28 +20,59 @@ export default function TaskDetail({
17
20
  task: ITask;
18
21
  projectId: string;
19
22
  subProjectId: string;
23
+ /** Other tasks under the same sub-project — used to widen autocomplete corpus. */
24
+ siblingTasks?: ITask[];
20
25
  onUpdate: (data: Partial<ITask>) => void;
21
26
  onDelete: () => void;
22
27
  onChatStateChange?: (taskId: string, state: 'idle' | 'loading' | 'done') => void;
23
28
  }) {
24
29
  const [title, setTitle] = useState(task.title);
25
30
  const [description, setDescription] = useState(task.description);
26
- const [promptContent, setPromptContent] = useState('');
27
- const [refining, setRefining] = useState(false);
28
31
  const [editingTitle, setEditingTitle] = useState(false);
29
- const [showPromptModal, setShowPromptModal] = useState(false);
32
+ const [copied, setCopied] = useState(false);
33
+ const [chatOpen, setChatOpen] = useState(false);
34
+ const [paletteOpen, setPaletteOpen] = useState(false);
35
+ const [refining, setRefining] = useState(false);
36
+ const [refineElapsed, setRefineElapsed] = useState(0);
37
+ const [refineError, setRefineError] = useState<string | null>(null);
38
+ const [hasSelection, setHasSelection] = useState(false);
39
+ const [undoSnapshot, setUndoSnapshot] = useState<{ taskId: string; doc: string } | null>(null);
40
+ const refineAbortRef = useRef<AbortController | null>(null);
30
41
 
31
42
  const basePath = `/api/projects/${projectId}/sub-projects/${subProjectId}/tasks/${task.id}`;
32
- const overlayRef = useRef<HTMLDivElement>(null);
43
+ const editorRef = useRef<ReactCodeMirrorRef>(null);
44
+ const [brainstormText, setBrainstormText] = useState('');
45
+
46
+ // Fetch the project's brainstorm once per project — used as autocomplete corpus.
47
+ useEffect(() => {
48
+ let cancelled = false;
49
+ fetch(`/api/projects/${projectId}/brainstorm`)
50
+ .then(r => r.ok ? r.json() : null)
51
+ .then(data => {
52
+ if (cancelled) return;
53
+ setBrainstormText(typeof data?.content === 'string' ? data.content : '');
54
+ })
55
+ .catch(() => { /* silent — corpus is non-critical */ });
56
+ return () => { cancelled = true; };
57
+ }, [projectId]);
58
+
59
+ const extraCorpus = useMemo<string[]>(() => {
60
+ const parts: string[] = [];
61
+ if (siblingTasks) {
62
+ for (const t of siblingTasks) {
63
+ if (t.id === task.id) continue;
64
+ if (t.title) parts.push(t.title);
65
+ if (t.description) parts.push(t.description);
66
+ }
67
+ }
68
+ if (brainstormText) parts.push(brainstormText);
69
+ return parts;
70
+ }, [siblingTasks, brainstormText, task.id]);
33
71
 
34
- // Load prompt
35
72
  useEffect(() => {
36
73
  setTitle(task.title);
37
74
  setDescription(task.description);
38
- fetch(`${basePath}/prompt`)
39
- .then(r => r.json())
40
- .then(data => setPromptContent(data.content || ''));
41
- }, [task.id, task.title, task.description, basePath]);
75
+ }, [task.id, task.title, task.description]);
42
76
 
43
77
  const saveTitle = useCallback(() => {
44
78
  const trimmed = title.trim();
@@ -56,47 +90,152 @@ export default function TaskDetail({
56
90
  }
57
91
  }, [description, task.description, onUpdate]);
58
92
 
59
- const savePrompt = useCallback(async (content: string) => {
60
- setPromptContent(content);
61
- await fetch(`${basePath}/prompt`, {
62
- method: 'PUT',
63
- headers: { 'Content-Type': 'application/json' },
64
- body: JSON.stringify({ content, prompt_type: 'manual' }),
65
- });
66
- }, [basePath]);
93
+ const insertIntoNote = useCallback((content: string) => {
94
+ const next = description
95
+ ? `${description.trimEnd()}\n\n${content.trim()}\n`
96
+ : `${content.trim()}\n`;
97
+ setDescription(next);
98
+ onUpdate({ description: next });
99
+ }, [description, onUpdate]);
100
+
101
+ const copyAsPrompt = useCallback(async () => {
102
+ const body = description?.trim() || '(비어있음)';
103
+ const text = `# ${task.title}\n\n${body}\n`;
104
+ try {
105
+ await navigator.clipboard.writeText(text);
106
+ setCopied(true);
107
+ setTimeout(() => setCopied(false), 1500);
108
+ } catch { /* silent */ }
109
+ }, [task.title, description]);
110
+
111
+ // Command palette — captures selection from CM view and replaces/inserts
112
+ const openPalette = useCallback(() => {
113
+ const view = editorRef.current?.view;
114
+ if (view) {
115
+ const sel = view.state.selection.main;
116
+ setHasSelection(!sel.empty);
117
+ }
118
+ setPaletteOpen(true);
119
+ }, []);
120
+
121
+ const runRefine = useCallback(async (cmd: RefineCommand, customText?: string) => {
122
+ const view = editorRef.current?.view;
123
+ if (!view) { setPaletteOpen(false); return; }
67
124
 
68
- const handleRefine = useCallback(async () => {
125
+ const launchTaskId = task.id;
126
+ const sel = view.state.selection.main;
127
+ const selFrom = sel.from;
128
+ const selTo = sel.to;
129
+ const selectionText = sel.empty ? '' : view.state.sliceDoc(selFrom, selTo);
130
+ const snapshotDoc = view.state.doc.toString();
131
+ setPaletteOpen(false);
132
+ setRefineError(null);
133
+ setUndoSnapshot(null);
69
134
  setRefining(true);
135
+ setRefineElapsed(0);
136
+ const abort = new AbortController();
137
+ refineAbortRef.current = abort;
138
+ const started = Date.now();
139
+ const tick = setInterval(() => setRefineElapsed(Math.floor((Date.now() - started) / 1000)), 500);
140
+
70
141
  try {
71
- const res = await fetch(`${basePath}/chat`, {
142
+ const res = await fetch(`${basePath}/refine`, {
72
143
  method: 'POST',
73
144
  headers: { 'Content-Type': 'application/json' },
74
145
  body: JSON.stringify({
75
- message: `Please refine and improve this prompt for a coding assistant. Current prompt: ${promptContent || '(empty - generate one based on the task)'}. Task: ${task.title}. Description: ${task.description}. Output ONLY the improved prompt text, nothing else.`,
146
+ command: cmd,
147
+ customText,
148
+ selection: selectionText || undefined,
149
+ note: snapshotDoc,
76
150
  }),
151
+ signal: abort.signal,
77
152
  });
78
- if (res.ok) {
79
- const data = await res.json();
80
- const refined = data.aiMessage?.content || '';
81
- if (refined) {
82
- await savePrompt(refined);
83
- }
153
+ const data = await res.json() as { result?: string; error?: string };
154
+ if (!res.ok || data.error) {
155
+ throw new Error(data.error || `HTTP ${res.status}`);
84
156
  }
85
- } catch { /* silent */ }
86
- setRefining(false);
87
- }, [basePath, promptContent, task.title, task.description, savePrompt]);
157
+ const output = (data.result || '').trim();
158
+ if (!output) {
159
+ throw new Error('AI 응답이 비어있습니다');
160
+ }
161
+
162
+ // If user switched to another task during the await, don't clobber it.
163
+ if (launchTaskId !== task.id) {
164
+ setRefineError('다른 태스크로 이동하여 결과를 버렸습니다');
165
+ setTimeout(() => setRefineError(null), 4000);
166
+ return;
167
+ }
168
+
169
+ // Build next doc via string splicing — independent of CM view lifecycle.
170
+ // CM view may have been recreated during the await; positions from the
171
+ // captured selection are still valid against the snapshot doc.
172
+ const safeFrom = Math.min(selFrom, snapshotDoc.length);
173
+ const safeTo = Math.min(selTo, snapshotDoc.length);
174
+ let nextDoc: string;
175
+ let nextCaret: number;
176
+ if (selFrom === selTo) {
177
+ // empty selection → insert at caret with blank-line padding when not at start
178
+ const prefix = safeFrom > 0 ? '\n\n' : '';
179
+ const insert = prefix + output + '\n';
180
+ nextDoc = snapshotDoc.slice(0, safeFrom) + insert + snapshotDoc.slice(safeFrom);
181
+ nextCaret = safeFrom + insert.length;
182
+ } else {
183
+ nextDoc = snapshotDoc.slice(0, safeFrom) + output + snapshotDoc.slice(safeTo);
184
+ nextCaret = safeFrom + output.length;
185
+ }
186
+
187
+ // Update React state first — triggers CM re-sync via value prop
188
+ setDescription(nextDoc);
189
+ onUpdate({ description: nextDoc });
190
+
191
+ // Remember the pre-refine doc so the user can undo a bad suggestion.
192
+ // 30s is enough to read the output and decide.
193
+ setUndoSnapshot({ taskId: launchTaskId, doc: snapshotDoc });
194
+ setTimeout(() => {
195
+ setUndoSnapshot(prev => (prev && prev.taskId === launchTaskId ? null : prev));
196
+ }, 30000);
88
197
 
89
- const handleApplyToPrompt = useCallback(async (content: string) => {
90
- await savePrompt(content);
91
- }, [savePrompt]);
198
+ // Best-effort: move caret inside the live view if still mounted
199
+ const liveView = editorRef.current?.view;
200
+ if (liveView) {
201
+ try {
202
+ const clamped = Math.min(nextCaret, liveView.state.doc.length);
203
+ liveView.dispatch({ selection: { anchor: clamped } });
204
+ } catch { /* selection restore is non-critical */ }
205
+ }
206
+ } catch (err) {
207
+ if ((err as { name?: string })?.name === 'AbortError') {
208
+ // User cancelled — no error message needed.
209
+ } else {
210
+ const msg = err instanceof Error ? err.message : '알 수 없는 오류';
211
+ setRefineError(msg);
212
+ setTimeout(() => setRefineError(null), 6000);
213
+ }
214
+ } finally {
215
+ clearInterval(tick);
216
+ setRefining(false);
217
+ refineAbortRef.current = null;
218
+ }
219
+ }, [basePath, onUpdate, task.id]);
220
+
221
+ const cancelRefine = useCallback(() => {
222
+ refineAbortRef.current?.abort();
223
+ }, []);
224
+
225
+ const undoRefine = useCallback(() => {
226
+ if (!undoSnapshot) return;
227
+ if (undoSnapshot.taskId !== task.id) { setUndoSnapshot(null); return; }
228
+ setDescription(undoSnapshot.doc);
229
+ onUpdate({ description: undoSnapshot.doc });
230
+ setUndoSnapshot(null);
231
+ }, [undoSnapshot, task.id, onUpdate]);
92
232
 
93
233
  const priorities: ItemPriority[] = ['high', 'medium', 'low'];
94
234
 
95
235
  return (
96
236
  <div className="flex flex-col h-full">
97
- {/* Compact header: Title + Status + Actions */}
237
+ {/* Header */}
98
238
  <div className="px-4 py-3 border-b border-border flex-shrink-0 space-y-2">
99
- {/* Title */}
100
239
  {editingTitle ? (
101
240
  <input
102
241
  value={title}
@@ -116,7 +255,6 @@ export default function TaskDetail({
116
255
  </h2>
117
256
  )}
118
257
 
119
- {/* Status + Priority + Today + Prompt + Delete */}
120
258
  <div className="flex items-center gap-3 flex-wrap">
121
259
  <StatusFlow status={task.status} onChange={(status: TaskStatus) => onUpdate({ status })} />
122
260
  <div className="flex items-center gap-1">
@@ -148,14 +286,30 @@ export default function TaskDetail({
148
286
  <span className="text-border">|</span>
149
287
 
150
288
  <button
151
- onClick={() => setShowPromptModal(true)}
289
+ onClick={openPalette}
290
+ title="AI 명령 (⌘K)"
291
+ className="text-xs px-2 py-0.5 rounded transition-colors border border-border
292
+ text-muted-foreground hover:text-foreground hover:border-muted-foreground"
293
+ >
294
+ ⌘K
295
+ </button>
296
+ <button
297
+ onClick={copyAsPrompt}
298
+ title="노트를 Claude Code용으로 클립보드에 복사"
299
+ className="text-xs px-2 py-0.5 rounded transition-colors border border-border
300
+ text-muted-foreground hover:text-foreground hover:border-muted-foreground"
301
+ >
302
+ {copied ? '✓ Copied' : 'Copy as Prompt'}
303
+ </button>
304
+ <button
305
+ onClick={() => setChatOpen(v => !v)}
152
306
  className={`text-xs px-2 py-0.5 rounded transition-colors border ${
153
- promptContent
154
- ? 'bg-accent/15 text-accent border-accent/30 hover:bg-accent/25'
307
+ chatOpen
308
+ ? 'bg-accent/15 text-accent border-accent/30'
155
309
  : 'text-muted-foreground border-border hover:text-foreground hover:border-muted-foreground'
156
310
  }`}
157
311
  >
158
- Prompt{promptContent ? ' *' : ''}
312
+ 💬 Chat
159
313
  </button>
160
314
 
161
315
  <button
@@ -165,60 +319,87 @@ export default function TaskDetail({
165
319
  Delete
166
320
  </button>
167
321
  </div>
168
-
169
- {/* Description - compact */}
170
- <textarea
171
- value={description}
172
- onChange={(e) => setDescription(e.target.value)}
173
- onBlur={saveDescription}
174
- placeholder="Background, conditions, notes..."
175
- className="w-full bg-input border border-border rounded-lg px-3 py-2 text-sm
176
- focus:border-primary focus:outline-none text-foreground resize-y
177
- leading-relaxed min-h-[3.5rem] max-h-[300px]"
178
- rows={2}
179
- />
180
322
  </div>
181
323
 
182
- {/* AI Chat - takes remaining space */}
183
- <div className="flex-1 min-h-0">
184
- <TaskChat
185
- basePath={basePath}
186
- taskStatus={task.status}
187
- onApplyToPrompt={handleApplyToPrompt}
188
- onChatStateChange={onChatStateChange ? (state) => onChatStateChange(task.id, state) : undefined}
189
- />
324
+ {/* Note editor */}
325
+ <div className="flex-1 min-h-0 flex flex-col relative">
326
+ <div className="px-4 pt-2 pb-1 flex items-center justify-between">
327
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Note</span>
328
+ <span className="text-[10px] text-muted-foreground/60">
329
+ Tab 제안 수락 · ⌘K AI 명령 · 자동 저장
330
+ </span>
331
+ </div>
332
+ <div className="flex-1 min-h-0">
333
+ <NoteEditor
334
+ ref={editorRef}
335
+ value={description}
336
+ onChange={setDescription}
337
+ onBlur={saveDescription}
338
+ onOpenCommand={openPalette}
339
+ extraCorpus={extraCorpus}
340
+ placeholder="자유롭게 작성하세요. 배경 · 목표 · 관련 파일 · 결정사항 · 질문 · 링크 등 뭐든..."
341
+ />
342
+ </div>
343
+ {refining && (
344
+ <div className="absolute bottom-2 right-3 text-xs px-2 py-1 rounded bg-muted/90 text-foreground flex items-center gap-2 shadow-lg border border-border">
345
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
346
+ <span>AI 작업 중 {refineElapsed}s (최대 90s)</span>
347
+ <button
348
+ onClick={cancelRefine}
349
+ className="ml-1 text-muted-foreground hover:text-destructive transition-colors"
350
+ title="취소"
351
+ >
352
+ 취소
353
+ </button>
354
+ </div>
355
+ )}
356
+ {!refining && undoSnapshot && undoSnapshot.taskId === task.id && (
357
+ <div className="absolute bottom-2 right-3 text-xs px-2 py-1 rounded bg-accent/15 text-foreground flex items-center gap-2 shadow-lg border border-accent/30">
358
+ <span className="text-accent">✓</span>
359
+ <span>AI 결과 적용됨</span>
360
+ <button
361
+ onClick={undoRefine}
362
+ className="px-1.5 py-0.5 rounded bg-background/60 hover:bg-background text-foreground transition-colors"
363
+ title="되돌리기 (30초 내)"
364
+ >
365
+ ↶ 되돌리기
366
+ </button>
367
+ <button
368
+ onClick={() => setUndoSnapshot(null)}
369
+ className="text-muted-foreground hover:text-foreground"
370
+ title="닫기"
371
+ >
372
+ ×
373
+ </button>
374
+ </div>
375
+ )}
376
+ {refineError && (
377
+ <div className="absolute bottom-2 right-3 text-xs px-3 py-2 rounded bg-destructive/15 text-destructive flex items-center gap-2 shadow-lg border border-destructive/40 max-w-[70%]">
378
+ <span>⚠</span>
379
+ <span className="truncate">AI 실패: {refineError}</span>
380
+ <button onClick={() => setRefineError(null)} className="text-destructive/60 hover:text-destructive">×</button>
381
+ </div>
382
+ )}
190
383
  </div>
191
384
 
192
- {/* Prompt Modal */}
193
- {showPromptModal && (
194
- <div
195
- ref={overlayRef}
196
- onClick={(e) => { if (e.target === overlayRef.current) setShowPromptModal(false); }}
197
- className="fixed inset-0 z-50 flex items-center justify-center"
198
- style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(2px)' }}
199
- >
200
- <div className="bg-card border border-border rounded-xl shadow-2xl shadow-black/40
201
- w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col animate-dialog-in">
202
- <div className="flex items-center justify-between px-5 py-3 border-b border-border">
203
- <h3 className="text-sm font-semibold text-foreground">Prompt</h3>
204
- <button
205
- onClick={() => setShowPromptModal(false)}
206
- className="text-muted-foreground hover:text-foreground transition-colors text-sm"
207
- >
208
- Close
209
- </button>
210
- </div>
211
- <div className="flex-1 overflow-y-auto p-5">
212
- <PromptEditor
213
- content={promptContent}
214
- onSave={savePrompt}
215
- onRefine={handleRefine}
216
- refining={refining}
217
- />
218
- </div>
219
- </div>
385
+ {/* Chat (optional, collapsed by default) */}
386
+ {chatOpen && (
387
+ <div className="h-[38%] min-h-[240px] border-t border-border">
388
+ <TaskChat
389
+ basePath={basePath}
390
+ taskStatus={task.status}
391
+ onInsertToNote={insertIntoNote}
392
+ onChatStateChange={onChatStateChange ? (state) => onChatStateChange(task.id, state) : undefined}
393
+ />
220
394
  </div>
221
395
  )}
396
+
397
+ <CommandPalette
398
+ open={paletteOpen}
399
+ hasSelection={hasSelection}
400
+ onClose={() => setPaletteOpen(false)}
401
+ onRun={runRefine}
402
+ />
222
403
  </div>
223
404
  );
224
405
  }
@@ -125,7 +125,7 @@ export default function TaskList({
125
125
  }
126
126
 
127
127
  function getNextStatus(current: TaskStatus): TaskStatus {
128
- const flow: TaskStatus[] = ['idea', 'writing', 'submitted', 'testing', 'done'];
128
+ const flow: TaskStatus[] = ['idea', 'doing', 'done'];
129
129
  const idx = flow.indexOf(current);
130
130
  if (idx === -1) return 'idea';
131
131
  return flow[(idx + 1) % flow.length];
@@ -336,7 +336,12 @@ export default function WorkspacePanel({
336
336
  useEffect(() => {
337
337
  const handler = (e: KeyboardEvent) => {
338
338
  if (!isActive) return;
339
- const isInput = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement;
339
+ const target = e.target as HTMLElement | null;
340
+ const isInput =
341
+ target instanceof HTMLInputElement ||
342
+ target instanceof HTMLTextAreaElement ||
343
+ (target?.isContentEditable ?? false) ||
344
+ !!target?.closest?.('.cm-editor');
340
345
 
341
346
  if (!isInput && e.code === 'KeyB' && !e.metaKey && !e.ctrlKey) {
342
347
  e.preventDefault();
@@ -356,8 +361,7 @@ export default function WorkspacePanel({
356
361
  }
357
362
  if (selectedTaskId && selectedSubId && !isInput) {
358
363
  const statusMap: Record<string, TaskStatus> = {
359
- 'Digit1': 'idea', 'Digit2': 'writing', 'Digit3': 'submitted',
360
- 'Digit4': 'testing', 'Digit5': 'done', 'Digit6': 'problem',
364
+ 'Digit1': 'idea', 'Digit2': 'doing', 'Digit3': 'done', 'Digit4': 'problem',
361
365
  };
362
366
  if ((e.metaKey || e.ctrlKey) && statusMap[e.code]) {
363
367
  e.preventDefault();
@@ -530,6 +534,7 @@ export default function WorkspacePanel({
530
534
  <div className="flex-1 min-w-0">
531
535
  {selectedTask ? (
532
536
  <TaskDetail task={selectedTask} projectId={id} subProjectId={selectedSubId!}
537
+ siblingTasks={tasks}
533
538
  onUpdate={handleTaskUpdate} onDelete={handleTaskDelete}
534
539
  onChatStateChange={(taskId, state) => {
535
540
  setChatStates(prev => ({ ...prev, [taskId]: state }));
@@ -3,7 +3,7 @@ import type { AgentType } from '../../types';
3
3
  export interface AgentConfig {
4
4
  name: string;
5
5
  binary: string;
6
- buildArgs: (opts: { streaming: boolean }) => string[];
6
+ buildArgs: (opts: { streaming: boolean; model?: string }) => string[];
7
7
  buildEnv: () => NodeJS.ProcessEnv;
8
8
  parseStreamEvent: (parsed: Record<string, unknown>) => { text?: string; final?: string } | null;
9
9
  cleanOutput?: (text: string) => string;
@@ -12,9 +12,9 @@ export interface AgentConfig {
12
12
  const claudeConfig: AgentConfig = {
13
13
  name: 'Claude',
14
14
  binary: 'claude',
15
- buildArgs: ({ streaming }) => [
15
+ buildArgs: ({ streaming, model }) => [
16
16
  '--dangerously-skip-permissions',
17
- '--model', 'opus',
17
+ '--model', model || 'opus',
18
18
  ...(streaming
19
19
  ? ['--output-format', 'stream-json', '--verbose']
20
20
  : ['--output-format', 'text']),