idea-manager 1.5.1 → 1.6.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/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +5 -0
- package/.next/required-server-files.json +5 -0
- package/.next/routes-manifest.json +10 -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.js +34 -4
- 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.js +34 -4
- 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.js +6 -82
- 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.js +6 -6
- 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.js +1 -77
- 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.js +1 -77
- package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/route.js +1 -77
- 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.js +38 -8
- 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.js +15 -10
- 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.js +34 -4
- 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.js +26 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.js +34 -4
- 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.js +34 -4
- 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.js +38 -8
- package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route.js +1 -77
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sync/route.js +34 -4
- package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- 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 +15 -6
- 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 +9 -8
- package/.next/server/chunks/117.js +107 -0
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/363642f4-9eb39e0bc542c65b.js +1 -0
- package/.next/static/chunks/374-23189d7e246ad164.js +1 -0
- package/.next/static/chunks/app/_global-error/page-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/archive/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/filesystem/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/filesystem/tree/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/global-memo/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/health/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/projects/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/api/sync/route-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/app/page-6a511af64da7531f.js +28 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-6ec0e723e471f87a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-6ec0e723e471f87a.js +1 -0
- package/.next/static/css/cc32379d0efa7d1d.css +3 -0
- package/next.config.mjs +3 -0
- package/package.json +11 -6
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +9 -5
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.ts +76 -0
- package/src/components/dashboard/DashboardPanel.tsx +1 -1
- package/src/components/dashboard/SubProjectCard.tsx +1 -0
- package/src/components/task/CommandPalette.tsx +137 -0
- package/src/components/task/NoteEditor.tsx +411 -0
- package/src/components/task/ProjectTree.tsx +1 -1
- package/src/components/task/StatusFlow.tsx +43 -20
- package/src/components/task/TaskChat.tsx +7 -7
- package/src/components/task/TaskDetail.tsx +270 -89
- package/src/components/task/TaskList.tsx +1 -1
- package/src/components/workspace/WorkspacePanel.tsx +8 -3
- package/src/lib/ai/agents.ts +3 -3
- package/src/lib/ai/client.ts +3 -1
- package/src/lib/db/index.ts +4 -1
- package/src/lib/db/queries/sub-projects.ts +3 -3
- package/src/lib/db/queries/tasks.ts +1 -1
- package/src/lib/db/schema.ts +60 -1
- package/src/types/index.ts +3 -1
- package/.next/server/chunks/806.js +0 -77
- package/.next/static/chunks/151-332d463cd8bd4db6.js +0 -1
- package/.next/static/chunks/app/_global-error/page-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/archive/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/filesystem/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/filesystem/tree/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/global-memo/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/health/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/projects/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/api/sync/route-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/app/page-d0d563bda0034c18.js +0 -19
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-fd75b71b49e9729e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-fd75b71b49e9729e.js +0 -1
- package/.next/static/css/22a3bf63fb41db4f.css +0 -3
- /package/.next/static/{3dIOxF31xgLe9pGE0yrsa → 63zinfEtSLCdG9nUZ3W-E}/_buildManifest.js +0 -0
- /package/.next/static/{3dIOxF31xgLe9pGE0yrsa → 63zinfEtSLCdG9nUZ3W-E}/_ssgManifest.js +0 -0
|
@@ -456,7 +456,7 @@ function SortableSubProject({
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
function getNextStatus(current: TaskStatus): TaskStatus {
|
|
459
|
-
const flow: TaskStatus[] = ['idea', '
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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',
|
|
10
|
-
{ key: 'done',
|
|
11
|
-
{ key: 'problem',
|
|
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
|
-
{
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
{
|
|
35
|
-
</
|
|
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
|
|
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
|
-
|
|
25
|
+
onInsertToNote,
|
|
26
26
|
onChatStateChange,
|
|
27
27
|
}: {
|
|
28
28
|
basePath: string;
|
|
29
29
|
taskStatus?: TaskStatus;
|
|
30
|
-
|
|
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
|
|
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">
|
|
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
|
-
|
|
149
|
+
노트 작성을 도와드립니다. 질문하거나 "이 부분 정리해줘" 같이 요청해보세요
|
|
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={() =>
|
|
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
|
-
|
|
168
|
+
↓ 노트에 삽입
|
|
169
169
|
</button>
|
|
170
170
|
)}
|
|
171
171
|
</div>
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
4
4
|
import type { ITask, TaskStatus, ItemPriority } from '@/types';
|
|
5
5
|
import StatusFlow from './StatusFlow';
|
|
6
|
-
import PromptEditor from './PromptEditor';
|
|
7
6
|
import TaskChat from './TaskChat';
|
|
7
|
+
import NoteEditor from './NoteEditor';
|
|
8
|
+
import CommandPalette, { type RefineCommand } from './CommandPalette';
|
|
9
|
+
import type { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
|
8
10
|
|
|
9
11
|
export default function TaskDetail({
|
|
10
12
|
task,
|
|
11
13
|
projectId,
|
|
12
14
|
subProjectId,
|
|
15
|
+
siblingTasks,
|
|
13
16
|
onUpdate,
|
|
14
17
|
onDelete,
|
|
15
18
|
onChatStateChange,
|
|
@@ -17,28 +20,59 @@ export default function TaskDetail({
|
|
|
17
20
|
task: ITask;
|
|
18
21
|
projectId: string;
|
|
19
22
|
subProjectId: string;
|
|
23
|
+
/** Other tasks under the same sub-project — used to widen autocomplete corpus. */
|
|
24
|
+
siblingTasks?: ITask[];
|
|
20
25
|
onUpdate: (data: Partial<ITask>) => void;
|
|
21
26
|
onDelete: () => void;
|
|
22
27
|
onChatStateChange?: (taskId: string, state: 'idle' | 'loading' | 'done') => void;
|
|
23
28
|
}) {
|
|
24
29
|
const [title, setTitle] = useState(task.title);
|
|
25
30
|
const [description, setDescription] = useState(task.description);
|
|
26
|
-
const [promptContent, setPromptContent] = useState('');
|
|
27
|
-
const [refining, setRefining] = useState(false);
|
|
28
31
|
const [editingTitle, setEditingTitle] = useState(false);
|
|
29
|
-
const [
|
|
32
|
+
const [copied, setCopied] = useState(false);
|
|
33
|
+
const [chatOpen, setChatOpen] = useState(false);
|
|
34
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
35
|
+
const [refining, setRefining] = useState(false);
|
|
36
|
+
const [refineElapsed, setRefineElapsed] = useState(0);
|
|
37
|
+
const [refineError, setRefineError] = useState<string | null>(null);
|
|
38
|
+
const [hasSelection, setHasSelection] = useState(false);
|
|
39
|
+
const [undoSnapshot, setUndoSnapshot] = useState<{ taskId: string; doc: string } | null>(null);
|
|
40
|
+
const refineAbortRef = useRef<AbortController | null>(null);
|
|
30
41
|
|
|
31
42
|
const basePath = `/api/projects/${projectId}/sub-projects/${subProjectId}/tasks/${task.id}`;
|
|
32
|
-
const
|
|
43
|
+
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
|
44
|
+
const [brainstormText, setBrainstormText] = useState('');
|
|
45
|
+
|
|
46
|
+
// Fetch the project's brainstorm once per project — used as autocomplete corpus.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
let cancelled = false;
|
|
49
|
+
fetch(`/api/projects/${projectId}/brainstorm`)
|
|
50
|
+
.then(r => r.ok ? r.json() : null)
|
|
51
|
+
.then(data => {
|
|
52
|
+
if (cancelled) return;
|
|
53
|
+
setBrainstormText(typeof data?.content === 'string' ? data.content : '');
|
|
54
|
+
})
|
|
55
|
+
.catch(() => { /* silent — corpus is non-critical */ });
|
|
56
|
+
return () => { cancelled = true; };
|
|
57
|
+
}, [projectId]);
|
|
58
|
+
|
|
59
|
+
const extraCorpus = useMemo<string[]>(() => {
|
|
60
|
+
const parts: string[] = [];
|
|
61
|
+
if (siblingTasks) {
|
|
62
|
+
for (const t of siblingTasks) {
|
|
63
|
+
if (t.id === task.id) continue;
|
|
64
|
+
if (t.title) parts.push(t.title);
|
|
65
|
+
if (t.description) parts.push(t.description);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (brainstormText) parts.push(brainstormText);
|
|
69
|
+
return parts;
|
|
70
|
+
}, [siblingTasks, brainstormText, task.id]);
|
|
33
71
|
|
|
34
|
-
// Load prompt
|
|
35
72
|
useEffect(() => {
|
|
36
73
|
setTitle(task.title);
|
|
37
74
|
setDescription(task.description);
|
|
38
|
-
|
|
39
|
-
.then(r => r.json())
|
|
40
|
-
.then(data => setPromptContent(data.content || ''));
|
|
41
|
-
}, [task.id, task.title, task.description, basePath]);
|
|
75
|
+
}, [task.id, task.title, task.description]);
|
|
42
76
|
|
|
43
77
|
const saveTitle = useCallback(() => {
|
|
44
78
|
const trimmed = title.trim();
|
|
@@ -56,47 +90,152 @@ export default function TaskDetail({
|
|
|
56
90
|
}
|
|
57
91
|
}, [description, task.description, onUpdate]);
|
|
58
92
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
const insertIntoNote = useCallback((content: string) => {
|
|
94
|
+
const next = description
|
|
95
|
+
? `${description.trimEnd()}\n\n${content.trim()}\n`
|
|
96
|
+
: `${content.trim()}\n`;
|
|
97
|
+
setDescription(next);
|
|
98
|
+
onUpdate({ description: next });
|
|
99
|
+
}, [description, onUpdate]);
|
|
100
|
+
|
|
101
|
+
const copyAsPrompt = useCallback(async () => {
|
|
102
|
+
const body = description?.trim() || '(비어있음)';
|
|
103
|
+
const text = `# ${task.title}\n\n${body}\n`;
|
|
104
|
+
try {
|
|
105
|
+
await navigator.clipboard.writeText(text);
|
|
106
|
+
setCopied(true);
|
|
107
|
+
setTimeout(() => setCopied(false), 1500);
|
|
108
|
+
} catch { /* silent */ }
|
|
109
|
+
}, [task.title, description]);
|
|
110
|
+
|
|
111
|
+
// Command palette — captures selection from CM view and replaces/inserts
|
|
112
|
+
const openPalette = useCallback(() => {
|
|
113
|
+
const view = editorRef.current?.view;
|
|
114
|
+
if (view) {
|
|
115
|
+
const sel = view.state.selection.main;
|
|
116
|
+
setHasSelection(!sel.empty);
|
|
117
|
+
}
|
|
118
|
+
setPaletteOpen(true);
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const runRefine = useCallback(async (cmd: RefineCommand, customText?: string) => {
|
|
122
|
+
const view = editorRef.current?.view;
|
|
123
|
+
if (!view) { setPaletteOpen(false); return; }
|
|
67
124
|
|
|
68
|
-
|
|
125
|
+
const launchTaskId = task.id;
|
|
126
|
+
const sel = view.state.selection.main;
|
|
127
|
+
const selFrom = sel.from;
|
|
128
|
+
const selTo = sel.to;
|
|
129
|
+
const selectionText = sel.empty ? '' : view.state.sliceDoc(selFrom, selTo);
|
|
130
|
+
const snapshotDoc = view.state.doc.toString();
|
|
131
|
+
setPaletteOpen(false);
|
|
132
|
+
setRefineError(null);
|
|
133
|
+
setUndoSnapshot(null);
|
|
69
134
|
setRefining(true);
|
|
135
|
+
setRefineElapsed(0);
|
|
136
|
+
const abort = new AbortController();
|
|
137
|
+
refineAbortRef.current = abort;
|
|
138
|
+
const started = Date.now();
|
|
139
|
+
const tick = setInterval(() => setRefineElapsed(Math.floor((Date.now() - started) / 1000)), 500);
|
|
140
|
+
|
|
70
141
|
try {
|
|
71
|
-
const res = await fetch(`${basePath}/
|
|
142
|
+
const res = await fetch(`${basePath}/refine`, {
|
|
72
143
|
method: 'POST',
|
|
73
144
|
headers: { 'Content-Type': 'application/json' },
|
|
74
145
|
body: JSON.stringify({
|
|
75
|
-
|
|
146
|
+
command: cmd,
|
|
147
|
+
customText,
|
|
148
|
+
selection: selectionText || undefined,
|
|
149
|
+
note: snapshotDoc,
|
|
76
150
|
}),
|
|
151
|
+
signal: abort.signal,
|
|
77
152
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (refined) {
|
|
82
|
-
await savePrompt(refined);
|
|
83
|
-
}
|
|
153
|
+
const data = await res.json() as { result?: string; error?: string };
|
|
154
|
+
if (!res.ok || data.error) {
|
|
155
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
84
156
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
157
|
+
const output = (data.result || '').trim();
|
|
158
|
+
if (!output) {
|
|
159
|
+
throw new Error('AI 응답이 비어있습니다');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If user switched to another task during the await, don't clobber it.
|
|
163
|
+
if (launchTaskId !== task.id) {
|
|
164
|
+
setRefineError('다른 태스크로 이동하여 결과를 버렸습니다');
|
|
165
|
+
setTimeout(() => setRefineError(null), 4000);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Build next doc via string splicing — independent of CM view lifecycle.
|
|
170
|
+
// CM view may have been recreated during the await; positions from the
|
|
171
|
+
// captured selection are still valid against the snapshot doc.
|
|
172
|
+
const safeFrom = Math.min(selFrom, snapshotDoc.length);
|
|
173
|
+
const safeTo = Math.min(selTo, snapshotDoc.length);
|
|
174
|
+
let nextDoc: string;
|
|
175
|
+
let nextCaret: number;
|
|
176
|
+
if (selFrom === selTo) {
|
|
177
|
+
// empty selection → insert at caret with blank-line padding when not at start
|
|
178
|
+
const prefix = safeFrom > 0 ? '\n\n' : '';
|
|
179
|
+
const insert = prefix + output + '\n';
|
|
180
|
+
nextDoc = snapshotDoc.slice(0, safeFrom) + insert + snapshotDoc.slice(safeFrom);
|
|
181
|
+
nextCaret = safeFrom + insert.length;
|
|
182
|
+
} else {
|
|
183
|
+
nextDoc = snapshotDoc.slice(0, safeFrom) + output + snapshotDoc.slice(safeTo);
|
|
184
|
+
nextCaret = safeFrom + output.length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Update React state first — triggers CM re-sync via value prop
|
|
188
|
+
setDescription(nextDoc);
|
|
189
|
+
onUpdate({ description: nextDoc });
|
|
190
|
+
|
|
191
|
+
// Remember the pre-refine doc so the user can undo a bad suggestion.
|
|
192
|
+
// 30s is enough to read the output and decide.
|
|
193
|
+
setUndoSnapshot({ taskId: launchTaskId, doc: snapshotDoc });
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
setUndoSnapshot(prev => (prev && prev.taskId === launchTaskId ? null : prev));
|
|
196
|
+
}, 30000);
|
|
88
197
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
198
|
+
// Best-effort: move caret inside the live view if still mounted
|
|
199
|
+
const liveView = editorRef.current?.view;
|
|
200
|
+
if (liveView) {
|
|
201
|
+
try {
|
|
202
|
+
const clamped = Math.min(nextCaret, liveView.state.doc.length);
|
|
203
|
+
liveView.dispatch({ selection: { anchor: clamped } });
|
|
204
|
+
} catch { /* selection restore is non-critical */ }
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if ((err as { name?: string })?.name === 'AbortError') {
|
|
208
|
+
// User cancelled — no error message needed.
|
|
209
|
+
} else {
|
|
210
|
+
const msg = err instanceof Error ? err.message : '알 수 없는 오류';
|
|
211
|
+
setRefineError(msg);
|
|
212
|
+
setTimeout(() => setRefineError(null), 6000);
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
clearInterval(tick);
|
|
216
|
+
setRefining(false);
|
|
217
|
+
refineAbortRef.current = null;
|
|
218
|
+
}
|
|
219
|
+
}, [basePath, onUpdate, task.id]);
|
|
220
|
+
|
|
221
|
+
const cancelRefine = useCallback(() => {
|
|
222
|
+
refineAbortRef.current?.abort();
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
const undoRefine = useCallback(() => {
|
|
226
|
+
if (!undoSnapshot) return;
|
|
227
|
+
if (undoSnapshot.taskId !== task.id) { setUndoSnapshot(null); return; }
|
|
228
|
+
setDescription(undoSnapshot.doc);
|
|
229
|
+
onUpdate({ description: undoSnapshot.doc });
|
|
230
|
+
setUndoSnapshot(null);
|
|
231
|
+
}, [undoSnapshot, task.id, onUpdate]);
|
|
92
232
|
|
|
93
233
|
const priorities: ItemPriority[] = ['high', 'medium', 'low'];
|
|
94
234
|
|
|
95
235
|
return (
|
|
96
236
|
<div className="flex flex-col h-full">
|
|
97
|
-
{/*
|
|
237
|
+
{/* Header */}
|
|
98
238
|
<div className="px-4 py-3 border-b border-border flex-shrink-0 space-y-2">
|
|
99
|
-
{/* Title */}
|
|
100
239
|
{editingTitle ? (
|
|
101
240
|
<input
|
|
102
241
|
value={title}
|
|
@@ -116,7 +255,6 @@ export default function TaskDetail({
|
|
|
116
255
|
</h2>
|
|
117
256
|
)}
|
|
118
257
|
|
|
119
|
-
{/* Status + Priority + Today + Prompt + Delete */}
|
|
120
258
|
<div className="flex items-center gap-3 flex-wrap">
|
|
121
259
|
<StatusFlow status={task.status} onChange={(status: TaskStatus) => onUpdate({ status })} />
|
|
122
260
|
<div className="flex items-center gap-1">
|
|
@@ -148,14 +286,30 @@ export default function TaskDetail({
|
|
|
148
286
|
<span className="text-border">|</span>
|
|
149
287
|
|
|
150
288
|
<button
|
|
151
|
-
onClick={
|
|
289
|
+
onClick={openPalette}
|
|
290
|
+
title="AI 명령 (⌘K)"
|
|
291
|
+
className="text-xs px-2 py-0.5 rounded transition-colors border border-border
|
|
292
|
+
text-muted-foreground hover:text-foreground hover:border-muted-foreground"
|
|
293
|
+
>
|
|
294
|
+
⌘K
|
|
295
|
+
</button>
|
|
296
|
+
<button
|
|
297
|
+
onClick={copyAsPrompt}
|
|
298
|
+
title="노트를 Claude Code용으로 클립보드에 복사"
|
|
299
|
+
className="text-xs px-2 py-0.5 rounded transition-colors border border-border
|
|
300
|
+
text-muted-foreground hover:text-foreground hover:border-muted-foreground"
|
|
301
|
+
>
|
|
302
|
+
{copied ? '✓ Copied' : 'Copy as Prompt'}
|
|
303
|
+
</button>
|
|
304
|
+
<button
|
|
305
|
+
onClick={() => setChatOpen(v => !v)}
|
|
152
306
|
className={`text-xs px-2 py-0.5 rounded transition-colors border ${
|
|
153
|
-
|
|
154
|
-
? 'bg-accent/15 text-accent border-accent/30
|
|
307
|
+
chatOpen
|
|
308
|
+
? 'bg-accent/15 text-accent border-accent/30'
|
|
155
309
|
: 'text-muted-foreground border-border hover:text-foreground hover:border-muted-foreground'
|
|
156
310
|
}`}
|
|
157
311
|
>
|
|
158
|
-
|
|
312
|
+
💬 Chat
|
|
159
313
|
</button>
|
|
160
314
|
|
|
161
315
|
<button
|
|
@@ -165,60 +319,87 @@ export default function TaskDetail({
|
|
|
165
319
|
Delete
|
|
166
320
|
</button>
|
|
167
321
|
</div>
|
|
168
|
-
|
|
169
|
-
{/* Description - compact */}
|
|
170
|
-
<textarea
|
|
171
|
-
value={description}
|
|
172
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
173
|
-
onBlur={saveDescription}
|
|
174
|
-
placeholder="Background, conditions, notes..."
|
|
175
|
-
className="w-full bg-input border border-border rounded-lg px-3 py-2 text-sm
|
|
176
|
-
focus:border-primary focus:outline-none text-foreground resize-y
|
|
177
|
-
leading-relaxed min-h-[3.5rem] max-h-[300px]"
|
|
178
|
-
rows={2}
|
|
179
|
-
/>
|
|
180
322
|
</div>
|
|
181
323
|
|
|
182
|
-
{/*
|
|
183
|
-
<div className="flex-1 min-h-0">
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
324
|
+
{/* Note editor */}
|
|
325
|
+
<div className="flex-1 min-h-0 flex flex-col relative">
|
|
326
|
+
<div className="px-4 pt-2 pb-1 flex items-center justify-between">
|
|
327
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Note</span>
|
|
328
|
+
<span className="text-[10px] text-muted-foreground/60">
|
|
329
|
+
Tab 제안 수락 · ⌘K AI 명령 · 자동 저장
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
<div className="flex-1 min-h-0">
|
|
333
|
+
<NoteEditor
|
|
334
|
+
ref={editorRef}
|
|
335
|
+
value={description}
|
|
336
|
+
onChange={setDescription}
|
|
337
|
+
onBlur={saveDescription}
|
|
338
|
+
onOpenCommand={openPalette}
|
|
339
|
+
extraCorpus={extraCorpus}
|
|
340
|
+
placeholder="자유롭게 작성하세요. 배경 · 목표 · 관련 파일 · 결정사항 · 질문 · 링크 등 뭐든..."
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
{refining && (
|
|
344
|
+
<div className="absolute bottom-2 right-3 text-xs px-2 py-1 rounded bg-muted/90 text-foreground flex items-center gap-2 shadow-lg border border-border">
|
|
345
|
+
<span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
|
|
346
|
+
<span>AI 작업 중 {refineElapsed}s (최대 90s)</span>
|
|
347
|
+
<button
|
|
348
|
+
onClick={cancelRefine}
|
|
349
|
+
className="ml-1 text-muted-foreground hover:text-destructive transition-colors"
|
|
350
|
+
title="취소"
|
|
351
|
+
>
|
|
352
|
+
취소
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
{!refining && undoSnapshot && undoSnapshot.taskId === task.id && (
|
|
357
|
+
<div className="absolute bottom-2 right-3 text-xs px-2 py-1 rounded bg-accent/15 text-foreground flex items-center gap-2 shadow-lg border border-accent/30">
|
|
358
|
+
<span className="text-accent">✓</span>
|
|
359
|
+
<span>AI 결과 적용됨</span>
|
|
360
|
+
<button
|
|
361
|
+
onClick={undoRefine}
|
|
362
|
+
className="px-1.5 py-0.5 rounded bg-background/60 hover:bg-background text-foreground transition-colors"
|
|
363
|
+
title="되돌리기 (30초 내)"
|
|
364
|
+
>
|
|
365
|
+
↶ 되돌리기
|
|
366
|
+
</button>
|
|
367
|
+
<button
|
|
368
|
+
onClick={() => setUndoSnapshot(null)}
|
|
369
|
+
className="text-muted-foreground hover:text-foreground"
|
|
370
|
+
title="닫기"
|
|
371
|
+
>
|
|
372
|
+
×
|
|
373
|
+
</button>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
{refineError && (
|
|
377
|
+
<div className="absolute bottom-2 right-3 text-xs px-3 py-2 rounded bg-destructive/15 text-destructive flex items-center gap-2 shadow-lg border border-destructive/40 max-w-[70%]">
|
|
378
|
+
<span>⚠</span>
|
|
379
|
+
<span className="truncate">AI 실패: {refineError}</span>
|
|
380
|
+
<button onClick={() => setRefineError(null)} className="text-destructive/60 hover:text-destructive">×</button>
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
190
383
|
</div>
|
|
191
384
|
|
|
192
|
-
{/*
|
|
193
|
-
{
|
|
194
|
-
<div
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col animate-dialog-in">
|
|
202
|
-
<div className="flex items-center justify-between px-5 py-3 border-b border-border">
|
|
203
|
-
<h3 className="text-sm font-semibold text-foreground">Prompt</h3>
|
|
204
|
-
<button
|
|
205
|
-
onClick={() => setShowPromptModal(false)}
|
|
206
|
-
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
207
|
-
>
|
|
208
|
-
Close
|
|
209
|
-
</button>
|
|
210
|
-
</div>
|
|
211
|
-
<div className="flex-1 overflow-y-auto p-5">
|
|
212
|
-
<PromptEditor
|
|
213
|
-
content={promptContent}
|
|
214
|
-
onSave={savePrompt}
|
|
215
|
-
onRefine={handleRefine}
|
|
216
|
-
refining={refining}
|
|
217
|
-
/>
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
385
|
+
{/* Chat (optional, collapsed by default) */}
|
|
386
|
+
{chatOpen && (
|
|
387
|
+
<div className="h-[38%] min-h-[240px] border-t border-border">
|
|
388
|
+
<TaskChat
|
|
389
|
+
basePath={basePath}
|
|
390
|
+
taskStatus={task.status}
|
|
391
|
+
onInsertToNote={insertIntoNote}
|
|
392
|
+
onChatStateChange={onChatStateChange ? (state) => onChatStateChange(task.id, state) : undefined}
|
|
393
|
+
/>
|
|
220
394
|
</div>
|
|
221
395
|
)}
|
|
396
|
+
|
|
397
|
+
<CommandPalette
|
|
398
|
+
open={paletteOpen}
|
|
399
|
+
hasSelection={hasSelection}
|
|
400
|
+
onClose={() => setPaletteOpen(false)}
|
|
401
|
+
onRun={runRefine}
|
|
402
|
+
/>
|
|
222
403
|
</div>
|
|
223
404
|
);
|
|
224
405
|
}
|
|
@@ -125,7 +125,7 @@ export default function TaskList({
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
function getNextStatus(current: TaskStatus): TaskStatus {
|
|
128
|
-
const flow: TaskStatus[] = ['idea', '
|
|
128
|
+
const flow: TaskStatus[] = ['idea', 'doing', 'done'];
|
|
129
129
|
const idx = flow.indexOf(current);
|
|
130
130
|
if (idx === -1) return 'idea';
|
|
131
131
|
return flow[(idx + 1) % flow.length];
|
|
@@ -336,7 +336,12 @@ export default function WorkspacePanel({
|
|
|
336
336
|
useEffect(() => {
|
|
337
337
|
const handler = (e: KeyboardEvent) => {
|
|
338
338
|
if (!isActive) return;
|
|
339
|
-
const
|
|
339
|
+
const target = e.target as HTMLElement | null;
|
|
340
|
+
const isInput =
|
|
341
|
+
target instanceof HTMLInputElement ||
|
|
342
|
+
target instanceof HTMLTextAreaElement ||
|
|
343
|
+
(target?.isContentEditable ?? false) ||
|
|
344
|
+
!!target?.closest?.('.cm-editor');
|
|
340
345
|
|
|
341
346
|
if (!isInput && e.code === 'KeyB' && !e.metaKey && !e.ctrlKey) {
|
|
342
347
|
e.preventDefault();
|
|
@@ -356,8 +361,7 @@ export default function WorkspacePanel({
|
|
|
356
361
|
}
|
|
357
362
|
if (selectedTaskId && selectedSubId && !isInput) {
|
|
358
363
|
const statusMap: Record<string, TaskStatus> = {
|
|
359
|
-
'Digit1': 'idea', 'Digit2': '
|
|
360
|
-
'Digit4': 'testing', 'Digit5': 'done', 'Digit6': 'problem',
|
|
364
|
+
'Digit1': 'idea', 'Digit2': 'doing', 'Digit3': 'done', 'Digit4': 'problem',
|
|
361
365
|
};
|
|
362
366
|
if ((e.metaKey || e.ctrlKey) && statusMap[e.code]) {
|
|
363
367
|
e.preventDefault();
|
|
@@ -530,6 +534,7 @@ export default function WorkspacePanel({
|
|
|
530
534
|
<div className="flex-1 min-w-0">
|
|
531
535
|
{selectedTask ? (
|
|
532
536
|
<TaskDetail task={selectedTask} projectId={id} subProjectId={selectedSubId!}
|
|
537
|
+
siblingTasks={tasks}
|
|
533
538
|
onUpdate={handleTaskUpdate} onDelete={handleTaskDelete}
|
|
534
539
|
onChatStateChange={(taskId, state) => {
|
|
535
540
|
setChatStates(prev => ({ ...prev, [taskId]: state }));
|
package/src/lib/ai/agents.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AgentType } from '../../types';
|
|
|
3
3
|
export interface AgentConfig {
|
|
4
4
|
name: string;
|
|
5
5
|
binary: string;
|
|
6
|
-
buildArgs: (opts: { streaming: boolean }) => string[];
|
|
6
|
+
buildArgs: (opts: { streaming: boolean; model?: string }) => string[];
|
|
7
7
|
buildEnv: () => NodeJS.ProcessEnv;
|
|
8
8
|
parseStreamEvent: (parsed: Record<string, unknown>) => { text?: string; final?: string } | null;
|
|
9
9
|
cleanOutput?: (text: string) => string;
|
|
@@ -12,9 +12,9 @@ export interface AgentConfig {
|
|
|
12
12
|
const claudeConfig: AgentConfig = {
|
|
13
13
|
name: 'Claude',
|
|
14
14
|
binary: 'claude',
|
|
15
|
-
buildArgs: ({ streaming }) => [
|
|
15
|
+
buildArgs: ({ streaming, model }) => [
|
|
16
16
|
'--dangerously-skip-permissions',
|
|
17
|
-
'--model', 'opus',
|
|
17
|
+
'--model', model || 'opus',
|
|
18
18
|
...(streaming
|
|
19
19
|
? ['--output-format', 'stream-json', '--verbose']
|
|
20
20
|
: ['--output-format', 'text']),
|