idea-manager 1.6.0 → 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 (134) 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/README.ja.md +4 -1
  90. package/README.ko.md +36 -6
  91. package/README.md +31 -6
  92. package/README.zh.md +4 -1
  93. package/package.json +1 -1
  94. package/src/app/api/search/route.ts +149 -0
  95. package/src/app/api/update/route.ts +52 -0
  96. package/src/app/api/version/route.ts +68 -0
  97. package/src/components/search/GlobalSearch.tsx +156 -0
  98. package/src/components/search/QuickCapture.tsx +208 -0
  99. package/src/components/tabs/TabBar.tsx +2 -0
  100. package/src/components/tabs/TabShell.tsx +4 -0
  101. package/src/components/task/CommandPalette.tsx +48 -2
  102. package/src/components/task/NoteEditor.tsx +16 -1
  103. package/src/components/task/TaskChat.tsx +31 -20
  104. package/src/components/task/TaskDetail.tsx +62 -2
  105. package/src/components/update/UpdateButton.tsx +190 -0
  106. package/src/components/workspace/WorkspacePanel.tsx +1 -0
  107. package/.next/static/63zinfEtSLCdG9nUZ3W-E/_buildManifest.js +0 -1
  108. package/.next/static/chunks/app/_global-error/page-6ec0e723e471f87a.js +0 -1
  109. package/.next/static/chunks/app/api/archive/route-6ec0e723e471f87a.js +0 -1
  110. package/.next/static/chunks/app/api/filesystem/route-6ec0e723e471f87a.js +0 -1
  111. package/.next/static/chunks/app/api/filesystem/tree/route-6ec0e723e471f87a.js +0 -1
  112. package/.next/static/chunks/app/api/global-memo/route-6ec0e723e471f87a.js +0 -1
  113. package/.next/static/chunks/app/api/health/route-6ec0e723e471f87a.js +0 -1
  114. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-6ec0e723e471f87a.js +0 -1
  115. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-6ec0e723e471f87a.js +0 -1
  116. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-6ec0e723e471f87a.js +0 -1
  117. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-6ec0e723e471f87a.js +0 -1
  118. package/.next/static/chunks/app/api/projects/[id]/route-6ec0e723e471f87a.js +0 -1
  119. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-6ec0e723e471f87a.js +0 -1
  120. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-6ec0e723e471f87a.js +0 -1
  121. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-6ec0e723e471f87a.js +0 -1
  122. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-6ec0e723e471f87a.js +0 -1
  123. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-6ec0e723e471f87a.js +0 -1
  124. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-6ec0e723e471f87a.js +0 -1
  125. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-6ec0e723e471f87a.js +0 -1
  126. package/.next/static/chunks/app/api/projects/route-6ec0e723e471f87a.js +0 -1
  127. package/.next/static/chunks/app/api/sync/route-6ec0e723e471f87a.js +0 -1
  128. package/.next/static/chunks/app/page-6a511af64da7531f.js +0 -28
  129. package/.next/static/chunks/next/dist/client/components/builtin/app-error-6ec0e723e471f87a.js +0 -1
  130. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-6ec0e723e471f87a.js +0 -1
  131. package/.next/static/chunks/next/dist/client/components/builtin/not-found-6ec0e723e471f87a.js +0 -1
  132. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-6ec0e723e471f87a.js +0 -1
  133. package/.next/static/css/cc32379d0efa7d1d.css +0 -3
  134. /package/.next/static/{63zinfEtSLCdG9nUZ3W-E → mxrEVQX3r5YlDPZgpDvSp}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -39,10 +39,12 @@ Workspace
39
39
  ### Task Status Flow
40
40
 
41
41
  ```
42
- 💡 Idea → ✏️ Writing🚀 Submitted → 🧪 Testing → ✅ Done
43
- 🔴 Problem
42
+ 💡 Idea → 🔥 Doing → ✅ Done
43
+ 🔴 Problem
44
44
  ```
45
45
 
46
+ Legacy statuses (`Writing`, `Submitted`, `Testing`) remain available and are shown as a dashed badge on pre-v1.6 tasks.
47
+
46
48
  ## CLI Commands
47
49
 
48
50
  | Command | Description |
@@ -130,6 +132,19 @@ im watch --project <id> # Specific project
130
132
  im watch --interval 30 --dry-run # Preview mode
131
133
  ```
132
134
 
135
+ ### Note-Centric Editor (v1.6)
136
+
137
+ The task detail replaces the old separate "description + prompt" pair with a single rich Markdown note.
138
+
139
+ - **CodeMirror Editor** — Markdown syntax highlighting with distinct styling for headings, list markers (`-`, `1.`), code, links, and quotes. GFM enabled (task checkboxes, strikethrough, tables).
140
+ - **⌘K AI Command Palette** — Refine the selection or continue at cursor without leaving the note:
141
+ - 이어서 써줘 (continue) · 이 부분 정리해줘 (tidy) · 할 일로 쪼개줘 (split into tasks) · 질문으로 바꿔줘 (to questions) · 요약해줘 (summarize) · Custom prompt
142
+ - Result is inserted inline. **Cancel** mid-run; **Undo** within 30s of applying.
143
+ - Runs on Sonnet without project context for ~7s typical latency.
144
+ - **Context-Aware Autocomplete** — Ghost text suggests multi-word phrases (up to 3 tokens). Corpus pulls from the current note, sibling tasks in the same project, and the project's brainstorm. Phrases sharing vocabulary with the current note are boosted, so related terms surface first. `Tab` accepts, `Esc` dismisses.
145
+ - **List Auto-Continue** — Enter continues bullets/numbers/checkboxes; Enter on an empty item exits the list.
146
+ - **Copy as Prompt** — One-click copy of the whole note formatted for pasting into Claude Code / another agent.
147
+
133
148
  ### Workspace
134
149
 
135
150
  - **3-Panel Layout** — Brainstorming | Project Tree | Task Detail (drag to resize)
@@ -137,12 +152,11 @@ im watch --interval 30 --dry-run # Preview mode
137
152
  - **File Tree Drawer** — Browse linked project directories
138
153
  - **Brainstorming Panel** — Free-form notes with inline AI memos
139
154
  - **Auto Distribute** — AI analyzes brainstorming and distributes tasks to sub-projects with preview/edit modal
140
- - **Prompt Editor** — Write/edit/copy prompts per task
141
- - **AI Chat** — Per-task conversations to refine work, with loading/done indicators in project tree
155
+ - **Note Assistant** — Per-task AI chat (formerly "AI Chat") for refining the note, with one-click insert into the note
142
156
  - **Quick Memo** — Global scratchpad on dashboard for free-form notes (auto-saved)
143
157
  - **Morning Notifications** — Daily macOS notification at 9 AM with today's tasks summary
144
- - **Dashboard** — Active / All / Today views
145
- - **Keyboard Shortcuts** — `B` brainstorm, `N` sub-project, `T` task, `Cmd+1~6` status
158
+ - **Dashboard** — Active / All / Today / Archive views
159
+ - **Keyboard Shortcuts** — `B` brainstorm, `N` project, `T` task, `⌘K` AI command palette, `⌘1/2/3/4` status (Idea/Doing/Done/Problem)
146
160
 
147
161
  ### Data
148
162
 
@@ -190,6 +204,17 @@ netstat -ano | findstr :3456 # Windows (then taskkill /PID <pid> /F)
190
204
 
191
205
  ## Changelog
192
206
 
207
+ ### v1.6.0
208
+
209
+ - **⌘K AI Command Palette** — Inline refine/continue/summarize/split commands, result inserted at cursor. Cancel + 30s Undo. Runs on Sonnet with lean context (~7s vs 90s previously).
210
+ - **CodeMirror Note Editor** — Replaces textarea with a full Markdown editor: syntax highlighting, GFM task lists / strikethrough / tables, list auto-continue, ghost-text autocomplete.
211
+ - **Context-Aware Autocomplete** — Multi-word phrase suggestions drawn from the current note + sibling tasks + brainstorm. Shared-vocabulary boost surfaces topically related completions first.
212
+ - **`doing` status** — Simplified default flow (Idea → Doing → Done). Legacy statuses preserved with a dashed badge.
213
+ - **Task archive & tags** — `is_archived` and `tags` columns with Archive dashboard tab.
214
+ - **Legacy prompt merge** — Existing `task_prompts` are one-time merged into the note description with a `<!-- legacy-prompt -->` marker.
215
+ - **Note Assistant** — Per-task AI chat repositioned around note refinement, with one-click insert.
216
+ - Runtime: `RunAgentOptions.model` override, CodeMirror-aware global-shortcut filter.
217
+
193
218
  ### v1.3.0
194
219
 
195
220
  - **Task Archive** — Delete → Archive/Delete choice; archived tasks preserved with prompts and conversations
package/README.zh.md CHANGED
@@ -34,7 +34,10 @@ im start
34
34
  - **三面板布局** — 头脑风暴 | 项目树 | 任务详情
35
35
  - **标签导航** — 同时打开多个项目
36
36
  - **文件树** — 浏览关联的项目目录
37
- - **AI聊天**按任务对话改进工作
37
+ - **CodeMirror笔记编辑器**Markdown语法高亮、GFM、列表自动延续(v1.6+)
38
+ - **⌘K AI命令面板** — 在笔记中对选区进行整理 / 续写 / 总结 / 任务拆分,结果直接插入光标位置(v1.6+)
39
+ - **上下文感知自动完成** — 从当前笔记、兄弟任务与头脑风暴中建议多词短语(v1.6+)
40
+ - **Note Assistant** — 按任务对话改进工作,一键插入笔记
38
41
  - **本地优先** — SQLite (sql.js),无原生依赖
39
42
 
40
43
  ## 要求
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idea-manager",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Turn free-form brainstorming into structured task trees with AI-generated prompts. Built-in MCP Server for autonomous AI agent execution. Local-first with SQLite, cross-PC sync via Git.",
5
5
  "keywords": [
6
6
  "brainstorm",
@@ -0,0 +1,149 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDb } from '@/lib/db';
3
+ import { ensureDb } from '@/lib/db';
4
+
5
+ export interface ISearchResult {
6
+ type: 'task' | 'project' | 'sub-project';
7
+ projectId: string;
8
+ projectName: string;
9
+ subProjectId?: string;
10
+ subProjectName?: string;
11
+ taskId?: string;
12
+ title: string;
13
+ snippet?: string;
14
+ status?: string;
15
+ isArchived?: boolean;
16
+ score: number;
17
+ }
18
+
19
+ interface TaskSearchRow {
20
+ id: string;
21
+ title: string;
22
+ description: string;
23
+ project_id: string;
24
+ project_name: string;
25
+ sub_project_id: string;
26
+ sub_project_name: string;
27
+ status: string;
28
+ is_archived: number;
29
+ }
30
+
31
+ interface ProjectSearchRow {
32
+ id: string;
33
+ name: string;
34
+ description: string;
35
+ }
36
+
37
+ interface SubProjectSearchRow {
38
+ id: string;
39
+ name: string;
40
+ project_id: string;
41
+ project_name: string;
42
+ }
43
+
44
+ function escapeLike(s: string): string {
45
+ return s.replace(/[\\%_]/g, m => '\\' + m);
46
+ }
47
+
48
+ function buildSnippet(body: string, query: string): string | undefined {
49
+ if (!body) return undefined;
50
+ const lower = body.toLowerCase();
51
+ const idx = lower.indexOf(query.toLowerCase());
52
+ if (idx < 0) return body.slice(0, 120);
53
+ const start = Math.max(0, idx - 30);
54
+ const end = Math.min(body.length, idx + query.length + 60);
55
+ return (start > 0 ? '…' : '') + body.slice(start, end) + (end < body.length ? '…' : '');
56
+ }
57
+
58
+ function scoreMatch(title: string, body: string, query: string): number {
59
+ const q = query.toLowerCase();
60
+ const t = (title ?? '').toLowerCase();
61
+ const b = (body ?? '').toLowerCase();
62
+ let score = 0;
63
+ if (t === q) score += 100;
64
+ else if (t.startsWith(q)) score += 50;
65
+ else if (t.includes(q)) score += 30;
66
+ if (b.includes(q)) score += 10;
67
+ return score;
68
+ }
69
+
70
+ export async function GET(request: NextRequest) {
71
+ await ensureDb();
72
+ const { searchParams } = new URL(request.url);
73
+ const raw = (searchParams.get('q') ?? '').trim();
74
+ if (raw.length < 1) return NextResponse.json([]);
75
+
76
+ const q = raw.slice(0, 200);
77
+ const like = `%${escapeLike(q)}%`;
78
+ const db = getDb();
79
+
80
+ const taskRows = db.prepare(`
81
+ SELECT t.id, t.title, t.description, t.status, t.is_archived,
82
+ t.project_id, p.name AS project_name,
83
+ t.sub_project_id, sp.name AS sub_project_name
84
+ FROM tasks t
85
+ JOIN projects p ON p.id = t.project_id
86
+ JOIN sub_projects sp ON sp.id = t.sub_project_id
87
+ WHERE (t.title LIKE ? ESCAPE '\\' OR t.description LIKE ? ESCAPE '\\')
88
+ ORDER BY t.is_archived ASC, t.updated_at DESC
89
+ LIMIT 40
90
+ `).all(like, like) as TaskSearchRow[];
91
+
92
+ const projectRows = db.prepare(`
93
+ SELECT id, name, description FROM projects
94
+ WHERE name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\'
95
+ LIMIT 10
96
+ `).all(like, like) as ProjectSearchRow[];
97
+
98
+ const subProjectRows = db.prepare(`
99
+ SELECT sp.id, sp.name, sp.project_id, p.name AS project_name
100
+ FROM sub_projects sp
101
+ JOIN projects p ON p.id = sp.project_id
102
+ WHERE sp.name LIKE ? ESCAPE '\\'
103
+ LIMIT 10
104
+ `).all(like) as SubProjectSearchRow[];
105
+
106
+ const results: ISearchResult[] = [];
107
+
108
+ for (const r of taskRows) {
109
+ results.push({
110
+ type: 'task',
111
+ projectId: r.project_id,
112
+ projectName: r.project_name,
113
+ subProjectId: r.sub_project_id,
114
+ subProjectName: r.sub_project_name,
115
+ taskId: r.id,
116
+ title: r.title,
117
+ snippet: buildSnippet(r.description, q),
118
+ status: r.status,
119
+ isArchived: r.is_archived === 1,
120
+ score: scoreMatch(r.title, r.description, q) + (r.is_archived ? -5 : 0),
121
+ });
122
+ }
123
+
124
+ for (const r of projectRows) {
125
+ results.push({
126
+ type: 'project',
127
+ projectId: r.id,
128
+ projectName: r.name,
129
+ title: r.name,
130
+ snippet: buildSnippet(r.description, q),
131
+ score: scoreMatch(r.name, r.description ?? '', q),
132
+ });
133
+ }
134
+
135
+ for (const r of subProjectRows) {
136
+ results.push({
137
+ type: 'sub-project',
138
+ projectId: r.project_id,
139
+ projectName: r.project_name,
140
+ subProjectId: r.id,
141
+ subProjectName: r.name,
142
+ title: r.name,
143
+ score: scoreMatch(r.name, '', q),
144
+ });
145
+ }
146
+
147
+ results.sort((a, b) => b.score - a.score);
148
+ return NextResponse.json(results.slice(0, 30));
149
+ }
@@ -0,0 +1,52 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { spawn } from 'child_process';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ // Runs `npm install -g idea-manager@latest` as the current user. This only
7
+ // makes sense for local installs of IM — the API is same-origin so there's no
8
+ // remote caller to worry about. After a successful install the running Node
9
+ // process still holds the OLD code; the response tells the client to prompt
10
+ // the user to restart `im start` (or to let PM2 auto-restart).
11
+ export async function POST() {
12
+ const started = Date.now();
13
+ return await new Promise<Response>((resolve) => {
14
+ const child = spawn('npm', ['install', '-g', 'idea-manager@latest', '--no-fund', '--no-audit'], {
15
+ stdio: ['ignore', 'pipe', 'pipe'],
16
+ shell: process.platform === 'win32',
17
+ env: process.env,
18
+ });
19
+
20
+ let stdout = '';
21
+ let stderr = '';
22
+ child.stdout?.on('data', (c: Buffer) => { stdout += c.toString(); });
23
+ child.stderr?.on('data', (c: Buffer) => { stderr += c.toString(); });
24
+
25
+ // 3-minute hard timeout — npm install on stale registry can hang otherwise.
26
+ const timeout = setTimeout(() => {
27
+ child.kill('SIGTERM');
28
+ }, 3 * 60 * 1000);
29
+
30
+ child.on('error', (err) => {
31
+ clearTimeout(timeout);
32
+ resolve(NextResponse.json({
33
+ ok: false,
34
+ error: err.message,
35
+ durationMs: Date.now() - started,
36
+ }, { status: 500 }));
37
+ });
38
+
39
+ child.on('exit', (code, signal) => {
40
+ clearTimeout(timeout);
41
+ const ok = code === 0;
42
+ resolve(NextResponse.json({
43
+ ok,
44
+ code,
45
+ signal: signal ?? null,
46
+ stdout: stdout.slice(-4000),
47
+ stderr: stderr.slice(-4000),
48
+ durationMs: Date.now() - started,
49
+ }, { status: ok ? 200 : 500 }));
50
+ });
51
+ });
52
+ }
@@ -0,0 +1,68 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ // Read installed version from the package that is actually running.
8
+ // process.cwd() may differ when launched via the CLI, so we resolve
9
+ // relative to this file's runtime location by walking up.
10
+ function readInstalledVersion(): string {
11
+ const candidates = [
12
+ // Running from built standalone output
13
+ join(process.cwd(), 'package.json'),
14
+ // Running from source (dev)
15
+ join(process.cwd(), '..', 'package.json'),
16
+ ];
17
+ for (const p of candidates) {
18
+ try {
19
+ const raw = readFileSync(p, 'utf8');
20
+ const parsed = JSON.parse(raw) as { name?: string; version?: string };
21
+ if (parsed.name === 'idea-manager' && typeof parsed.version === 'string') {
22
+ return parsed.version;
23
+ }
24
+ } catch { /* try next */ }
25
+ }
26
+ return '0.0.0';
27
+ }
28
+
29
+ function cmp(a: string, b: string): number {
30
+ const as = a.split('.').map(n => parseInt(n, 10));
31
+ const bs = b.split('.').map(n => parseInt(n, 10));
32
+ for (let i = 0; i < Math.max(as.length, bs.length); i++) {
33
+ const av = as[i] ?? 0, bv = bs[i] ?? 0;
34
+ if (av !== bv) return av - bv;
35
+ }
36
+ return 0;
37
+ }
38
+
39
+ interface CacheEntry { latest: string; at: number }
40
+ let cache: CacheEntry | null = null;
41
+ const CACHE_MS = 10 * 60 * 1000; // 10 minutes
42
+
43
+ async function fetchLatest(): Promise<string | null> {
44
+ if (cache && Date.now() - cache.at < CACHE_MS) return cache.latest;
45
+ try {
46
+ const controller = new AbortController();
47
+ const timer = setTimeout(() => controller.abort(), 5000);
48
+ const res = await fetch('https://registry.npmjs.org/idea-manager/latest', {
49
+ signal: controller.signal,
50
+ cache: 'no-store',
51
+ });
52
+ clearTimeout(timer);
53
+ if (!res.ok) return null;
54
+ const data = await res.json() as { version?: string };
55
+ if (typeof data.version === 'string') {
56
+ cache = { latest: data.version, at: Date.now() };
57
+ return data.version;
58
+ }
59
+ } catch { /* network / timeout */ }
60
+ return null;
61
+ }
62
+
63
+ export async function GET() {
64
+ const current = readInstalledVersion();
65
+ const latest = await fetchLatest();
66
+ const updateAvailable = !!latest && cmp(latest, current) > 0;
67
+ return NextResponse.json({ current, latest, updateAvailable });
68
+ }
@@ -0,0 +1,156 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState, useCallback } from 'react';
4
+ import { useTabContext } from '@/components/tabs/TabContext';
5
+ import type { ISearchResult } from '@/app/api/search/route';
6
+
7
+ export default function GlobalSearch() {
8
+ const [open, setOpen] = useState(false);
9
+ const [query, setQuery] = useState('');
10
+ const [results, setResults] = useState<ISearchResult[]>([]);
11
+ const [idx, setIdx] = useState(0);
12
+ const [loading, setLoading] = useState(false);
13
+ const inputRef = useRef<HTMLInputElement>(null);
14
+ const { openProject } = useTabContext();
15
+ const abortRef = useRef<AbortController | null>(null);
16
+
17
+ // Global ⌘P / Ctrl+P shortcut (K is also acceptable in some editors;
18
+ // P is used because ⌘K is the note-refine palette).
19
+ useEffect(() => {
20
+ const onKey = (e: KeyboardEvent) => {
21
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'p') {
22
+ e.preventDefault();
23
+ setOpen(prev => !prev);
24
+ }
25
+ };
26
+ window.addEventListener('keydown', onKey);
27
+ return () => window.removeEventListener('keydown', onKey);
28
+ }, []);
29
+
30
+ // Reset state each time it opens
31
+ useEffect(() => {
32
+ if (!open) return;
33
+ setQuery('');
34
+ setResults([]);
35
+ setIdx(0);
36
+ // defer focus until after modal animation paint
37
+ const id = requestAnimationFrame(() => inputRef.current?.focus());
38
+ return () => cancelAnimationFrame(id);
39
+ }, [open]);
40
+
41
+ // Debounced search
42
+ useEffect(() => {
43
+ if (!open) return;
44
+ const q = query.trim();
45
+ if (q.length < 1) { setResults([]); setLoading(false); return; }
46
+ setLoading(true);
47
+ const timer = setTimeout(() => {
48
+ abortRef.current?.abort();
49
+ const abort = new AbortController();
50
+ abortRef.current = abort;
51
+ fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: abort.signal })
52
+ .then(r => r.ok ? r.json() : [])
53
+ .then((data: ISearchResult[]) => {
54
+ setResults(Array.isArray(data) ? data : []);
55
+ setIdx(0);
56
+ setLoading(false);
57
+ })
58
+ .catch(() => { /* aborted or network — leave UI steady */ });
59
+ }, 120);
60
+ return () => clearTimeout(timer);
61
+ }, [query, open]);
62
+
63
+ const pick = useCallback((r: ISearchResult) => {
64
+ openProject(r.projectId, r.projectName, r.subProjectId, r.taskId);
65
+ setOpen(false);
66
+ }, [openProject]);
67
+
68
+ const handleKey = (e: React.KeyboardEvent) => {
69
+ if (e.key === 'Escape') { setOpen(false); return; }
70
+ if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(i + 1, results.length - 1)); return; }
71
+ if (e.key === 'ArrowUp') { e.preventDefault(); setIdx(i => Math.max(i - 1, 0)); return; }
72
+ if (e.key === 'Enter') {
73
+ e.preventDefault();
74
+ const target = results[idx];
75
+ if (target) pick(target);
76
+ }
77
+ };
78
+
79
+ if (!open) return null;
80
+
81
+ return (
82
+ <div
83
+ onClick={() => setOpen(false)}
84
+ className="fixed inset-0 z-[60] flex items-start justify-center pt-[14vh]"
85
+ style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(3px)' }}
86
+ >
87
+ <div
88
+ onClick={(e) => e.stopPropagation()}
89
+ className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-xl animate-dialog-in"
90
+ >
91
+ <div className="px-4 py-3 border-b border-border flex items-center gap-2">
92
+ <span className="text-muted-foreground">🔎</span>
93
+ <input
94
+ ref={inputRef}
95
+ value={query}
96
+ onChange={(e) => setQuery(e.target.value)}
97
+ onKeyDown={handleKey}
98
+ placeholder="태스크 · 프로젝트 · 워크스페이스 검색… (⌘P)"
99
+ className="flex-1 bg-transparent text-sm text-foreground focus:outline-none"
100
+ />
101
+ <span className="text-[10px] text-muted-foreground/70 px-1.5 py-0.5 border border-border rounded">
102
+ ↑↓ · ↵ · Esc
103
+ </span>
104
+ </div>
105
+
106
+ <div className="max-h-[55vh] overflow-y-auto">
107
+ {loading && (
108
+ <div className="px-4 py-6 text-xs text-muted-foreground">검색 중…</div>
109
+ )}
110
+ {!loading && query.trim() && results.length === 0 && (
111
+ <div className="px-4 py-6 text-xs text-muted-foreground">일치하는 항목 없음</div>
112
+ )}
113
+ {!loading && !query.trim() && (
114
+ <div className="px-4 py-6 text-xs text-muted-foreground">
115
+ 무엇을 찾으시나요? 태스크 제목·본문, 프로젝트·워크스페이스 이름을 검색합니다.
116
+ </div>
117
+ )}
118
+ <ul>
119
+ {results.map((r, i) => (
120
+ <li
121
+ key={`${r.type}-${r.taskId ?? r.subProjectId ?? r.projectId}`}
122
+ onMouseEnter={() => setIdx(i)}
123
+ onClick={() => pick(r)}
124
+ className={`px-4 py-2.5 cursor-pointer border-l-2 ${
125
+ i === idx ? 'bg-muted border-primary' : 'border-transparent'
126
+ }`}
127
+ >
128
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
129
+ <span className={`px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wide ${
130
+ r.type === 'task' ? 'bg-primary/15 text-primary' :
131
+ r.type === 'project' ? 'bg-accent/15 text-accent' :
132
+ 'bg-warning/15 text-warning'
133
+ }`}>
134
+ {r.type === 'sub-project' ? 'project' : r.type}
135
+ </span>
136
+ <span>{r.projectName}</span>
137
+ {r.subProjectName && r.type === 'task' && (
138
+ <>
139
+ <span className="opacity-50">›</span>
140
+ <span>{r.subProjectName}</span>
141
+ </>
142
+ )}
143
+ {r.isArchived && <span className="text-muted-foreground/70 italic">(archived)</span>}
144
+ </div>
145
+ <div className="text-sm text-foreground mt-0.5 truncate">{r.title}</div>
146
+ {r.snippet && (
147
+ <div className="text-xs text-muted-foreground/80 mt-0.5 truncate">{r.snippet}</div>
148
+ )}
149
+ </li>
150
+ ))}
151
+ </ul>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ }