idea-manager 1.5.2 → 1.6.1

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 (149) hide show
  1. package/.next/build-manifest.json +2 -2
  2. package/.next/routes-manifest.json +10 -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.js +34 -4
  22. package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/global-memo/route.js +34 -4
  26. package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/projects/[id]/apply-distribute/route.js +6 -82
  29. package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/projects/[id]/auto-distribute/route.js +5 -5
  31. package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/projects/[id]/brainstorm/route.js +1 -77
  33. package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/projects/[id]/git-sync/route.js +1 -77
  35. package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/projects/[id]/route.js +1 -77
  37. package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +37 -7
  39. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.js +15 -10
  41. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.js +34 -4
  43. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.js +26 -0
  45. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -0
  46. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.js +34 -4
  47. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route.js +34 -4
  49. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/projects/[id]/sub-projects/route.js +37 -7
  51. package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
  52. package/.next/server/app/api/projects/route.js +1 -77
  53. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/sync/route.js +34 -4
  55. package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/index.html +2 -2
  57. package/.next/server/app/index.rsc +3 -3
  58. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  59. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  60. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  61. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  62. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/page.js +15 -6
  64. package/.next/server/app/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app-paths-manifest.json +8 -7
  67. package/.next/server/chunks/117.js +107 -0
  68. package/.next/server/pages/404.html +2 -2
  69. package/.next/server/pages/500.html +2 -2
  70. package/.next/static/chunks/363642f4-9eb39e0bc542c65b.js +1 -0
  71. package/.next/static/chunks/374-23189d7e246ad164.js +1 -0
  72. package/.next/static/chunks/app/_global-error/page-6ec0e723e471f87a.js +1 -0
  73. package/.next/static/chunks/app/api/archive/route-6ec0e723e471f87a.js +1 -0
  74. package/.next/static/chunks/app/api/filesystem/route-6ec0e723e471f87a.js +1 -0
  75. package/.next/static/chunks/app/api/filesystem/tree/route-6ec0e723e471f87a.js +1 -0
  76. package/.next/static/chunks/app/api/global-memo/route-6ec0e723e471f87a.js +1 -0
  77. package/.next/static/chunks/app/api/health/route-6ec0e723e471f87a.js +1 -0
  78. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-6ec0e723e471f87a.js +1 -0
  79. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-6ec0e723e471f87a.js +1 -0
  80. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-6ec0e723e471f87a.js +1 -0
  81. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-6ec0e723e471f87a.js +1 -0
  82. package/.next/static/chunks/app/api/projects/[id]/route-6ec0e723e471f87a.js +1 -0
  83. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-6ec0e723e471f87a.js +1 -0
  84. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-6ec0e723e471f87a.js +1 -0
  85. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-6ec0e723e471f87a.js +1 -0
  86. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-6ec0e723e471f87a.js +1 -0
  87. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-6ec0e723e471f87a.js +1 -0
  88. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-6ec0e723e471f87a.js +1 -0
  89. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-6ec0e723e471f87a.js +1 -0
  90. package/.next/static/chunks/app/api/projects/route-6ec0e723e471f87a.js +1 -0
  91. package/.next/static/chunks/app/api/sync/route-6ec0e723e471f87a.js +1 -0
  92. package/.next/static/chunks/app/page-6a511af64da7531f.js +28 -0
  93. package/.next/static/chunks/next/dist/client/components/builtin/app-error-6ec0e723e471f87a.js +1 -0
  94. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-6ec0e723e471f87a.js +1 -0
  95. package/.next/static/chunks/next/dist/client/components/builtin/not-found-6ec0e723e471f87a.js +1 -0
  96. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-6ec0e723e471f87a.js +1 -0
  97. package/.next/static/css/cc32379d0efa7d1d.css +3 -0
  98. package/README.ja.md +4 -1
  99. package/README.ko.md +36 -6
  100. package/README.md +31 -6
  101. package/README.zh.md +4 -1
  102. package/package.json +11 -6
  103. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +9 -5
  104. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.ts +76 -0
  105. package/src/components/dashboard/DashboardPanel.tsx +1 -1
  106. package/src/components/dashboard/SubProjectCard.tsx +1 -0
  107. package/src/components/task/CommandPalette.tsx +137 -0
  108. package/src/components/task/NoteEditor.tsx +411 -0
  109. package/src/components/task/ProjectTree.tsx +1 -1
  110. package/src/components/task/StatusFlow.tsx +43 -20
  111. package/src/components/task/TaskChat.tsx +7 -7
  112. package/src/components/task/TaskDetail.tsx +270 -89
  113. package/src/components/task/TaskList.tsx +1 -1
  114. package/src/components/workspace/WorkspacePanel.tsx +8 -3
  115. package/src/lib/ai/agents.ts +3 -3
  116. package/src/lib/ai/client.ts +3 -1
  117. package/src/lib/db/queries/sub-projects.ts +3 -3
  118. package/src/lib/db/queries/tasks.ts +1 -1
  119. package/src/lib/db/schema.ts +60 -1
  120. package/src/types/index.ts +3 -1
  121. package/.next/server/chunks/806.js +0 -77
  122. package/.next/static/chunks/151-332d463cd8bd4db6.js +0 -1
  123. package/.next/static/chunks/app/_global-error/page-fd75b71b49e9729e.js +0 -1
  124. package/.next/static/chunks/app/api/archive/route-fd75b71b49e9729e.js +0 -1
  125. package/.next/static/chunks/app/api/filesystem/route-fd75b71b49e9729e.js +0 -1
  126. package/.next/static/chunks/app/api/filesystem/tree/route-fd75b71b49e9729e.js +0 -1
  127. package/.next/static/chunks/app/api/global-memo/route-fd75b71b49e9729e.js +0 -1
  128. package/.next/static/chunks/app/api/health/route-fd75b71b49e9729e.js +0 -1
  129. package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-fd75b71b49e9729e.js +0 -1
  130. package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-fd75b71b49e9729e.js +0 -1
  131. package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-fd75b71b49e9729e.js +0 -1
  132. package/.next/static/chunks/app/api/projects/[id]/git-sync/route-fd75b71b49e9729e.js +0 -1
  133. package/.next/static/chunks/app/api/projects/[id]/route-fd75b71b49e9729e.js +0 -1
  134. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-fd75b71b49e9729e.js +0 -1
  135. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-fd75b71b49e9729e.js +0 -1
  136. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-fd75b71b49e9729e.js +0 -1
  137. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-fd75b71b49e9729e.js +0 -1
  138. package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-fd75b71b49e9729e.js +0 -1
  139. package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-fd75b71b49e9729e.js +0 -1
  140. package/.next/static/chunks/app/api/projects/route-fd75b71b49e9729e.js +0 -1
  141. package/.next/static/chunks/app/api/sync/route-fd75b71b49e9729e.js +0 -1
  142. package/.next/static/chunks/app/page-d0d563bda0034c18.js +0 -19
  143. package/.next/static/chunks/next/dist/client/components/builtin/app-error-fd75b71b49e9729e.js +0 -1
  144. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-fd75b71b49e9729e.js +0 -1
  145. package/.next/static/chunks/next/dist/client/components/builtin/not-found-fd75b71b49e9729e.js +0 -1
  146. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-fd75b71b49e9729e.js +0 -1
  147. package/.next/static/css/22a3bf63fb41db4f.css +0 -3
  148. /package/.next/static/{VX2xgzKvKv0NCoI2qh_rr → eQXRVHrJt1cKjgp4hKYm8}/_buildManifest.js +0 -0
  149. /package/.next/static/{VX2xgzKvKv0NCoI2qh_rr → eQXRVHrJt1cKjgp4hKYm8}/_ssgManifest.js +0 -0
@@ -0,0 +1,411 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, useMemo, useRef } from 'react';
4
+ import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
5
+ import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
6
+ import { EditorView, Decoration, keymap, ViewPlugin, WidgetType } from '@codemirror/view';
7
+ import { EditorState, StateEffect, StateField, Prec } from '@codemirror/state';
8
+ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
9
+ import { tags as t } from '@lezer/highlight';
10
+
11
+ // ─────────────────────────────────────────────────────────────
12
+ // Ghost-text state: a decoration widget after the cursor.
13
+ // ─────────────────────────────────────────────────────────────
14
+ const setGhost = StateEffect.define<{ from: number; text: string } | null>();
15
+
16
+ class GhostWidget extends WidgetType {
17
+ constructor(public readonly text: string) { super(); }
18
+ eq(other: GhostWidget) { return other.text === this.text; }
19
+ toDOM() {
20
+ const span = document.createElement('span');
21
+ span.className = 'cm-ghost-text';
22
+ span.textContent = this.text;
23
+ span.style.opacity = '0.35';
24
+ span.style.pointerEvents = 'none';
25
+ return span;
26
+ }
27
+ ignoreEvent() { return true; }
28
+ }
29
+
30
+ const ghostField = StateField.define<{ from: number; text: string } | null>({
31
+ create: () => null,
32
+ update(value, tr) {
33
+ for (const e of tr.effects) {
34
+ if (e.is(setGhost)) return e.value;
35
+ }
36
+ // Any doc change or selection move clears ghost unless effect explicitly set it
37
+ if (tr.docChanged || tr.selection) return null;
38
+ return value;
39
+ },
40
+ });
41
+
42
+ const ghostDecorations = EditorView.decorations.compute([ghostField], (state) => {
43
+ const g = state.field(ghostField);
44
+ if (!g) return Decoration.none;
45
+ return Decoration.set([
46
+ Decoration.widget({ widget: new GhostWidget(g.text), side: 1 }).range(g.from),
47
+ ]);
48
+ });
49
+
50
+ // ─────────────────────────────────────────────────────────────
51
+ // Local autocomplete: suggest multi-word phrases sourced from the
52
+ // current doc + an optional wider corpus (sibling tasks, brainstorm).
53
+ // Phrases are segmented by punctuation; longer repeated n-grams win
54
+ // over single-word matches when they appear often.
55
+ // ─────────────────────────────────────────────────────────────
56
+ const CURRENT_DOC_WEIGHT = 3;
57
+ const MAX_PHRASE_LEN = 3;
58
+ const MAX_GHOST_CHARS = 40;
59
+ const TOKEN_MIN_LEN = 2;
60
+ const SEGMENT_SPLIT = /[.!?,;:·\n\r()[\]{}"]+/;
61
+
62
+ interface Segment { tokens: string[]; weight: number }
63
+
64
+ function tokenizeSegments(text: string): string[][] {
65
+ const out: string[][] = [];
66
+ for (const raw of text.split(SEGMENT_SPLIT)) {
67
+ const tokens = raw.trim().split(/\s+/).filter(t => t.length >= TOKEN_MIN_LEN);
68
+ if (tokens.length) out.push(tokens);
69
+ }
70
+ return out;
71
+ }
72
+
73
+ // Cache extra-corpus segmentation by array identity to avoid re-splitting
74
+ // a potentially large text blob on every keystroke.
75
+ let cachedExtraRef: string[] | null = null;
76
+ let cachedExtraSegs: Segment[] = [];
77
+ function getExtraSegments(extraCorpus: string[]): Segment[] {
78
+ if (extraCorpus === cachedExtraRef) return cachedExtraSegs;
79
+ const segs: Segment[] = [];
80
+ for (const text of extraCorpus) {
81
+ for (const toks of tokenizeSegments(text)) segs.push({ tokens: toks, weight: 1 });
82
+ }
83
+ cachedExtraRef = extraCorpus;
84
+ cachedExtraSegs = segs;
85
+ return segs;
86
+ }
87
+
88
+ function buildSegments(doc: string, extraCorpus: string[]): Segment[] {
89
+ const out: Segment[] = [];
90
+ for (const toks of tokenizeSegments(doc)) out.push({ tokens: toks, weight: CURRENT_DOC_WEIGHT });
91
+ return out.concat(getExtraSegments(extraCorpus));
92
+ }
93
+
94
+ function caretPrefix(state: EditorState): { word: string; from: number } | null {
95
+ const pos = state.selection.main.head;
96
+ const line = state.doc.lineAt(pos);
97
+ const col = pos - line.from;
98
+ const before = line.text.slice(0, col);
99
+ const after = line.text.slice(col);
100
+ // Don't suggest when the caret sits inside a word — would duplicate the
101
+ // trailing part (e.g. "안녕하|세요" + ghost "세요" → "안녕하세요세요").
102
+ if (/^[A-Za-z가-힣\w_-]/.test(after)) return null;
103
+ const m = before.match(/([A-Za-z가-힣][\w가-힣_-]*)$/);
104
+ if (!m) return null;
105
+ return { word: m[1], from: pos - m[1].length };
106
+ }
107
+
108
+ // Gather vocabulary already present in the current doc. Segments from the
109
+ // extra corpus that share vocabulary with this set are considered "topically
110
+ // related" and receive a score boost — so if the user is writing about Pods,
111
+ // "Pod"-adjacent completions win over unrelated ones.
112
+ function buildContextVocabulary(doc: string): Set<string> {
113
+ const set = new Set<string>();
114
+ const matches = doc.toLowerCase().match(/[a-z가-힣][\w가-힣_-]{1,}/g);
115
+ if (!matches) return set;
116
+ for (const w of matches) {
117
+ if (w.length >= TOKEN_MIN_LEN) set.add(w);
118
+ }
119
+ return set;
120
+ }
121
+
122
+ function computeLocalGhost(
123
+ state: EditorState,
124
+ extraCorpus: string[],
125
+ ): { from: number; text: string } | null {
126
+ const ctx = caretPrefix(state);
127
+ if (!ctx || ctx.word.length < 2) return null;
128
+ const doc = state.doc.toString();
129
+ const segments = buildSegments(doc, extraCorpus);
130
+ const prefix = ctx.word.toLowerCase();
131
+ const contextVocab = buildContextVocabulary(doc);
132
+
133
+ // Tally candidate completions by the full tail text that would be inserted.
134
+ // Longer phrases get a small length bonus so "Claude Code" beats "Claude"
135
+ // when both appear with equal frequency. Segments that share vocabulary
136
+ // with the current doc get a relevance boost (up to 2×) so topically
137
+ // related completions surface first.
138
+ const scores = new Map<string, number>();
139
+ for (const seg of segments) {
140
+ let overlap = 0;
141
+ for (const tk of seg.tokens) {
142
+ if (contextVocab.has(tk.toLowerCase())) overlap++;
143
+ }
144
+ const relevance = 1 + Math.min(overlap * 0.25, 1.0);
145
+
146
+ for (let i = 0; i < seg.tokens.length; i++) {
147
+ const first = seg.tokens[i];
148
+ if (first.length <= ctx.word.length) continue;
149
+ if (!first.toLowerCase().startsWith(prefix)) continue;
150
+ if (first === ctx.word) continue;
151
+
152
+ const maxExtra = Math.min(MAX_PHRASE_LEN - 1, seg.tokens.length - i - 1);
153
+ for (let n = 0; n <= maxExtra; n++) {
154
+ const tail = n === 0
155
+ ? first.slice(ctx.word.length)
156
+ : `${first.slice(ctx.word.length)} ${seg.tokens.slice(i + 1, i + 1 + n).join(' ')}`;
157
+ if (tail.length > MAX_GHOST_CHARS) break;
158
+ const lengthBonus = 1 + n * 0.5;
159
+ scores.set(tail, (scores.get(tail) ?? 0) + seg.weight * lengthBonus * relevance);
160
+ }
161
+ }
162
+ }
163
+
164
+ if (!scores.size) return null;
165
+ // On exact tie, prefer later-inserted (longer) phrases using `>=`.
166
+ let best: { text: string; score: number } | null = null;
167
+ for (const [text, score] of scores) {
168
+ if (!best || score >= best.score) best = { text, score };
169
+ }
170
+ if (!best) return null;
171
+ return { from: state.selection.main.head, text: best.text };
172
+ }
173
+
174
+ function createLocalCompletionPlugin(corpusRef: { current: string[] }) {
175
+ return ViewPlugin.fromClass(class {
176
+ timer: ReturnType<typeof setTimeout> | null = null;
177
+ constructor(public view: EditorView) {}
178
+ update(u: { docChanged: boolean; selectionSet: boolean; state: EditorState; view: EditorView }) {
179
+ if (!u.docChanged && !u.selectionSet) return;
180
+ if (this.timer) clearTimeout(this.timer);
181
+ this.timer = setTimeout(() => {
182
+ const ghost = computeLocalGhost(u.view.state, corpusRef.current);
183
+ u.view.dispatch({ effects: setGhost.of(ghost) });
184
+ }, 120);
185
+ }
186
+ destroy() {
187
+ if (this.timer) clearTimeout(this.timer);
188
+ }
189
+ });
190
+ }
191
+
192
+ // Accept-ghost command — bound to Tab, only when ghost exists
193
+ function acceptGhost(view: EditorView): boolean {
194
+ const ghost = view.state.field(ghostField, false);
195
+ if (!ghost) return false;
196
+ view.dispatch({
197
+ changes: { from: ghost.from, insert: ghost.text },
198
+ selection: { anchor: ghost.from + ghost.text.length },
199
+ effects: setGhost.of(null),
200
+ });
201
+ return true;
202
+ }
203
+
204
+ function dismissGhost(view: EditorView): boolean {
205
+ const ghost = view.state.field(ghostField, false);
206
+ if (!ghost) return false;
207
+ view.dispatch({ effects: setGhost.of(null) });
208
+ return true;
209
+ }
210
+
211
+ // ─────────────────────────────────────────────────────────────
212
+ // Markdown list / checkbox continuation on Enter.
213
+ // ─────────────────────────────────────────────────────────────
214
+ function continueList(view: EditorView): boolean {
215
+ const { state } = view;
216
+ const pos = state.selection.main.head;
217
+ const line = state.doc.lineAt(pos);
218
+ // Only when caret is at end of line
219
+ if (pos !== line.to) return false;
220
+ const text = line.text;
221
+
222
+ const bullet = text.match(/^(\s*)([-*+])\s(\[[ xX]\]\s)?(.*)$/);
223
+ const ordered = text.match(/^(\s*)(\d+)\.\s(.*)$/);
224
+
225
+ if (bullet) {
226
+ const [, indent, mark, check, content] = bullet;
227
+ if (!content.trim()) {
228
+ // Empty bullet → exit list
229
+ view.dispatch({
230
+ changes: { from: line.from, to: line.to, insert: indent },
231
+ selection: { anchor: line.from + indent.length },
232
+ });
233
+ return true;
234
+ }
235
+ const prefix = check ? `${indent}${mark} [ ] ` : `${indent}${mark} `;
236
+ view.dispatch({
237
+ changes: { from: pos, insert: `\n${prefix}` },
238
+ selection: { anchor: pos + 1 + prefix.length },
239
+ });
240
+ return true;
241
+ }
242
+ if (ordered) {
243
+ const [, indent, numStr, content] = ordered;
244
+ const num = parseInt(numStr, 10);
245
+ if (!content.trim()) {
246
+ view.dispatch({
247
+ changes: { from: line.from, to: line.to, insert: indent },
248
+ selection: { anchor: line.from + indent.length },
249
+ });
250
+ return true;
251
+ }
252
+ const prefix = `${indent}${num + 1}. `;
253
+ view.dispatch({
254
+ changes: { from: pos, insert: `\n${prefix}` },
255
+ selection: { anchor: pos + 1 + prefix.length },
256
+ });
257
+ return true;
258
+ }
259
+ return false;
260
+ }
261
+
262
+ // ─────────────────────────────────────────────────────────────
263
+ // Markdown syntax highlighting — explicit Lezer-tag mapping so list
264
+ // marks, headings and inline code stand out clearly against plain
265
+ // text. `t.processingInstruction` covers ATX heading `#`s, list
266
+ // bullets (`-` `*` `+`), ordered list `1.`, quote `>`, emphasis `*`
267
+ // and link brackets — all of which we want to visually punctuate.
268
+ // ─────────────────────────────────────────────────────────────
269
+ const mdHighlight = HighlightStyle.define([
270
+ { tag: t.heading1, color: 'hsl(var(--foreground))', fontWeight: '800', fontSize: '1.35em' },
271
+ { tag: t.heading2, color: 'hsl(var(--foreground))', fontWeight: '700', fontSize: '1.2em' },
272
+ { tag: t.heading3, color: 'hsl(var(--foreground))', fontWeight: '700', fontSize: '1.08em' },
273
+ { tag: [t.heading4, t.heading5, t.heading6], color: 'hsl(var(--foreground))', fontWeight: '700' },
274
+ { tag: t.processingInstruction, color: 'hsl(var(--accent))', fontWeight: '700' },
275
+ { tag: t.list, color: 'hsl(var(--foreground))' },
276
+ { tag: t.emphasis, fontStyle: 'italic', color: 'hsl(var(--foreground))' },
277
+ { tag: t.strong, fontWeight: '700', color: 'hsl(var(--foreground))' },
278
+ { tag: [t.link, t.url], color: 'hsl(var(--primary))', textDecoration: 'underline' },
279
+ { tag: t.monospace, color: 'hsl(var(--warning))' },
280
+ { tag: t.quote, color: 'hsl(var(--muted-foreground))', fontStyle: 'italic' },
281
+ { tag: t.contentSeparator, color: 'hsl(var(--border))' },
282
+ { tag: t.strikethrough, textDecoration: 'line-through', color: 'hsl(var(--muted-foreground))' },
283
+ ]);
284
+
285
+ // ─────────────────────────────────────────────────────────────
286
+ // Component
287
+ // ─────────────────────────────────────────────────────────────
288
+ export interface NoteEditorProps {
289
+ value: string;
290
+ onChange: (v: string) => void;
291
+ onBlur?: () => void;
292
+ onOpenCommand?: () => void;
293
+ placeholder?: string;
294
+ /** Extra text blobs (sibling tasks, brainstorm, …) to widen the autocomplete corpus. */
295
+ extraCorpus?: string[];
296
+ }
297
+
298
+ const NoteEditor = forwardRef<ReactCodeMirrorRef, NoteEditorProps>(function NoteEditor(
299
+ { value, onChange, onBlur, onOpenCommand, placeholder, extraCorpus },
300
+ ref,
301
+ ) {
302
+ // Mutable ref keeps the plugin in sync with the latest corpus without
303
+ // rebuilding the extension list (which would re-init CodeMirror).
304
+ const corpusRef = useRef<string[]>(extraCorpus ?? []);
305
+ corpusRef.current = extraCorpus ?? [];
306
+
307
+ const extensions = useMemo(() => [
308
+ markdown({ base: markdownLanguage }),
309
+ syntaxHighlighting(mdHighlight),
310
+ ghostField,
311
+ ghostDecorations,
312
+ createLocalCompletionPlugin(corpusRef),
313
+ Prec.highest(keymap.of([
314
+ { key: 'Tab', run: acceptGhost },
315
+ { key: 'Escape', run: dismissGhost },
316
+ { key: 'Enter', run: continueList },
317
+ { key: 'Mod-k', run: () => { onOpenCommand?.(); return true; } },
318
+ ])),
319
+ EditorView.lineWrapping,
320
+ EditorView.theme({
321
+ '&': {
322
+ fontSize: '13px',
323
+ backgroundColor: 'transparent',
324
+ color: 'hsl(var(--foreground))',
325
+ height: '100%',
326
+ },
327
+ '.cm-editor': { backgroundColor: 'transparent' },
328
+ '&.cm-focused': { outline: 'none' },
329
+ '.cm-scroller': {
330
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
331
+ lineHeight: '1.7',
332
+ backgroundColor: 'transparent',
333
+ },
334
+ '.cm-content': {
335
+ padding: '12px 16px',
336
+ caretColor: 'hsl(var(--primary))',
337
+ color: 'hsl(var(--foreground))',
338
+ },
339
+ '.cm-gutters': { display: 'none' },
340
+ '.cm-activeLine': { backgroundColor: 'transparent' },
341
+ '.cm-activeLineGutter': { backgroundColor: 'transparent' },
342
+ '.cm-line': { backgroundColor: 'transparent' },
343
+ '.cm-selectionLayer .cm-selectionBackground, .cm-content ::selection, ::selection': {
344
+ backgroundColor: 'hsl(var(--primary) / 0.25)',
345
+ },
346
+ '&.cm-focused .cm-selectionBackground': {
347
+ backgroundColor: 'hsl(var(--primary) / 0.3)',
348
+ },
349
+ '.cm-cursor, .cm-dropCursor': {
350
+ borderLeftColor: 'hsl(var(--primary))',
351
+ borderLeftWidth: '2px',
352
+ },
353
+ '.cm-ghost-text': {
354
+ color: 'hsl(var(--muted-foreground))',
355
+ opacity: '0.55',
356
+ fontStyle: 'italic',
357
+ },
358
+ '.cm-placeholder': {
359
+ color: 'hsl(var(--muted-foreground) / 0.55)',
360
+ whiteSpace: 'normal',
361
+ display: 'inline-block',
362
+ maxWidth: '90%',
363
+ lineHeight: '1.6',
364
+ },
365
+ // Markdown syntax coloring (one-dark style tuned to IM palette)
366
+ '.tok-heading, .tok-heading1, .tok-heading2, .tok-heading3, .tok-heading4, .tok-heading5, .tok-heading6': {
367
+ color: 'hsl(var(--foreground))',
368
+ fontWeight: '700',
369
+ },
370
+ '.tok-emphasis': { fontStyle: 'italic', color: 'hsl(var(--foreground))' },
371
+ '.tok-strong': { fontWeight: '700', color: 'hsl(var(--foreground))' },
372
+ '.tok-link': { color: 'hsl(var(--primary))', textDecoration: 'underline' },
373
+ '.tok-url': { color: 'hsl(var(--primary))' },
374
+ '.tok-monospace, .tok-literal': {
375
+ color: 'hsl(var(--warning))',
376
+ backgroundColor: 'hsl(var(--muted) / 0.5)',
377
+ padding: '0 3px',
378
+ borderRadius: '3px',
379
+ },
380
+ '.tok-list': { color: 'hsl(var(--accent))' },
381
+ '.tok-quote': { color: 'hsl(var(--muted-foreground))', fontStyle: 'italic' },
382
+ '.tok-comment, .tok-meta': { color: 'hsl(var(--muted-foreground))' },
383
+ }, { dark: true }),
384
+ ], [onOpenCommand]);
385
+
386
+ return (
387
+ <div className="h-full w-full">
388
+ <CodeMirror
389
+ ref={ref}
390
+ value={value}
391
+ onChange={onChange}
392
+ onBlur={onBlur}
393
+ extensions={extensions}
394
+ theme="none"
395
+ basicSetup={{
396
+ lineNumbers: false,
397
+ foldGutter: false,
398
+ highlightActiveLine: false,
399
+ highlightActiveLineGutter: false,
400
+ autocompletion: false,
401
+ searchKeymap: false,
402
+ }}
403
+ placeholder={placeholder}
404
+ height="100%"
405
+ style={{ height: '100%' }}
406
+ />
407
+ </div>
408
+ );
409
+ });
410
+
411
+ export default NoteEditor;
@@ -456,7 +456,7 @@ function SortableSubProject({
456
456
  }
457
457
 
458
458
  function getNextStatus(current: TaskStatus): TaskStatus {
459
- const flow: TaskStatus[] = ['idea', 'writing', 'submitted', 'testing', 'done'];
459
+ const flow: TaskStatus[] = ['idea', 'doing', 'done'];
460
460
  const idx = flow.indexOf(current);
461
461
  if (idx === -1) return 'idea';
462
462
  return flow[(idx + 1) % flow.length];
@@ -1,16 +1,24 @@
1
1
  'use client';
2
2
 
3
3
  import type { TaskStatus } from '@/types';
4
+ import { ACTIVE_STATUSES, LEGACY_STATUSES } from '@/types';
4
5
 
5
- const STATUSES: { key: TaskStatus; label: string; icon: string; color: string }[] = [
6
- { key: 'idea', label: 'Idea', icon: '\u{1F4A1}', color: 'text-muted-foreground' },
7
- { key: 'writing', label: 'Writing', icon: '\u{270F}\u{FE0F}', color: 'text-warning' },
6
+ type StatusMeta = { key: TaskStatus; label: string; icon: string; color: string };
7
+
8
+ const ALL: StatusMeta[] = [
9
+ { key: 'idea', label: 'Idea', icon: '\u{1F4A1}', color: 'text-muted-foreground' },
10
+ { key: 'doing', label: 'Doing', icon: '\u{1F525}', color: 'text-primary' },
11
+ { key: 'writing', label: 'Writing', icon: '\u{270F}\u{FE0F}', color: 'text-warning' },
8
12
  { key: 'submitted', label: 'Submitted', icon: '\u{1F680}', color: 'text-primary' },
9
- { key: 'testing', label: 'Testing', icon: '\u{1F9EA}', color: 'text-accent' },
10
- { key: 'done', label: 'Done', icon: '\u{2705}', color: 'text-success' },
11
- { key: 'problem', label: 'Problem', icon: '\u{1F534}', color: 'text-destructive' },
13
+ { key: 'testing', label: 'Testing', icon: '\u{1F9EA}', color: 'text-accent' },
14
+ { key: 'done', label: 'Done', icon: '\u{2705}', color: 'text-success' },
15
+ { key: 'problem', label: 'Problem', icon: '\u{1F534}', color: 'text-destructive' },
12
16
  ];
13
17
 
18
+ function meta(key: TaskStatus): StatusMeta {
19
+ return ALL.find(s => s.key === key) ?? ALL[0];
20
+ }
21
+
14
22
  export default function StatusFlow({
15
23
  status,
16
24
  onChange,
@@ -18,26 +26,41 @@ export default function StatusFlow({
18
26
  status: TaskStatus;
19
27
  onChange: (status: TaskStatus) => void;
20
28
  }) {
29
+ const isLegacy = LEGACY_STATUSES.includes(status);
30
+ const current = meta(status);
31
+
21
32
  return (
22
33
  <div className="flex items-center gap-1">
23
- {STATUSES.map((s) => (
24
- <button
25
- key={s.key}
26
- onClick={() => onChange(s.key)}
27
- title={s.label}
28
- className={`px-2 py-1 rounded text-base transition-all ${
29
- status === s.key
30
- ? `${s.color} bg-muted scale-110`
31
- : 'opacity-40 hover:opacity-80'
32
- }`}
34
+ {isLegacy && (
35
+ <span
36
+ title={`Legacy: ${current.label} (클릭해서 새 상태로 이동)`}
37
+ className={`px-2 py-1 rounded text-xs ${current.color} bg-muted/50 border border-dashed border-muted-foreground/30 mr-1`}
33
38
  >
34
- {s.icon}
35
- </button>
36
- ))}
39
+ {current.icon} {current.label}
40
+ </span>
41
+ )}
42
+ {ACTIVE_STATUSES.map((key) => {
43
+ const s = meta(key);
44
+ const active = status === key;
45
+ return (
46
+ <button
47
+ key={key}
48
+ onClick={() => onChange(key)}
49
+ title={s.label}
50
+ className={`px-2 py-1 rounded text-base transition-all ${
51
+ active
52
+ ? `${s.color} bg-muted scale-110`
53
+ : 'opacity-40 hover:opacity-80'
54
+ }`}
55
+ >
56
+ {s.icon}
57
+ </button>
58
+ );
59
+ })}
37
60
  </div>
38
61
  );
39
62
  }
40
63
 
41
64
  export function statusIcon(status: TaskStatus): string {
42
- return STATUSES.find(s => s.key === status)?.icon ?? '';
65
+ return meta(status).icon;
43
66
  }
@@ -22,12 +22,12 @@ function notifyAiResponse(preview: string) {
22
22
  export default function TaskChat({
23
23
  basePath,
24
24
  taskStatus,
25
- onApplyToPrompt,
25
+ onInsertToNote,
26
26
  onChatStateChange,
27
27
  }: {
28
28
  basePath: string;
29
29
  taskStatus?: TaskStatus;
30
- onApplyToPrompt: (content: string) => void;
30
+ onInsertToNote: (content: string) => void;
31
31
  onChatStateChange?: (state: 'idle' | 'loading' | 'done') => void;
32
32
  }) {
33
33
  const [messages, setMessages] = useState<ITaskConversation[]>([]);
@@ -131,9 +131,9 @@ export default function TaskChat({
131
131
  };
132
132
 
133
133
  return (
134
- <div className="flex flex-col h-full border-t border-border">
134
+ <div className="flex flex-col h-full">
135
135
  <div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
136
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">AI Chat</span>
136
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Note Assistant</span>
137
137
  {taskStatus === 'testing' && (
138
138
  <span className="flex items-center gap-1.5 text-xs text-warning">
139
139
  <span className="inline-block w-2 h-2 rounded-full bg-warning animate-pulse" />
@@ -146,7 +146,7 @@ export default function TaskChat({
146
146
  <div className="flex-1 overflow-y-auto px-3 py-2 space-y-2 min-h-0">
147
147
  {messages.length === 0 && !loading && (
148
148
  <div className="text-sm text-muted-foreground text-center py-4">
149
- Ask AI to help refine your task or prompt
149
+ 노트 작성을 도와드립니다. 질문하거나 &quot;이 부분 정리해줘&quot; 같이 요청해보세요
150
150
  </div>
151
151
  )}
152
152
  {messages.filter(msg => msg.content).map((msg) => (
@@ -162,10 +162,10 @@ export default function TaskChat({
162
162
  </div>
163
163
  {msg.role === 'assistant' && (
164
164
  <button
165
- onClick={() => onApplyToPrompt(msg.content)}
165
+ onClick={() => onInsertToNote(msg.content)}
166
166
  className="text-xs text-muted-foreground hover:text-primary mt-0.5 px-1 transition-colors"
167
167
  >
168
- Apply to prompt
168
+ 노트에 삽입
169
169
  </button>
170
170
  )}
171
171
  </div>