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.
- package/.next/build-manifest.json +2 -2
- package/.next/routes-manifest.json +18 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/search/route.js +127 -0
- package/.next/server/app/api/search/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/update/route.js +1 -0
- package/.next/server/app/api/update/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/version/route.js +1 -0
- package/.next/server/app/api/version/route_client-reference-manifest.js +1 -0
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +12 -12
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -7
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/static/chunks/app/_global-error/page-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/archive/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/filesystem/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/filesystem/tree/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/global-memo/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/health/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/projects/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/search/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/sync/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/update/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/api/version/route-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/app/page-9a1dc101e82c397c.js +28 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-e6a77f238d2cdbb9.js +1 -0
- package/.next/static/css/eab748b03f49c43a.css +3 -0
- package/.next/static/mxrEVQX3r5YlDPZgpDvSp/_buildManifest.js +1 -0
- package/README.ja.md +4 -1
- package/README.ko.md +36 -6
- package/README.md +31 -6
- package/README.zh.md +4 -1
- package/package.json +1 -1
- package/src/app/api/search/route.ts +149 -0
- package/src/app/api/update/route.ts +52 -0
- package/src/app/api/version/route.ts +68 -0
- package/src/components/search/GlobalSearch.tsx +156 -0
- package/src/components/search/QuickCapture.tsx +208 -0
- package/src/components/tabs/TabBar.tsx +2 -0
- package/src/components/tabs/TabShell.tsx +4 -0
- package/src/components/task/CommandPalette.tsx +48 -2
- package/src/components/task/NoteEditor.tsx +16 -1
- package/src/components/task/TaskChat.tsx +31 -20
- package/src/components/task/TaskDetail.tsx +62 -2
- package/src/components/update/UpdateButton.tsx +190 -0
- package/src/components/workspace/WorkspacePanel.tsx +1 -0
- package/.next/static/63zinfEtSLCdG9nUZ3W-E/_buildManifest.js +0 -1
- package/.next/static/chunks/app/_global-error/page-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/archive/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/filesystem/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/filesystem/tree/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/global-memo/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/health/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/projects/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/api/sync/route-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/app/page-6a511af64da7531f.js +0 -28
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-6ec0e723e471f87a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-6ec0e723e471f87a.js +0 -1
- package/.next/static/css/cc32379d0efa7d1d.css +0 -3
- /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 →
|
|
43
|
-
|
|
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
|
-
- **
|
|
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`
|
|
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
|
-
- **
|
|
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.
|
|
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
|
+
}
|