idea-manager 0.7.6 → 0.7.8

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/README.ja.md ADDED
@@ -0,0 +1,127 @@
1
+ # IM (Idea Manager)
2
+
3
+ [English](README.md) | [한국어](README.ko.md) | **日本語** | [中文](README.zh.md)
4
+
5
+ > アイデアから実行可能なプロンプトまで — マルチプロジェクトワークフローマネージャー
6
+
7
+ 複数のプロジェクトを同時に進めるデベロッパー向けのタスク管理ツールです。アイデアをサブプロジェクトとタスクに整理し、各タスクごとにプロンプトを磨いてClaude CodeなどのAIエージェントに渡すことができます。MCP Serverを内蔵しており、AIエージェントが自律的にタスクを取得して実行できます。
8
+
9
+ ![IM Workspace](docs/screenshot.png)
10
+
11
+ ## コアワークフロー
12
+
13
+ ```
14
+ ブレインストーミング → サブプロジェクト/タスクに整理 → プロンプト精製 → MCPでAI実行
15
+ ```
16
+
17
+ ### 階層構造
18
+
19
+ ```
20
+ プロジェクト
21
+ ├── サブプロジェクト A
22
+ │ ├── タスク 1 → プロンプト
23
+ │ ├── タスク 2 → プロンプト
24
+ │ └── タスク 3 → プロンプト
25
+ └── サブプロジェクト B
26
+ ├── タスク 4 → プロンプト
27
+ └── タスク 5 → プロンプト
28
+ ```
29
+
30
+ ### タスクステータスフロー
31
+
32
+ ```
33
+ 💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
34
+ 🔴 Problem
35
+ ```
36
+
37
+ ## インストール
38
+
39
+ ```bash
40
+ npm install -g idea-manager
41
+ ```
42
+
43
+ ## 使い方
44
+
45
+ ### Web UIの起動
46
+
47
+ ```bash
48
+ im start
49
+ ```
50
+
51
+ `http://localhost:3456`でWeb UIが開きます。
52
+
53
+ ```bash
54
+ # ポート変更
55
+ im start -p 4000
56
+ ```
57
+
58
+ ### MCP Serverの起動
59
+
60
+ ```bash
61
+ im mcp
62
+ ```
63
+
64
+ #### Claude Desktop設定 (claude_desktop_config.json)
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "idea-manager": {
70
+ "command": "npx",
71
+ "args": ["-y", "idea-manager", "mcp"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ #### Claude Code設定
78
+
79
+ ```bash
80
+ claude mcp add idea-manager -- npx -y idea-manager mcp
81
+ ```
82
+
83
+ ### MCPツール
84
+
85
+ | ツール | 説明 |
86
+ |--------|------|
87
+ | `list-projects` | プロジェクト一覧の取得 |
88
+ | `get-project-context` | サブプロジェクト+タスクツリー全体の取得 |
89
+ | `get-next-task` | 次に実行するタスクとプロンプトの取得(status=submitted) |
90
+ | `get-task-prompt` | 特定タスクのプロンプト取得 |
91
+ | `update-status` | タスクステータスの変更(idea/writing/submitted/testing/done/problem) |
92
+ | `report-completion` | タスク完了の報告 |
93
+
94
+ ## 主な機能
95
+
96
+ - **タブベースのマルチプロジェクト** — ブラウザ/IDEのように複数プロジェクトをタブで同時に開き、タブ切替時に状態を保持
97
+ - **3パネルワークスペース** — ブレインストーミング | プロジェクトツリー | タスク詳細、パネル間ドラッグでサイズ調整
98
+ - **ツリー型プロジェクト構造** — サブプロジェクト配下にタスクを階層的に表示
99
+ - **ブレインストーミングパネル** — 自由形式メモ、折りたたみ/展開可能
100
+ - **プロンプトエディタ** — タスクごとにプロンプトを作成/編集/コピー、AIによる磨き上げ
101
+ - **AIチャット** — タスクごとのAI対話でプロンプトを具体化
102
+ - **3タブダッシュボード** — 進行中 / 全体 / 今日のタスク
103
+ - **キーボードショートカット** — Ctrl+Tab/Ctrl+Shift+Tabでタブ移動、B: ブレインストーミング切替、N: サブプロジェクト追加、T: タスク追加、Cmd+1~6: ステータス変更
104
+ - **PWA対応** — アプリとしてインストールして独立ウィンドウで使用可能
105
+ - **Watchモード** — submittedタスクをClaude CLIで自動実行、リアルタイム進捗表示
106
+ - **MCP Server内蔵** — AIエージェントの自律実行をサポート
107
+ - **ローカルファースト** — SQLiteベース、データは`~/.idea-manager/`に保存
108
+
109
+ ## 技術スタック
110
+
111
+ | 領域 | 技術 |
112
+ |------|------|
113
+ | フロントエンド | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
114
+ | バックエンド | Next.js API Routes |
115
+ | データベース | SQLite (better-sqlite3) |
116
+ | AI | Claude CLI(サブスクリプションベース、APIキー不要) |
117
+ | MCP | Model Context Protocol (stdio) |
118
+ | CLI | Commander.js |
119
+
120
+ ## 必要条件
121
+
122
+ - **Node.js** 18+
123
+ - **Claude CLI** — AIチャット/磨き上げ機能に必要(Claudeサブスクリプション必要)。なくてもタスク管理やプロンプト作成などの基本機能は正常に動作します。
124
+
125
+ ## ライセンス
126
+
127
+ MIT
package/README.ko.md ADDED
@@ -0,0 +1,127 @@
1
+ # IM (Idea Manager)
2
+
3
+ [English](README.md) | **한국어** | [日本語](README.ja.md) | [中文](README.zh.md)
4
+
5
+ > 아이디어에서 실행 가능한 프롬프트까지, 멀티 프로젝트 워크플로우 매니저
6
+
7
+ 여러 프로젝트를 동시에 진행하는 개발자를 위한 태스크 관리 도구입니다. 아이디어를 서브 프로젝트와 태스크로 조직화하고, 각 태스크별 프롬프트를 정제하여 Claude Code 등 AI 에이전트에게 전달할 수 있습니다. MCP Server를 내장하고 있어 AI 에이전트가 자율적으로 태스크를 가져가 실행할 수 있습니다.
8
+
9
+ ![IM Workspace](docs/screenshot.png)
10
+
11
+ ## 핵심 워크플로우
12
+
13
+ ```
14
+ 브레인스토밍 → 서브 프로젝트/태스크 조직화 → 프롬프트 정제 → MCP로 AI 실행
15
+ ```
16
+
17
+ ### 계층 구조
18
+
19
+ ```
20
+ 프로젝트
21
+ ├── 서브 프로젝트 A
22
+ │ ├── 태스크 1 → 프롬프트
23
+ │ ├── 태스크 2 → 프롬프트
24
+ │ └── 태스크 3 → 프롬프트
25
+ └── 서브 프로젝트 B
26
+ ├── 태스크 4 → 프롬프트
27
+ └── 태스크 5 → 프롬프트
28
+ ```
29
+
30
+ ### 태스크 상태 흐름
31
+
32
+ ```
33
+ 💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
34
+ 🔴 Problem
35
+ ```
36
+
37
+ ## 설치
38
+
39
+ ```bash
40
+ npm install -g idea-manager
41
+ ```
42
+
43
+ ## 사용법
44
+
45
+ ### 웹 UI 실행
46
+
47
+ ```bash
48
+ im start
49
+ ```
50
+
51
+ `http://localhost:3456`에서 웹 UI가 열립니다.
52
+
53
+ ```bash
54
+ # 포트 변경
55
+ im start -p 4000
56
+ ```
57
+
58
+ ### MCP Server 실행
59
+
60
+ ```bash
61
+ im mcp
62
+ ```
63
+
64
+ #### Claude Desktop 설정 (claude_desktop_config.json)
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "idea-manager": {
70
+ "command": "npx",
71
+ "args": ["-y", "idea-manager", "mcp"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ #### Claude Code 설정
78
+
79
+ ```bash
80
+ claude mcp add idea-manager -- npx -y idea-manager mcp
81
+ ```
82
+
83
+ ### MCP 제공 도구
84
+
85
+ | 도구 | 설명 |
86
+ |------|------|
87
+ | `list-projects` | 프로젝트 목록 조회 |
88
+ | `get-project-context` | 서브 프로젝트 + 태스크 트리 전체 조회 |
89
+ | `get-next-task` | 다음 실행할 태스크와 프롬프트 조회 (status=submitted) |
90
+ | `get-task-prompt` | 특정 태스크의 프롬프트 조회 |
91
+ | `update-status` | 태스크 상태 변경 (idea/writing/submitted/testing/done/problem) |
92
+ | `report-completion` | 태스크 완료 보고 |
93
+
94
+ ## 주요 기능
95
+
96
+ - **탭 기반 멀티 프로젝트** — 브라우저/IDE처럼 여러 프로젝트를 탭으로 동시에 열기, 탭 전환 시 상태 보존
97
+ - **3-패널 워크스페이스** — 브레인스토밍 | 프로젝트 트리 | 태스크 상세, 패널 간 드래그로 크기 조절
98
+ - **트리형 프로젝트 구조** — 서브 프로젝트 아래 태스크가 계층적으로 표시
99
+ - **브레인스토밍 패널** — 자유 형식 메모, 접기/펼치기 가능
100
+ - **프롬프트 에디터** — 태스크별 프롬프트 작성/편집/복사, AI 다듬기
101
+ - **AI 채팅** — 태스크별 AI 대화로 프롬프트 구체화
102
+ - **3탭 대시보드** — 진행 중 / 전체 / 오늘 할 일
103
+ - **키보드 단축키** — Ctrl+Tab/Ctrl+Shift+Tab으로 탭 이동, B: 브레인스토밍 토글, N: 서브 프로젝트 추가, T: 태스크 추가, Cmd+1~6: 상태 변경 (한영 전환 상관없이 동작)
104
+ - **PWA 지원** — 앱으로 설치하여 독립 창에서 사용 가능
105
+ - **Watch 모드** — submitted 태스크를 Claude CLI로 자동 실행, 실시간 진행 표시
106
+ - **MCP Server 내장** — AI 에이전트 자율 실행 지원
107
+ - **로컬 우선** — SQLite 기반, 데이터는 `~/.idea-manager/`에 저장
108
+
109
+ ## 기술 스택
110
+
111
+ | 영역 | 기술 |
112
+ |------|------|
113
+ | Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
114
+ | Backend | Next.js API Routes |
115
+ | Database | SQLite (better-sqlite3) |
116
+ | AI | Claude CLI (구독 기반, API 키 불필요) |
117
+ | MCP | Model Context Protocol (stdio) |
118
+ | CLI | Commander.js |
119
+
120
+ ## 요구 사항
121
+
122
+ - **Node.js** 18+
123
+ - **Claude CLI** — AI 채팅/다듬기 기능 사용 시 필요 (Claude 구독 필요). 없어도 태스크 관리, 프롬프트 작성 등 기본 기능은 정상 동작합니다.
124
+
125
+ ## 라이선스
126
+
127
+ MIT
package/README.md CHANGED
@@ -1,65 +1,67 @@
1
1
  # IM (Idea Manager)
2
2
 
3
- > 아이디어에서 실행 가능한 프롬프트까지, 멀티 프로젝트 워크플로우 매니저
3
+ **English** | [한국어](README.ko.md) | [日本語](README.ja.md) | [中文](README.zh.md)
4
4
 
5
- 여러 프로젝트를 동시에 진행하는 개발자를 위한 태스크 관리 도구입니다. 아이디어를 서브 프로젝트와 태스크로 조직화하고, 각 태스크별 프롬프트를 정제하여 Claude Code 등 AI 에이전트에게 전달할 수 있습니다. MCP Server를 내장하고 있어 AI 에이전트가 자율적으로 태스크를 가져가 실행할 수 있습니다.
5
+ > From ideas to executable prompts a multi-project workflow manager
6
+
7
+ A task management tool for developers juggling multiple projects simultaneously. Organize ideas into sub-projects and tasks, refine prompts for each task, and hand them off to AI agents like Claude Code. With a built-in MCP Server, AI agents can autonomously pick up and execute tasks.
6
8
 
7
9
  ![IM Workspace](docs/screenshot.png)
8
10
 
9
- ## 핵심 워크플로우
11
+ ## Core Workflow
10
12
 
11
13
  ```
12
- 브레인스토밍서브 프로젝트/태스크 조직화프롬프트 정제MCP로 AI 실행
14
+ BrainstormingOrganize into Sub-projects/TasksRefine PromptsExecute via MCP
13
15
  ```
14
16
 
15
- ### 계층 구조
17
+ ### Hierarchy
16
18
 
17
19
  ```
18
- 프로젝트
19
- ├── 서브 프로젝트 A
20
- │ ├── 태스크 1 → 프롬프트
21
- │ ├── 태스크 2 → 프롬프트
22
- │ └── 태스크 3 → 프롬프트
23
- └── 서브 프로젝트 B
24
- ├── 태스크 4 → 프롬프트
25
- └── 태스크 5 → 프롬프트
20
+ Project
21
+ ├── Sub-project A
22
+ │ ├── Task 1 → Prompt
23
+ │ ├── Task 2 → Prompt
24
+ │ └── Task 3 → Prompt
25
+ └── Sub-project B
26
+ ├── Task 4 → Prompt
27
+ └── Task 5 → Prompt
26
28
  ```
27
29
 
28
- ### 태스크 상태 흐름
30
+ ### Task Status Flow
29
31
 
30
32
  ```
31
33
  💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
32
34
  🔴 Problem
33
35
  ```
34
36
 
35
- ## 설치
37
+ ## Installation
36
38
 
37
39
  ```bash
38
40
  npm install -g idea-manager
39
41
  ```
40
42
 
41
- ## 사용법
43
+ ## Usage
42
44
 
43
- ### UI 실행
45
+ ### Start Web UI
44
46
 
45
47
  ```bash
46
48
  im start
47
49
  ```
48
50
 
49
- `http://localhost:3456`에서 웹 UI가 열립니다.
51
+ Opens the web UI at `http://localhost:3456`.
50
52
 
51
53
  ```bash
52
- # 포트 변경
54
+ # Custom port
53
55
  im start -p 4000
54
56
  ```
55
57
 
56
- ### MCP Server 실행
58
+ ### Start MCP Server
57
59
 
58
60
  ```bash
59
61
  im mcp
60
62
  ```
61
63
 
62
- #### Claude Desktop 설정 (claude_desktop_config.json)
64
+ #### Claude Desktop Configuration (claude_desktop_config.json)
63
65
 
64
66
  ```json
65
67
  {
@@ -72,54 +74,54 @@ im mcp
72
74
  }
73
75
  ```
74
76
 
75
- #### Claude Code 설정
77
+ #### Claude Code Configuration
76
78
 
77
79
  ```bash
78
80
  claude mcp add idea-manager -- npx -y idea-manager mcp
79
81
  ```
80
82
 
81
- ### MCP 제공 도구
82
-
83
- | 도구 | 설명 |
84
- |------|------|
85
- | `list-projects` | 프로젝트 목록 조회 |
86
- | `get-project-context` | 서브 프로젝트 + 태스크 트리 전체 조회 |
87
- | `get-next-task` | 다음 실행할 태스크와 프롬프트 조회 (status=submitted) |
88
- | `get-task-prompt` | 특정 태스크의 프롬프트 조회 |
89
- | `update-status` | 태스크 상태 변경 (idea/writing/submitted/testing/done/problem) |
90
- | `report-completion` | 태스크 완료 보고 |
91
-
92
- ## 주요 기능
93
-
94
- - **탭 기반 멀티 프로젝트** 브라우저/IDE처럼 여러 프로젝트를 탭으로 동시에 열기, 전환 상태 보존
95
- - **3-패널 워크스페이스**브레인스토밍 | 프로젝트 트리 | 태스크 상세, 패널 드래그로 크기 조절
96
- - **트리형 프로젝트 구조** 서브 프로젝트 아래 태스크가 계층적으로 표시
97
- - **브레인스토밍 패널**자유 형식 메모, 접기/펼치기 가능
98
- - **프롬프트 에디터**태스크별 프롬프트 작성/편집/복사, AI 다듬기
99
- - **AI 채팅**태스크별 AI 대화로 프롬프트 구체화
100
- - **3 대시보드**진행 / 전체 / 오늘 할 일
101
- - **키보드 단축키** — Ctrl+Tab/Ctrl+Shift+Tab으로 이동, B: 브레인스토밍 토글, N: 서브 프로젝트 추가, T: 태스크 추가, Cmd+1~6: 상태 변경 (한영 전환 상관없이 동작)
102
- - **PWA 지원**앱으로 설치하여 독립 창에서 사용 가능
103
- - **Watch 모드** — submitted 태스크를 Claude CLI 자동 실행, 실시간 진행 표시
104
- - **MCP Server 내장** AI 에이전트 자율 실행 지원
105
- - **로컬 우선** — SQLite 기반, 데이터는 `~/.idea-manager/`에 저장
106
-
107
- ## 기술 스택
108
-
109
- | 영역 | 기술 |
110
- |------|------|
83
+ ### MCP Tools
84
+
85
+ | Tool | Description |
86
+ |------|-------------|
87
+ | `list-projects` | List all projects |
88
+ | `get-project-context` | Get full sub-project + task tree |
89
+ | `get-next-task` | Get next task to execute (status=submitted) |
90
+ | `get-task-prompt` | Get prompt for a specific task |
91
+ | `update-status` | Change task status (idea/writing/submitted/testing/done/problem) |
92
+ | `report-completion` | Report task completion |
93
+
94
+ ## Key Features
95
+
96
+ - **Tab-based Multi-project** Open multiple projects in tabs like a browser/IDE, state preserved on tab switch
97
+ - **3-Panel Workspace**Brainstorming | Project Tree | Task Detail, drag to resize panels
98
+ - **Tree-structured Projects**Tasks displayed hierarchically under sub-projects
99
+ - **Brainstorming Panel**Free-form notes, collapsible
100
+ - **Prompt Editor**Write/edit/copy prompts per task, AI refinement
101
+ - **AI Chat**Per-task AI conversations to refine prompts
102
+ - **3-Tab Dashboard**Active / All / Today
103
+ - **Keyboard Shortcuts** — Ctrl+Tab/Ctrl+Shift+Tab for tab navigation, B: toggle brainstorm, N: add sub-project, T: add task, Cmd+1~6: change status
104
+ - **PWA Support**Install as an app for a standalone window experience
105
+ - **Watch Mode**Auto-execute submitted tasks via Claude CLI with real-time progress
106
+ - **Built-in MCP Server**Supports autonomous AI agent execution
107
+ - **Local-first** — SQLite-based, data stored in `~/.idea-manager/`
108
+
109
+ ## Tech Stack
110
+
111
+ | Area | Technology |
112
+ |------|------------|
111
113
  | Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
112
114
  | Backend | Next.js API Routes |
113
115
  | Database | SQLite (better-sqlite3) |
114
- | AI | Claude CLI (구독 기반, API 불필요) |
116
+ | AI | Claude CLI (subscription-based, no API key needed) |
115
117
  | MCP | Model Context Protocol (stdio) |
116
118
  | CLI | Commander.js |
117
119
 
118
- ## 요구 사항
120
+ ## Requirements
119
121
 
120
122
  - **Node.js** 18+
121
- - **Claude CLI** — AI 채팅/다듬기 기능 사용 필요 (Claude 구독 필요). 없어도 태스크 관리, 프롬프트 작성 기본 기능은 정상 동작합니다.
123
+ - **Claude CLI** — Required for AI chat/refinement features (Claude subscription needed). Core features like task management and prompt editing work without it.
122
124
 
123
- ## 라이선스
125
+ ## License
124
126
 
125
127
  MIT
package/README.zh.md ADDED
@@ -0,0 +1,127 @@
1
+ # IM (Idea Manager)
2
+
3
+ [English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | **中文**
4
+
5
+ > 从创意到可执行提示词 — 多项目工作流管理器
6
+
7
+ 专为同时管理多个项目的开发者设计的任务管理工具。将创意组织成子项目和任务,为每个任务精炼提示词,并交给 Claude Code 等 AI 代理执行。内置 MCP Server,AI 代理可以自主获取并执行任务。
8
+
9
+ ![IM Workspace](docs/screenshot.png)
10
+
11
+ ## 核心工作流
12
+
13
+ ```
14
+ 头脑风暴 → 组织成子项目/任务 → 精炼提示词 → 通过 MCP 执行 AI
15
+ ```
16
+
17
+ ### 层级结构
18
+
19
+ ```
20
+ 项目
21
+ ├── 子项目 A
22
+ │ ├── 任务 1 → 提示词
23
+ │ ├── 任务 2 → 提示词
24
+ │ └── 任务 3 → 提示词
25
+ └── 子项目 B
26
+ ├── 任务 4 → 提示词
27
+ └── 任务 5 → 提示词
28
+ ```
29
+
30
+ ### 任务状态流
31
+
32
+ ```
33
+ 💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
34
+ 🔴 Problem
35
+ ```
36
+
37
+ ## 安装
38
+
39
+ ```bash
40
+ npm install -g idea-manager
41
+ ```
42
+
43
+ ## 使用方法
44
+
45
+ ### 启动 Web UI
46
+
47
+ ```bash
48
+ im start
49
+ ```
50
+
51
+ 在 `http://localhost:3456` 打开 Web UI。
52
+
53
+ ```bash
54
+ # 自定义端口
55
+ im start -p 4000
56
+ ```
57
+
58
+ ### 启动 MCP Server
59
+
60
+ ```bash
61
+ im mcp
62
+ ```
63
+
64
+ #### Claude Desktop 配置 (claude_desktop_config.json)
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "idea-manager": {
70
+ "command": "npx",
71
+ "args": ["-y", "idea-manager", "mcp"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ #### Claude Code 配置
78
+
79
+ ```bash
80
+ claude mcp add idea-manager -- npx -y idea-manager mcp
81
+ ```
82
+
83
+ ### MCP 工具
84
+
85
+ | 工具 | 描述 |
86
+ |------|------|
87
+ | `list-projects` | 获取项目列表 |
88
+ | `get-project-context` | 获取完整的子项目+任务树 |
89
+ | `get-next-task` | 获取下一个待执行的任务和提示词(status=submitted) |
90
+ | `get-task-prompt` | 获取指定任务的提示词 |
91
+ | `update-status` | 更改任务状态(idea/writing/submitted/testing/done/problem) |
92
+ | `report-completion` | 报告任务完成 |
93
+
94
+ ## 主要功能
95
+
96
+ - **标签式多项目** — 像浏览器/IDE一样用标签页同时打开多个项目,切换标签时保持状态
97
+ - **三栏工作区** — 头脑风暴 | 项目树 | 任务详情,拖拽调整面板大小
98
+ - **树形项目结构** — 任务在子项目下层级展示
99
+ - **头脑风暴面板** — 自由格式笔记,可折叠/展开
100
+ - **提示词编辑器** — 按任务编写/编辑/复制提示词,AI 润色
101
+ - **AI 聊天** — 按任务进行 AI 对话以细化提示词
102
+ - **三标签仪表盘** — 进行中 / 全部 / 今日待办
103
+ - **键盘快捷键** — Ctrl+Tab/Ctrl+Shift+Tab 切换标签,B:切换头脑风暴,N:添加子项目,T:添加任务,Cmd+1~6:更改状态
104
+ - **PWA 支持** — 可安装为应用,在独立窗口中使用
105
+ - **Watch 模式** — 通过 Claude CLI 自动执行已提交的任务,实时显示进度
106
+ - **内置 MCP Server** — 支持 AI 代理自主执行
107
+ - **本地优先** — 基于 SQLite,数据存储在 `~/.idea-manager/`
108
+
109
+ ## 技术栈
110
+
111
+ | 领域 | 技术 |
112
+ |------|------|
113
+ | 前端 | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
114
+ | 后端 | Next.js API Routes |
115
+ | 数据库 | SQLite (better-sqlite3) |
116
+ | AI | Claude CLI(基于订阅,无需 API 密钥) |
117
+ | MCP | Model Context Protocol (stdio) |
118
+ | CLI | Commander.js |
119
+
120
+ ## 系统要求
121
+
122
+ - **Node.js** 18+
123
+ - **Claude CLI** — AI 聊天/润色功能需要(需要 Claude 订阅)。即使没有,任务管理和提示词编写等基本功能也可正常使用。
124
+
125
+ ## 许可证
126
+
127
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idea-manager",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "AI 기반 브레인스토밍 → 구조화 → 프롬프트 생성 도구. MCP Server 내장.",
5
5
  "keywords": [
6
6
  "brainstorm",
@@ -39,6 +39,9 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.78.0",
42
+ "@dnd-kit/core": "^6.3.1",
43
+ "@dnd-kit/sortable": "^10.0.0",
44
+ "@dnd-kit/utilities": "^3.2.2",
42
45
  "@modelcontextprotocol/sdk": "^1.27.1",
43
46
  "better-sqlite3": "^12.6.2",
44
47
  "commander": "^14.0.3",
package/public/sw.js CHANGED
@@ -1,27 +1,49 @@
1
- const CACHE_NAME = 'im-v1';
1
+ const CACHE_NAME = 'im-v3';
2
2
 
3
3
  self.addEventListener('install', (event) => {
4
4
  self.skipWaiting();
5
5
  });
6
6
 
7
7
  self.addEventListener('activate', (event) => {
8
- event.waitUntil(clients.claim());
8
+ event.waitUntil(
9
+ caches.keys().then((keys) =>
10
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
11
+ ).then(() => clients.claim())
12
+ );
9
13
  });
10
14
 
11
15
  self.addEventListener('fetch', (event) => {
12
- // Network-first strategy: always try network, fall back to cache
16
+ const url = new URL(event.request.url);
17
+
18
+ // Skip non-GET and API requests
19
+ if (event.request.method !== 'GET' || url.pathname.startsWith('/api/')) {
20
+ return;
21
+ }
22
+
23
+ // Navigation requests — network-first, fallback to cached root
24
+ if (event.request.mode === 'navigate') {
25
+ event.respondWith(
26
+ fetch(event.request).catch(async () => {
27
+ const cached = await caches.match('/');
28
+ return cached || new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/html' } });
29
+ })
30
+ );
31
+ return;
32
+ }
33
+
34
+ // Static assets — network-first with cache fallback
13
35
  event.respondWith(
14
36
  fetch(event.request)
15
37
  .then((response) => {
16
- // Cache successful GET responses
17
- if (event.request.method === 'GET' && response.status === 200) {
38
+ if (response.status === 200) {
18
39
  const clone = response.clone();
19
- caches.open(CACHE_NAME).then((cache) => {
20
- cache.put(event.request, clone);
21
- });
40
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
22
41
  }
23
42
  return response;
24
43
  })
25
- .catch(() => caches.match(event.request))
44
+ .catch(async () => {
45
+ const cached = await caches.match(event.request);
46
+ return cached || new Response('', { status: 404 });
47
+ })
26
48
  );
27
49
  });
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { getSubProjectsWithStats, createSubProject } from '@/lib/db/queries/sub-projects';
2
+ import { getSubProjectsWithStats, createSubProject, reorderSubProjects } from '@/lib/db/queries/sub-projects';
3
3
 
4
4
  export async function GET(
5
5
  _request: NextRequest,
@@ -29,3 +29,19 @@ export async function POST(
29
29
  });
30
30
  return NextResponse.json(sp, { status: 201 });
31
31
  }
32
+
33
+ export async function PUT(
34
+ request: NextRequest,
35
+ { params }: { params: Promise<{ id: string }> },
36
+ ) {
37
+ const { id } = await params;
38
+ const body = await request.json();
39
+
40
+ if (!Array.isArray(body.orderedIds)) {
41
+ return NextResponse.json({ error: 'orderedIds array is required' }, { status: 400 });
42
+ }
43
+
44
+ reorderSubProjects(id, body.orderedIds);
45
+ const subProjects = getSubProjectsWithStats(id);
46
+ return NextResponse.json(subProjects);
47
+ }
@@ -1,6 +1,22 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
+ import {
5
+ DndContext,
6
+ closestCenter,
7
+ KeyboardSensor,
8
+ PointerSensor,
9
+ useSensor,
10
+ useSensors,
11
+ type DragEndEvent,
12
+ } from '@dnd-kit/core';
13
+ import {
14
+ SortableContext,
15
+ sortableKeyboardCoordinates,
16
+ verticalListSortingStrategy,
17
+ useSortable,
18
+ } from '@dnd-kit/sortable';
19
+ import { CSS } from '@dnd-kit/utilities';
4
20
  import type { ITask, ISubProjectWithStats, TaskStatus } from '@/types';
5
21
  import { statusIcon } from './StatusFlow';
6
22
 
@@ -23,6 +39,7 @@ export default function ProjectTree({
23
39
  onStatusChange,
24
40
  onTodayToggle,
25
41
  onDeleteTask,
42
+ onReorderSubs,
26
43
  }: {
27
44
  subProjects: ISubProjectWithStats[];
28
45
  tasks: ITask[];
@@ -36,11 +53,31 @@ export default function ProjectTree({
36
53
  onStatusChange: (taskId: string, status: TaskStatus) => void;
37
54
  onTodayToggle: (taskId: string, isToday: boolean) => void;
38
55
  onDeleteTask: (taskId: string) => void;
56
+ onReorderSubs?: (orderedIds: string[]) => void;
39
57
  }) {
40
58
  const [collapsedSubs, setCollapsedSubs] = useState<Set<string>>(new Set());
41
59
  const [addingTaskFor, setAddingTaskFor] = useState<string | null>(null);
42
60
  const [newTaskTitle, setNewTaskTitle] = useState('');
43
61
 
62
+ const sensors = useSensors(
63
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
64
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
65
+ );
66
+
67
+ const handleDragEnd = (event: DragEndEvent) => {
68
+ const { active, over } = event;
69
+ if (!over || active.id === over.id || !onReorderSubs) return;
70
+
71
+ const oldIndex = subProjects.findIndex(sp => sp.id === active.id);
72
+ const newIndex = subProjects.findIndex(sp => sp.id === over.id);
73
+ if (oldIndex === -1 || newIndex === -1) return;
74
+
75
+ const newOrder = [...subProjects];
76
+ const [moved] = newOrder.splice(oldIndex, 1);
77
+ newOrder.splice(newIndex, 0, moved);
78
+ onReorderSubs(newOrder.map(sp => sp.id));
79
+ };
80
+
44
81
  const toggleCollapse = (subId: string) => {
45
82
  setCollapsedSubs(prev => {
46
83
  const next = new Set(prev);
@@ -79,151 +116,236 @@ export default function ProjectTree({
79
116
  </div>
80
117
  )}
81
118
 
82
- {subProjects.map((sp) => {
83
- const isSelected = selectedSubId === sp.id;
84
- const isCollapsed = collapsedSubs.has(sp.id);
85
- const subTasks = isSelected ? tasks : [];
86
-
87
- return (
88
- <div key={sp.id} className="mb-0.5">
89
- {/* Sub-project node */}
90
- <div
91
- onClick={() => {
92
- onSelectSub(sp.id);
93
- if (isCollapsed) toggleCollapse(sp.id);
119
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
120
+ <SortableContext items={subProjects.map(sp => sp.id)} strategy={verticalListSortingStrategy}>
121
+ {subProjects.map((sp) => (
122
+ <SortableSubProject
123
+ key={sp.id}
124
+ sp={sp}
125
+ isSelected={selectedSubId === sp.id}
126
+ isCollapsed={collapsedSubs.has(sp.id)}
127
+ tasks={selectedSubId === sp.id ? tasks : []}
128
+ selectedTaskId={selectedTaskId}
129
+ addingTaskFor={addingTaskFor}
130
+ newTaskTitle={newTaskTitle}
131
+ onSelectSub={onSelectSub}
132
+ onSelectTask={onSelectTask}
133
+ onToggleCollapse={toggleCollapse}
134
+ onDeleteSub={onDeleteSub}
135
+ onStatusChange={onStatusChange}
136
+ onTodayToggle={onTodayToggle}
137
+ onDeleteTask={onDeleteTask}
138
+ onAddTask={handleAddTask}
139
+ onSetAddingTaskFor={setAddingTaskFor}
140
+ onSetNewTaskTitle={setNewTaskTitle}
141
+ />
142
+ ))}
143
+ </SortableContext>
144
+ </DndContext>
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ function SortableSubProject({
151
+ sp,
152
+ isSelected,
153
+ isCollapsed,
154
+ tasks: subTasks,
155
+ selectedTaskId,
156
+ addingTaskFor,
157
+ newTaskTitle,
158
+ onSelectSub,
159
+ onSelectTask,
160
+ onToggleCollapse,
161
+ onDeleteSub,
162
+ onStatusChange,
163
+ onTodayToggle,
164
+ onDeleteTask,
165
+ onAddTask,
166
+ onSetAddingTaskFor,
167
+ onSetNewTaskTitle,
168
+ }: {
169
+ sp: ISubProjectWithStats;
170
+ isSelected: boolean;
171
+ isCollapsed: boolean;
172
+ tasks: ITask[];
173
+ selectedTaskId: string | null;
174
+ addingTaskFor: string | null;
175
+ newTaskTitle: string;
176
+ onSelectSub: (subId: string) => void;
177
+ onSelectTask: (taskId: string) => void;
178
+ onToggleCollapse: (subId: string) => void;
179
+ onDeleteSub: (subId: string) => void;
180
+ onStatusChange: (taskId: string, status: TaskStatus) => void;
181
+ onTodayToggle: (taskId: string, isToday: boolean) => void;
182
+ onDeleteTask: (taskId: string) => void;
183
+ onAddTask: (subId: string) => void;
184
+ onSetAddingTaskFor: (subId: string | null) => void;
185
+ onSetNewTaskTitle: (title: string) => void;
186
+ }) {
187
+ const {
188
+ attributes,
189
+ listeners,
190
+ setNodeRef,
191
+ transform,
192
+ transition,
193
+ isDragging,
194
+ } = useSortable({ id: sp.id });
195
+
196
+ const style = {
197
+ transform: CSS.Transform.toString(transform),
198
+ transition,
199
+ opacity: isDragging ? 0.5 : 1,
200
+ zIndex: isDragging ? 10 : undefined,
201
+ };
202
+
203
+ return (
204
+ <div ref={setNodeRef} style={style} className="mb-0.5">
205
+ {/* Sub-project node */}
206
+ <div
207
+ onClick={() => {
208
+ onSelectSub(sp.id);
209
+ if (isCollapsed) onToggleCollapse(sp.id);
210
+ }}
211
+ className={`flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition-colors group text-sm ${
212
+ isSelected
213
+ ? 'text-foreground'
214
+ : 'text-muted-foreground hover:text-foreground'
215
+ }`}
216
+ >
217
+ {/* Drag handle */}
218
+ <span
219
+ {...attributes}
220
+ {...listeners}
221
+ className="w-4 h-4 flex items-center justify-center text-xs text-muted-foreground/40 hover:text-muted-foreground cursor-grab active:cursor-grabbing flex-shrink-0"
222
+ title="Drag to reorder"
223
+ onClick={(e) => e.stopPropagation()}
224
+ >
225
+
226
+ </span>
227
+ <button
228
+ onClick={(e) => { e.stopPropagation(); onToggleCollapse(sp.id); }}
229
+ className="w-4 h-4 flex items-center justify-center text-xs text-muted-foreground flex-shrink-0"
230
+ >
231
+ {isCollapsed ? '\u25B6' : '\u25BC'}
232
+ </button>
233
+ <span className={`flex-1 truncate font-medium ${isSelected ? 'text-primary' : ''}`}>
234
+ {sp.name}
235
+ </span>
236
+ <div className="flex items-center gap-1.5">
237
+ {sp.task_count > 0 && (
238
+ <span className="text-xs text-muted-foreground tabular-nums">{sp.task_count}</span>
239
+ )}
240
+ <button
241
+ onClick={(e) => { e.stopPropagation(); onDeleteSub(sp.id); }}
242
+ className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity text-xs"
243
+ >
244
+ x
245
+ </button>
246
+ </div>
247
+ </div>
248
+
249
+ {/* Tasks (children) */}
250
+ {!isCollapsed && isSelected && (
251
+ <div className="ml-3 border-l border-border/50">
252
+ {subTasks.length === 0 && !addingTaskFor && (
253
+ <div className="text-xs text-muted-foreground py-2 pl-4">
254
+ No tasks
255
+ </div>
256
+ )}
257
+ {subTasks.map((task) => (
258
+ <div
259
+ key={task.id}
260
+ onClick={() => onSelectTask(task.id)}
261
+ className={`group/task flex items-center gap-1.5 pl-4 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
262
+ selectedTaskId === task.id
263
+ ? 'bg-card-hover border-l-primary'
264
+ : 'border-l-transparent hover:bg-card-hover/50'
265
+ }`}
266
+ >
267
+ <button
268
+ onClick={(e) => {
269
+ e.stopPropagation();
270
+ const nextStatus = getNextStatus(task.status);
271
+ onStatusChange(task.id, nextStatus);
94
272
  }}
95
- className={`flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition-colors group text-sm ${
96
- isSelected
97
- ? 'text-foreground'
98
- : 'text-muted-foreground hover:text-foreground'
99
- }`}
273
+ className="flex-shrink-0 text-sm"
274
+ title={`Status: ${task.status}`}
100
275
  >
276
+ {statusIcon(task.status)}
277
+ </button>
278
+ <span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
279
+ <span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
280
+ {task.title}
281
+ </span>
282
+ {task.is_today && (
101
283
  <button
102
- onClick={(e) => { e.stopPropagation(); toggleCollapse(sp.id); }}
103
- className="w-4 h-4 flex items-center justify-center text-xs text-muted-foreground flex-shrink-0"
284
+ onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }}
285
+ className="text-xs flex-shrink-0 text-primary" title="Remove from today"
104
286
  >
105
- {isCollapsed ? '\u25B6' : '\u25BC'}
287
+ *
106
288
  </button>
107
- <span className={`flex-1 truncate font-medium ${isSelected ? 'text-primary' : ''}`}>
108
- {sp.name}
109
- </span>
110
- <div className="flex items-center gap-1.5">
111
- {sp.task_count > 0 && (
112
- <span className="text-xs text-muted-foreground tabular-nums">{sp.task_count}</span>
113
- )}
114
- <button
115
- onClick={(e) => { e.stopPropagation(); onDeleteSub(sp.id); }}
116
- className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity text-xs"
117
- >
118
- x
119
- </button>
120
- </div>
121
- </div>
122
-
123
- {/* Tasks (children) */}
124
- {!isCollapsed && isSelected && (
125
- <div className="ml-3 border-l border-border/50">
126
- {subTasks.length === 0 && !addingTaskFor && (
127
- <div className="text-xs text-muted-foreground py-2 pl-4">
128
- No tasks
129
- </div>
130
- )}
131
- {subTasks.map((task) => (
132
- <div
133
- key={task.id}
134
- onClick={() => onSelectTask(task.id)}
135
- className={`group/task flex items-center gap-1.5 pl-4 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
136
- selectedTaskId === task.id
137
- ? 'bg-card-hover border-l-primary'
138
- : 'border-l-transparent hover:bg-card-hover/50'
139
- }`}
140
- >
141
- <button
142
- onClick={(e) => {
143
- e.stopPropagation();
144
- const nextStatus = getNextStatus(task.status);
145
- onStatusChange(task.id, nextStatus);
146
- }}
147
- className="flex-shrink-0 text-sm"
148
- title={`Status: ${task.status}`}
149
- >
150
- {statusIcon(task.status)}
151
- </button>
152
- <span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
153
- <span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
154
- {task.title}
155
- </span>
156
- {task.is_today && (
157
- <button
158
- onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }}
159
- className="text-xs flex-shrink-0 text-primary" title="Remove from today"
160
- >
161
- *
162
- </button>
163
- )}
164
- <button
165
- onClick={(e) => {
166
- e.stopPropagation();
167
- onDeleteTask(task.id);
168
- }}
169
- className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground
170
- hover:!text-destructive transition-colors text-xs px-0.5"
171
- title="Delete task"
172
- >
173
- ×
174
- </button>
175
- </div>
176
- ))}
177
-
178
- {/* Add task input */}
179
- {addingTaskFor === sp.id ? (
180
- <div className="pl-4 pr-2 py-1">
181
- <input
182
- type="text"
183
- value={newTaskTitle}
184
- onChange={(e) => setNewTaskTitle(e.target.value)}
185
- onKeyDown={(e) => {
186
- if (e.key === 'Enter') handleAddTask(sp.id);
187
- if (e.key === 'Escape') { setNewTaskTitle(''); setAddingTaskFor(null); }
188
- }}
189
- placeholder="Task title..."
190
- className="w-full bg-input border border-border rounded px-2 py-1 text-sm
191
- focus:border-primary focus:outline-none text-foreground"
192
- autoFocus
193
- />
194
- </div>
195
- ) : (
196
- <button
197
- data-add-task
198
- onClick={() => { onSelectSub(sp.id); setAddingTaskFor(sp.id); }}
199
- className="pl-4 pr-2 py-1 text-xs text-muted-foreground hover:text-foreground
200
- transition-colors text-left w-full"
201
- >
202
- + Add task <span className="text-muted-foreground/50 ml-1">T</span>
203
- </button>
204
- )}
205
- </div>
206
289
  )}
290
+ <button
291
+ onClick={(e) => {
292
+ e.stopPropagation();
293
+ onDeleteTask(task.id);
294
+ }}
295
+ className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground
296
+ hover:!text-destructive transition-colors text-xs px-0.5"
297
+ title="Delete task"
298
+ >
299
+ ×
300
+ </button>
301
+ </div>
302
+ ))}
207
303
 
208
- {/* Show task previews for non-selected sub-projects */}
209
- {!isCollapsed && !isSelected && sp.preview_tasks && sp.preview_tasks.length > 0 && (
210
- <div className="ml-3 border-l border-border/50">
211
- {sp.preview_tasks.map((pt, i) => (
212
- <div
213
- key={i}
214
- onClick={() => onSelectSub(sp.id)}
215
- className="flex items-center gap-1.5 pl-4 pr-2 py-1 cursor-pointer text-xs text-muted-foreground hover:text-foreground transition-colors"
216
- >
217
- <span className="flex-shrink-0">{statusIcon(pt.status)}</span>
218
- <span className="truncate">{pt.title}</span>
219
- </div>
220
- ))}
221
- </div>
222
- )}
304
+ {/* Add task input */}
305
+ {addingTaskFor === sp.id ? (
306
+ <div className="pl-4 pr-2 py-1">
307
+ <input
308
+ type="text"
309
+ value={newTaskTitle}
310
+ onChange={(e) => onSetNewTaskTitle(e.target.value)}
311
+ onKeyDown={(e) => {
312
+ if (e.key === 'Enter') onAddTask(sp.id);
313
+ if (e.key === 'Escape') { onSetNewTaskTitle(''); onSetAddingTaskFor(null); }
314
+ }}
315
+ placeholder="Task title..."
316
+ className="w-full bg-input border border-border rounded px-2 py-1 text-sm
317
+ focus:border-primary focus:outline-none text-foreground"
318
+ autoFocus
319
+ />
223
320
  </div>
224
- );
225
- })}
226
- </div>
321
+ ) : (
322
+ <button
323
+ data-add-task
324
+ onClick={() => { onSelectSub(sp.id); onSetAddingTaskFor(sp.id); }}
325
+ className="pl-4 pr-2 py-1 text-xs text-muted-foreground hover:text-foreground
326
+ transition-colors text-left w-full"
327
+ >
328
+ + Add task <span className="text-muted-foreground/50 ml-1">T</span>
329
+ </button>
330
+ )}
331
+ </div>
332
+ )}
333
+
334
+ {/* Show task previews for non-selected sub-projects */}
335
+ {!isCollapsed && !isSelected && sp.preview_tasks && sp.preview_tasks.length > 0 && (
336
+ <div className="ml-3 border-l border-border/50">
337
+ {sp.preview_tasks.map((pt, i) => (
338
+ <div
339
+ key={i}
340
+ onClick={() => onSelectSub(sp.id)}
341
+ className="flex items-center gap-1.5 pl-4 pr-2 py-1 cursor-pointer text-xs text-muted-foreground hover:text-foreground transition-colors"
342
+ >
343
+ <span className="flex-shrink-0">{statusIcon(pt.status)}</span>
344
+ <span className="truncate">{pt.title}</span>
345
+ </div>
346
+ ))}
347
+ </div>
348
+ )}
227
349
  </div>
228
350
  );
229
351
  }
@@ -160,9 +160,9 @@ export default function TaskDetail({
160
160
  onBlur={saveDescription}
161
161
  placeholder="Background, conditions, notes..."
162
162
  className="w-full bg-input border border-border rounded-lg px-3 py-2.5 text-sm
163
- focus:border-primary focus:outline-none text-foreground resize-none min-h-[60px]
163
+ focus:border-primary focus:outline-none text-foreground resize-y min-h-[60px]
164
164
  leading-relaxed"
165
- rows={2}
165
+ rows={3}
166
166
  />
167
167
  </div>
168
168
 
@@ -78,7 +78,7 @@ export default function WorkspacePanel({
78
78
  const handleMouseMove = (e: MouseEvent) => {
79
79
  if (!draggingRef.current) return;
80
80
  const delta = e.clientX - startXRef.current;
81
- const newWidth = Math.max(180, Math.min(500, startWidthRef.current + delta));
81
+ const newWidth = Math.max(180, Math.min(900, startWidthRef.current + delta));
82
82
  if (draggingRef.current === 'left') setLeftWidth(newWidth);
83
83
  else setCenterWidth(newWidth);
84
84
  };
@@ -217,6 +217,20 @@ export default function WorkspacePanel({
217
217
  }
218
218
  };
219
219
 
220
+ const handleReorderSubs = async (orderedIds: string[]) => {
221
+ // Optimistic update
222
+ setSubProjects(prev => {
223
+ const map = new Map(prev.map(sp => [sp.id, sp]));
224
+ return orderedIds.map(id => map.get(id)!).filter(Boolean);
225
+ });
226
+
227
+ await fetch(`/api/projects/${id}/sub-projects`, {
228
+ method: 'PUT',
229
+ headers: { 'Content-Type': 'application/json' },
230
+ body: JSON.stringify({ orderedIds }),
231
+ });
232
+ };
233
+
220
234
  const handleTaskDelete = (taskId?: string) => {
221
235
  const tid = taskId || selectedTaskId;
222
236
  if (!tid) return;
@@ -403,7 +417,8 @@ export default function WorkspacePanel({
403
417
  onSelectTask={setSelectedTaskId}
404
418
  onCreateSub={() => setShowAddSub(true)} onDeleteSub={handleDeleteSubProject}
405
419
  onCreateTask={handleCreateTask} onStatusChange={handleTaskStatusChange}
406
- onTodayToggle={handleTaskTodayToggle} onDeleteTask={handleTaskDelete} />
420
+ onTodayToggle={handleTaskTodayToggle} onDeleteTask={handleTaskDelete}
421
+ onReorderSubs={handleReorderSubs} />
407
422
  </div>
408
423
 
409
424
  <div className="panel-resize-handle" onMouseDown={(e) => handleMouseDown('center', e)}>