idea-manager 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.next/build-manifest.json +2 -2
  2. package/.next/routes-manifest.json +18 -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/archive/route_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/search/route.js +127 -0
  40. package/.next/server/app/api/search/route_client-reference-manifest.js +1 -0
  41. package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/update/route.js +1 -0
  43. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -0
  44. package/.next/server/app/api/version/route.js +1 -0
  45. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -0
  46. package/.next/server/app/index.html +2 -2
  47. package/.next/server/app/index.rsc +3 -3
  48. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  49. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  50. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  51. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  52. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/page.js +12 -12
  54. package/.next/server/app/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app-paths-manifest.json +10 -7
  57. package/.next/server/pages/404.html +2 -2
  58. package/.next/server/pages/500.html +2 -2
  59. package/.next/static/chunks/app/_global-error/page-e6a77f238d2cdbb9.js +1 -0
  60. package/.next/static/chunks/app/api/archive/route-e6a77f238d2cdbb9.js +1 -0
  61. package/.next/static/chunks/app/api/filesystem/route-e6a77f238d2cdbb9.js +1 -0
  62. package/.next/static/chunks/app/api/filesystem/tree/route-e6a77f238d2cdbb9.js +1 -0
  63. package/.next/static/chunks/app/api/global-memo/route-e6a77f238d2cdbb9.js +1 -0
  64. package/.next/static/chunks/app/api/health/route-e6a77f238d2cdbb9.js +1 -0
  65. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-e6a77f238d2cdbb9.js +1 -0
  66. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-e6a77f238d2cdbb9.js +1 -0
  67. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-e6a77f238d2cdbb9.js +1 -0
  68. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-e6a77f238d2cdbb9.js +1 -0
  69. package/.next/static/chunks/app/api/projects/[id]/route-e6a77f238d2cdbb9.js +1 -0
  70. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-e6a77f238d2cdbb9.js +1 -0
  71. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-e6a77f238d2cdbb9.js +1 -0
  72. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-e6a77f238d2cdbb9.js +1 -0
  73. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-e6a77f238d2cdbb9.js +1 -0
  74. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-e6a77f238d2cdbb9.js +1 -0
  75. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-e6a77f238d2cdbb9.js +1 -0
  76. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-e6a77f238d2cdbb9.js +1 -0
  77. package/.next/static/chunks/app/api/projects/route-e6a77f238d2cdbb9.js +1 -0
  78. package/.next/static/chunks/app/api/search/route-e6a77f238d2cdbb9.js +1 -0
  79. package/.next/static/chunks/app/api/sync/route-e6a77f238d2cdbb9.js +1 -0
  80. package/.next/static/chunks/app/api/update/route-e6a77f238d2cdbb9.js +1 -0
  81. package/.next/static/chunks/app/api/version/route-e6a77f238d2cdbb9.js +1 -0
  82. package/.next/static/chunks/app/page-9a1dc101e82c397c.js +28 -0
  83. package/.next/static/chunks/next/dist/client/components/builtin/app-error-e6a77f238d2cdbb9.js +1 -0
  84. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-e6a77f238d2cdbb9.js +1 -0
  85. package/.next/static/chunks/next/dist/client/components/builtin/not-found-e6a77f238d2cdbb9.js +1 -0
  86. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-e6a77f238d2cdbb9.js +1 -0
  87. package/.next/static/css/eab748b03f49c43a.css +3 -0
  88. package/.next/static/mxrEVQX3r5YlDPZgpDvSp/_buildManifest.js +1 -0
  89. package/package.json +1 -1
  90. package/src/app/api/search/route.ts +149 -0
  91. package/src/app/api/update/route.ts +52 -0
  92. package/src/app/api/version/route.ts +68 -0
  93. package/src/components/search/GlobalSearch.tsx +156 -0
  94. package/src/components/search/QuickCapture.tsx +208 -0
  95. package/src/components/tabs/TabBar.tsx +2 -0
  96. package/src/components/tabs/TabShell.tsx +4 -0
  97. package/src/components/task/CommandPalette.tsx +48 -2
  98. package/src/components/task/NoteEditor.tsx +16 -1
  99. package/src/components/task/TaskChat.tsx +31 -20
  100. package/src/components/task/TaskDetail.tsx +62 -2
  101. package/src/components/update/UpdateButton.tsx +190 -0
  102. package/src/components/workspace/WorkspacePanel.tsx +1 -0
  103. package/.next/static/chunks/app/_global-error/page-6ec0e723e471f87a.js +0 -1
  104. package/.next/static/chunks/app/api/archive/route-6ec0e723e471f87a.js +0 -1
  105. package/.next/static/chunks/app/api/filesystem/route-6ec0e723e471f87a.js +0 -1
  106. package/.next/static/chunks/app/api/filesystem/tree/route-6ec0e723e471f87a.js +0 -1
  107. package/.next/static/chunks/app/api/global-memo/route-6ec0e723e471f87a.js +0 -1
  108. package/.next/static/chunks/app/api/health/route-6ec0e723e471f87a.js +0 -1
  109. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-6ec0e723e471f87a.js +0 -1
  110. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-6ec0e723e471f87a.js +0 -1
  111. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-6ec0e723e471f87a.js +0 -1
  112. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-6ec0e723e471f87a.js +0 -1
  113. package/.next/static/chunks/app/api/projects/[id]/route-6ec0e723e471f87a.js +0 -1
  114. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-6ec0e723e471f87a.js +0 -1
  115. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-6ec0e723e471f87a.js +0 -1
  116. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-6ec0e723e471f87a.js +0 -1
  117. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-6ec0e723e471f87a.js +0 -1
  118. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-6ec0e723e471f87a.js +0 -1
  119. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-6ec0e723e471f87a.js +0 -1
  120. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-6ec0e723e471f87a.js +0 -1
  121. package/.next/static/chunks/app/api/projects/route-6ec0e723e471f87a.js +0 -1
  122. package/.next/static/chunks/app/api/sync/route-6ec0e723e471f87a.js +0 -1
  123. package/.next/static/chunks/app/page-6a511af64da7531f.js +0 -28
  124. package/.next/static/chunks/next/dist/client/components/builtin/app-error-6ec0e723e471f87a.js +0 -1
  125. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-6ec0e723e471f87a.js +0 -1
  126. package/.next/static/chunks/next/dist/client/components/builtin/not-found-6ec0e723e471f87a.js +0 -1
  127. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-6ec0e723e471f87a.js +0 -1
  128. package/.next/static/css/cc32379d0efa7d1d.css +0 -3
  129. package/.next/static/eQXRVHrJt1cKjgp4hKYm8/_buildManifest.js +0 -1
  130. /package/.next/static/{eQXRVHrJt1cKjgp4hKYm8 → mxrEVQX3r5YlDPZgpDvSp}/_ssgManifest.js +0 -0
@@ -19,6 +19,28 @@ const COMMANDS: { key: RefineCommand; label: string; hint: string }[] = [
19
19
  { key: 'custom', label: '직접 입력…', hint: '임의 명령을 프롬프트로 전달' },
20
20
  ];
21
21
 
22
+ const HISTORY_KEY = 'im-refine-custom-history';
23
+ const HISTORY_MAX = 8;
24
+
25
+ function loadHistory(): string[] {
26
+ if (typeof window === 'undefined') return [];
27
+ try {
28
+ const raw = localStorage.getItem(HISTORY_KEY);
29
+ if (!raw) return [];
30
+ const arr = JSON.parse(raw);
31
+ return Array.isArray(arr) ? arr.filter((v): v is string => typeof v === 'string') : [];
32
+ } catch { return []; }
33
+ }
34
+
35
+ function pushHistory(entry: string) {
36
+ if (typeof window === 'undefined') return;
37
+ const trimmed = entry.trim();
38
+ if (!trimmed) return;
39
+ const prev = loadHistory().filter(x => x !== trimmed);
40
+ const next = [trimmed, ...prev].slice(0, HISTORY_MAX);
41
+ try { localStorage.setItem(HISTORY_KEY, JSON.stringify(next)); } catch { /* quota */ }
42
+ }
43
+
22
44
  export default function CommandPalette({
23
45
  open,
24
46
  hasSelection,
@@ -33,6 +55,7 @@ export default function CommandPalette({
33
55
  const [idx, setIdx] = useState(0);
34
56
  const [customMode, setCustomMode] = useState(false);
35
57
  const [custom, setCustom] = useState('');
58
+ const [history, setHistory] = useState<string[]>([]);
36
59
  const inputRef = useRef<HTMLInputElement>(null);
37
60
 
38
61
  useEffect(() => {
@@ -44,7 +67,10 @@ export default function CommandPalette({
44
67
  }, [open]);
45
68
 
46
69
  useEffect(() => {
47
- if (customMode) inputRef.current?.focus();
70
+ if (customMode) {
71
+ inputRef.current?.focus();
72
+ setHistory(loadHistory());
73
+ }
48
74
  }, [customMode]);
49
75
 
50
76
  if (!open) return null;
@@ -61,6 +87,7 @@ export default function CommandPalette({
61
87
  const submitCustom = () => {
62
88
  const t = custom.trim();
63
89
  if (!t) return;
90
+ pushHistory(t);
64
91
  onRun('custom', t);
65
92
  };
66
93
 
@@ -114,11 +141,30 @@ export default function CommandPalette({
114
141
  onChange={(e) => setCustom(e.target.value)}
115
142
  onKeyDown={(e) => {
116
143
  if (e.key === 'Enter') submitCustom();
117
- if (e.key === 'Escape') onClose();
144
+ if (e.key === 'Escape') { e.preventDefault(); setCustomMode(false); }
118
145
  }}
119
146
  placeholder="예: 이 부분 markdown 표로 만들어줘"
120
147
  className="w-full bg-input border border-border rounded-md px-3 py-2 text-sm focus:border-primary focus:outline-none"
121
148
  />
149
+ {history.length > 0 && (
150
+ <div className="flex flex-col gap-1.5">
151
+ <div className="text-[10px] uppercase tracking-wider text-muted-foreground/70">최근 명령</div>
152
+ <div className="flex flex-wrap gap-1.5">
153
+ {history.map((h) => (
154
+ <button
155
+ key={h}
156
+ onClick={() => setCustom(h)}
157
+ title={h}
158
+ className="text-xs px-2 py-1 rounded border border-border text-muted-foreground
159
+ hover:text-foreground hover:border-muted-foreground transition-colors
160
+ max-w-[220px] truncate text-left"
161
+ >
162
+ {h}
163
+ </button>
164
+ ))}
165
+ </div>
166
+ </div>
167
+ )}
122
168
  <div className="flex justify-end gap-2">
123
169
  <button onClick={() => setCustomMode(false)} className="text-xs text-muted-foreground px-2 py-1">뒤로</button>
124
170
  <button
@@ -285,18 +285,32 @@ const mdHighlight = HighlightStyle.define([
285
285
  // ─────────────────────────────────────────────────────────────
286
286
  // Component
287
287
  // ─────────────────────────────────────────────────────────────
288
+ // Extract the current bullet/checkbox line text suitable for promoting to a task.
289
+ // Returns the "content" portion (without the `- [ ]` marker) and the line range.
290
+ export function getPromotableLine(view: EditorView): { content: string; from: number; to: number } | null {
291
+ const state = view.state;
292
+ const pos = state.selection.main.head;
293
+ const line = state.doc.lineAt(pos);
294
+ const m = line.text.match(/^(\s*)([-*+])\s(?:\[[ xX]\]\s)?(.*)$/);
295
+ if (!m) return null;
296
+ const content = m[3]?.trim();
297
+ if (!content) return null;
298
+ return { content, from: line.from, to: line.to };
299
+ }
300
+
288
301
  export interface NoteEditorProps {
289
302
  value: string;
290
303
  onChange: (v: string) => void;
291
304
  onBlur?: () => void;
292
305
  onOpenCommand?: () => void;
306
+ onPromoteLine?: () => void;
293
307
  placeholder?: string;
294
308
  /** Extra text blobs (sibling tasks, brainstorm, …) to widen the autocomplete corpus. */
295
309
  extraCorpus?: string[];
296
310
  }
297
311
 
298
312
  const NoteEditor = forwardRef<ReactCodeMirrorRef, NoteEditorProps>(function NoteEditor(
299
- { value, onChange, onBlur, onOpenCommand, placeholder, extraCorpus },
313
+ { value, onChange, onBlur, onOpenCommand, onPromoteLine, placeholder, extraCorpus },
300
314
  ref,
301
315
  ) {
302
316
  // Mutable ref keeps the plugin in sync with the latest corpus without
@@ -315,6 +329,7 @@ const NoteEditor = forwardRef<ReactCodeMirrorRef, NoteEditorProps>(function Note
315
329
  { key: 'Escape', run: dismissGhost },
316
330
  { key: 'Enter', run: continueList },
317
331
  { key: 'Mod-k', run: () => { onOpenCommand?.(); return true; } },
332
+ { key: 'Mod-Shift-t', run: () => { onPromoteLine?.(); return true; } },
318
333
  ])),
319
334
  EditorView.lineWrapping,
320
335
  EditorView.theme({
@@ -149,27 +149,38 @@ export default function TaskChat({
149
149
  노트 작성을 도와드립니다. 질문하거나 &quot;이 부분 정리해줘&quot; 같이 요청해보세요
150
150
  </div>
151
151
  )}
152
- {messages.filter(msg => msg.content).map((msg) => (
153
- <div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
154
- <div className={`max-w-[90%] px-3 py-2 rounded-lg text-sm leading-relaxed ${
155
- msg.role === 'user'
156
- ? 'bg-accent text-white rounded-br-sm whitespace-pre-wrap'
157
- : 'bg-muted text-foreground rounded-bl-sm chat-markdown'
158
- }`}>
159
- {msg.role === 'assistant'
160
- ? <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
161
- : msg.content}
152
+ {messages.filter(msg => msg.content).map((msg) => {
153
+ const isProgress = msg.role === 'assistant' && msg.content.startsWith('[진행 중]');
154
+ return (
155
+ <div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
156
+ {isProgress && (
157
+ <div className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-warning mb-0.5 pl-1">
158
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
159
+ Watch 실행 중 · 실시간 출력
160
+ </div>
161
+ )}
162
+ <div className={`max-w-[90%] px-3 py-2 rounded-lg text-sm leading-relaxed ${
163
+ msg.role === 'user'
164
+ ? 'bg-accent text-white rounded-br-sm whitespace-pre-wrap'
165
+ : isProgress
166
+ ? 'bg-warning/10 text-foreground rounded-bl-sm chat-markdown border border-warning/30'
167
+ : 'bg-muted text-foreground rounded-bl-sm chat-markdown'
168
+ }`}>
169
+ {msg.role === 'assistant'
170
+ ? <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
171
+ : msg.content}
172
+ </div>
173
+ {msg.role === 'assistant' && !isProgress && (
174
+ <button
175
+ onClick={() => onInsertToNote(msg.content)}
176
+ className="text-xs text-muted-foreground hover:text-primary mt-0.5 px-1 transition-colors"
177
+ >
178
+ ↓ 노트에 삽입
179
+ </button>
180
+ )}
162
181
  </div>
163
- {msg.role === 'assistant' && (
164
- <button
165
- onClick={() => onInsertToNote(msg.content)}
166
- className="text-xs text-muted-foreground hover:text-primary mt-0.5 px-1 transition-colors"
167
- >
168
- ↓ 노트에 삽입
169
- </button>
170
- )}
171
- </div>
172
- ))}
182
+ );
183
+ })}
173
184
  {loading && (
174
185
  <div className="flex gap-1 px-2 py-2">
175
186
  <div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce" style={{ animationDelay: '0ms' }} />
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
4
4
  import type { ITask, TaskStatus, ItemPriority } from '@/types';
5
5
  import StatusFlow from './StatusFlow';
6
6
  import TaskChat from './TaskChat';
7
- import NoteEditor from './NoteEditor';
7
+ import NoteEditor, { getPromotableLine } from './NoteEditor';
8
8
  import CommandPalette, { type RefineCommand } from './CommandPalette';
9
9
  import type { ReactCodeMirrorRef } from '@uiw/react-codemirror';
10
10
 
@@ -15,6 +15,7 @@ export default function TaskDetail({
15
15
  siblingTasks,
16
16
  onUpdate,
17
17
  onDelete,
18
+ onTaskPromoted,
18
19
  onChatStateChange,
19
20
  }: {
20
21
  task: ITask;
@@ -24,6 +25,8 @@ export default function TaskDetail({
24
25
  siblingTasks?: ITask[];
25
26
  onUpdate: (data: Partial<ITask>) => void;
26
27
  onDelete: () => void;
28
+ /** Fired after a checkbox line is promoted to a new task. Parent should refresh its task list. */
29
+ onTaskPromoted?: (newTask: ITask) => void;
27
30
  onChatStateChange?: (taskId: string, state: 'idle' | 'loading' | 'done') => void;
28
31
  }) {
29
32
  const [title, setTitle] = useState(task.title);
@@ -31,6 +34,16 @@ export default function TaskDetail({
31
34
  const [editingTitle, setEditingTitle] = useState(false);
32
35
  const [copied, setCopied] = useState(false);
33
36
  const [chatOpen, setChatOpen] = useState(false);
37
+ const chatWasManuallyToggled = useRef(false);
38
+
39
+ // Auto-open the chat panel while the task is being executed by the watcher —
40
+ // that's where streaming progress shows up. Don't override a manual toggle
41
+ // the user made in this session.
42
+ useEffect(() => {
43
+ if (task.status === 'testing' && !chatOpen && !chatWasManuallyToggled.current) {
44
+ setChatOpen(true);
45
+ }
46
+ }, [task.status, chatOpen]);
34
47
  const [paletteOpen, setPaletteOpen] = useState(false);
35
48
  const [refining, setRefining] = useState(false);
36
49
  const [refineElapsed, setRefineElapsed] = useState(0);
@@ -230,6 +243,46 @@ export default function TaskDetail({
230
243
  setUndoSnapshot(null);
231
244
  }, [undoSnapshot, task.id, onUpdate]);
232
245
 
246
+ const [promoteNotice, setPromoteNotice] = useState<string | null>(null);
247
+
248
+ const promoteCheckbox = useCallback(async () => {
249
+ const view = editorRef.current?.view;
250
+ if (!view) return;
251
+ const line = getPromotableLine(view);
252
+ if (!line) {
253
+ setRefineError('체크박스나 불릿 목록 줄에 커서를 두고 실행하세요');
254
+ setTimeout(() => setRefineError(null), 3000);
255
+ return;
256
+ }
257
+ const titleText = line.content.slice(0, 200);
258
+ try {
259
+ const res = await fetch(`/api/projects/${projectId}/sub-projects/${subProjectId}/tasks`, {
260
+ method: 'POST',
261
+ headers: { 'Content-Type': 'application/json' },
262
+ body: JSON.stringify({ title: titleText }),
263
+ });
264
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
265
+ const newTask = await res.json() as ITask;
266
+
267
+ // Remove the promoted line from the note (and a trailing newline, if any).
268
+ const doc = view.state.doc.toString();
269
+ const before = doc.slice(0, line.from);
270
+ const afterRaw = doc.slice(line.to);
271
+ const trimmed = afterRaw.startsWith('\n') ? afterRaw.slice(1) : afterRaw;
272
+ const nextDoc = before + trimmed;
273
+ setDescription(nextDoc);
274
+ onUpdate({ description: nextDoc });
275
+ onTaskPromoted?.(newTask);
276
+
277
+ setPromoteNotice(`→ 태스크 "${titleText}" 생성됨`);
278
+ setTimeout(() => setPromoteNotice(null), 3000);
279
+ } catch (err) {
280
+ const msg = err instanceof Error ? err.message : '태스크 생성 실패';
281
+ setRefineError(msg);
282
+ setTimeout(() => setRefineError(null), 4000);
283
+ }
284
+ }, [projectId, subProjectId, onUpdate, onTaskPromoted]);
285
+
233
286
  const priorities: ItemPriority[] = ['high', 'medium', 'low'];
234
287
 
235
288
  return (
@@ -302,7 +355,7 @@ export default function TaskDetail({
302
355
  {copied ? '✓ Copied' : 'Copy as Prompt'}
303
356
  </button>
304
357
  <button
305
- onClick={() => setChatOpen(v => !v)}
358
+ onClick={() => { chatWasManuallyToggled.current = true; setChatOpen(v => !v); }}
306
359
  className={`text-xs px-2 py-0.5 rounded transition-colors border ${
307
360
  chatOpen
308
361
  ? 'bg-accent/15 text-accent border-accent/30'
@@ -336,6 +389,7 @@ export default function TaskDetail({
336
389
  onChange={setDescription}
337
390
  onBlur={saveDescription}
338
391
  onOpenCommand={openPalette}
392
+ onPromoteLine={promoteCheckbox}
339
393
  extraCorpus={extraCorpus}
340
394
  placeholder="자유롭게 작성하세요. 배경 · 목표 · 관련 파일 · 결정사항 · 질문 · 링크 등 뭐든..."
341
395
  />
@@ -353,6 +407,12 @@ export default function TaskDetail({
353
407
  </button>
354
408
  </div>
355
409
  )}
410
+ {promoteNotice && (
411
+ <div className="absolute bottom-2 right-3 text-xs px-3 py-1.5 rounded bg-success/15 text-success flex items-center gap-2 shadow-lg border border-success/30">
412
+ <span>✓</span>
413
+ <span className="truncate max-w-[50ch]">{promoteNotice}</span>
414
+ </div>
415
+ )}
356
416
  {!refining && undoSnapshot && undoSnapshot.taskId === task.id && (
357
417
  <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
418
  <span className="text-accent">✓</span>
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState, useCallback } from 'react';
4
+
5
+ interface VersionInfo {
6
+ current: string;
7
+ latest: string | null;
8
+ updateAvailable: boolean;
9
+ }
10
+
11
+ interface UpdateResult {
12
+ ok: boolean;
13
+ code?: number | null;
14
+ signal?: string | null;
15
+ stdout?: string;
16
+ stderr?: string;
17
+ error?: string;
18
+ durationMs?: number;
19
+ }
20
+
21
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // re-check hourly
22
+ const DISMISS_KEY = 'im-update-dismissed';
23
+
24
+ export default function UpdateButton() {
25
+ const [info, setInfo] = useState<VersionInfo | null>(null);
26
+ const [modalOpen, setModalOpen] = useState(false);
27
+ const [installing, setInstalling] = useState(false);
28
+ const [installed, setInstalled] = useState(false);
29
+ const [result, setResult] = useState<UpdateResult | null>(null);
30
+ const dismissedRef = useRef<string | null>(null);
31
+
32
+ useEffect(() => {
33
+ dismissedRef.current = typeof window !== 'undefined' ? localStorage.getItem(DISMISS_KEY) : null;
34
+ }, []);
35
+
36
+ const fetchVersion = useCallback(async () => {
37
+ try {
38
+ const res = await fetch('/api/version');
39
+ if (!res.ok) return;
40
+ const data = await res.json() as VersionInfo;
41
+ setInfo(data);
42
+ } catch { /* offline or cold — ignore */ }
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ fetchVersion();
47
+ const id = setInterval(fetchVersion, CHECK_INTERVAL_MS);
48
+ return () => clearInterval(id);
49
+ }, [fetchVersion]);
50
+
51
+ const isDismissed = info?.latest !== null && info?.latest === dismissedRef.current;
52
+ const showBadge = !!info?.updateAvailable && !isDismissed;
53
+
54
+ const install = useCallback(async () => {
55
+ setInstalling(true);
56
+ setResult(null);
57
+ try {
58
+ const res = await fetch('/api/update', { method: 'POST' });
59
+ const data = await res.json() as UpdateResult;
60
+ setResult(data);
61
+ if (data.ok) {
62
+ setInstalled(true);
63
+ await fetchVersion();
64
+ }
65
+ } catch (e) {
66
+ setResult({ ok: false, error: e instanceof Error ? e.message : 'Network error' });
67
+ } finally {
68
+ setInstalling(false);
69
+ }
70
+ }, [fetchVersion]);
71
+
72
+ const dismiss = useCallback(() => {
73
+ if (info?.latest) {
74
+ try { localStorage.setItem(DISMISS_KEY, info.latest); } catch { /* quota */ }
75
+ dismissedRef.current = info.latest;
76
+ }
77
+ setInfo(prev => prev ? { ...prev, updateAvailable: false } : prev);
78
+ }, [info?.latest]);
79
+
80
+ return (
81
+ <>
82
+ {showBadge ? (
83
+ <button
84
+ onClick={() => setModalOpen(true)}
85
+ title={`IM v${info?.latest} 업데이트 가능 (현재 ${info?.current})`}
86
+ className="text-xs px-2 py-1 rounded-md border border-success/40 bg-success/15 text-success
87
+ hover:bg-success/25 transition-colors flex items-center gap-1.5 mr-2"
88
+ >
89
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-success animate-pulse" />
90
+ v{info?.latest} 업데이트
91
+ </button>
92
+ ) : info ? (
93
+ <button
94
+ onClick={() => setModalOpen(true)}
95
+ title={`현재 IM v${info.current} · 업데이트 확인`}
96
+ className="text-[10px] px-1.5 py-0.5 rounded text-muted-foreground/60 hover:text-muted-foreground transition-colors mr-2"
97
+ >
98
+ v{info.current}
99
+ </button>
100
+ ) : null}
101
+
102
+ {modalOpen && (
103
+ <div
104
+ onClick={() => !installing && setModalOpen(false)}
105
+ className="fixed inset-0 z-[60] flex items-center justify-center"
106
+ style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(3px)' }}
107
+ >
108
+ <div
109
+ onClick={(e) => e.stopPropagation()}
110
+ className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-md animate-dialog-in p-5 flex flex-col gap-3"
111
+ >
112
+ <div className="flex items-start justify-between">
113
+ <div>
114
+ <div className="text-sm font-semibold text-foreground">IM 업데이트</div>
115
+ <div className="text-xs text-muted-foreground mt-0.5">
116
+ 현재 <span className="text-foreground font-mono">v{info?.current}</span>
117
+ {info?.latest && (
118
+ <>
119
+ {' → '}
120
+ <span className={`font-mono ${info.updateAvailable ? 'text-success' : 'text-foreground'}`}>
121
+ v{info.latest}
122
+ </span>
123
+ </>
124
+ )}
125
+ </div>
126
+ </div>
127
+ {!installing && (
128
+ <button onClick={() => setModalOpen(false)} className="text-muted-foreground hover:text-foreground text-lg leading-none">×</button>
129
+ )}
130
+ </div>
131
+
132
+ {installed ? (
133
+ <div className="flex flex-col gap-2">
134
+ <div className="text-sm text-success flex items-center gap-2">
135
+ <span>✓</span>
136
+ <span>설치 완료</span>
137
+ {result?.durationMs && (
138
+ <span className="text-xs text-muted-foreground">({Math.round(result.durationMs / 1000)}s)</span>
139
+ )}
140
+ </div>
141
+ <div className="text-xs text-muted-foreground leading-relaxed">
142
+ 새 버전을 반영하려면 <span className="font-mono text-foreground">im start</span> 프로세스를 재시작하세요.
143
+ PM2로 실행 중이면 <span className="font-mono text-foreground">pm2 restart idea-manager</span>로 즉시 반영됩니다.
144
+ </div>
145
+ </div>
146
+ ) : installing ? (
147
+ <div className="flex items-center gap-2 text-sm text-foreground">
148
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
149
+ <span>설치 중… (최대 3분)</span>
150
+ </div>
151
+ ) : info?.updateAvailable ? (
152
+ <>
153
+ <div className="text-xs text-muted-foreground leading-relaxed">
154
+ <span className="font-mono">npm install -g idea-manager@latest</span>를 실행해 최신 버전을 설치합니다.
155
+ 설치가 끝나면 재시작 안내가 표시됩니다.
156
+ </div>
157
+ <div className="flex justify-end gap-2 mt-1">
158
+ <button onClick={dismiss} className="text-xs text-muted-foreground px-2 py-1 hover:text-foreground transition-colors">
159
+ 이 버전 건너뜀
160
+ </button>
161
+ <button
162
+ onClick={install}
163
+ className="text-xs px-3 py-1.5 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
164
+ >
165
+ 지금 설치
166
+ </button>
167
+ </div>
168
+ </>
169
+ ) : (
170
+ <div className="text-xs text-muted-foreground">
171
+ 최신 버전을 사용 중입니다.
172
+ </div>
173
+ )}
174
+
175
+ {result && !result.ok && (
176
+ <div className="mt-2 flex flex-col gap-1.5">
177
+ <div className="text-xs text-destructive">⚠ 설치 실패{result.code !== undefined && result.code !== null ? ` (exit ${result.code})` : ''}</div>
178
+ {(result.stderr || result.error) && (
179
+ <pre className="text-[10px] bg-muted/50 border border-border rounded p-2 max-h-40 overflow-auto whitespace-pre-wrap break-words text-muted-foreground">
180
+ {result.stderr || result.error}
181
+ </pre>
182
+ )}
183
+ </div>
184
+ )}
185
+ </div>
186
+ </div>
187
+ )}
188
+ </>
189
+ );
190
+ }
@@ -536,6 +536,7 @@ export default function WorkspacePanel({
536
536
  <TaskDetail task={selectedTask} projectId={id} subProjectId={selectedSubId!}
537
537
  siblingTasks={tasks}
538
538
  onUpdate={handleTaskUpdate} onDelete={handleTaskDelete}
539
+ onTaskPromoted={(newTask) => setTasks(prev => [...prev, newTask])}
539
540
  onChatStateChange={(taskId, state) => {
540
541
  setChatStates(prev => ({ ...prev, [taskId]: state }));
541
542
  }} />
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,212,363,460,514,595,711,770,772,819,822,851,860,896,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);