@jcjeon/integration-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/.gitignore +23 -0
  2. package/.npmignore +21 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +26 -0
  5. package/AGENTS.md +10 -0
  6. package/CLAUDE.md +10 -0
  7. package/README.md +384 -0
  8. package/apps/server/README.md +294 -0
  9. package/apps/server/eslint.config.mjs +20 -0
  10. package/apps/server/nest-cli.json +8 -0
  11. package/apps/server/package.json +89 -0
  12. package/apps/server/scripts/postinstall.js +53 -0
  13. package/apps/server/src/__mocks__/glob.js +6 -0
  14. package/apps/server/src/__mocks__/uuid.js +5 -0
  15. package/apps/server/src/app.controller.spec.ts +24 -0
  16. package/apps/server/src/app.controller.ts +13 -0
  17. package/apps/server/src/app.module.ts +18 -0
  18. package/apps/server/src/app.service.ts +8 -0
  19. package/apps/server/src/common/ji-paths.ts +41 -0
  20. package/apps/server/src/database/database.module.ts +27 -0
  21. package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
  22. package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
  23. package/apps/server/src/database/entities/conversation.entity.ts +41 -0
  24. package/apps/server/src/database/entities/session.entity.ts +16 -0
  25. package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
  26. package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
  27. package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
  28. package/apps/server/src/database/entities/task-run.entity.ts +41 -0
  29. package/apps/server/src/database/entities/task.entity.ts +44 -0
  30. package/apps/server/src/main.ts +65 -0
  31. package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
  32. package/apps/server/src/modules/agents/agents.module.ts +11 -0
  33. package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
  34. package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
  35. package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
  36. package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
  37. package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
  38. package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
  39. package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
  40. package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
  41. package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
  42. package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
  43. package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
  44. package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
  45. package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
  46. package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
  47. package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
  48. package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
  49. package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
  50. package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
  51. package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
  52. package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
  53. package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
  54. package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
  55. package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
  56. package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
  57. package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
  58. package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
  59. package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
  60. package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
  61. package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
  62. package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
  63. package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
  64. package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
  65. package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
  66. package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
  67. package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
  68. package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
  69. package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
  70. package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
  71. package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
  72. package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
  73. package/apps/server/src/modules/fs/fs.controller.ts +29 -0
  74. package/apps/server/src/modules/fs/fs.module.ts +8 -0
  75. package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
  76. package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
  77. package/apps/server/src/modules/harness/harness.controller.ts +35 -0
  78. package/apps/server/src/modules/harness/harness.module.ts +11 -0
  79. package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
  80. package/apps/server/src/modules/harness/harness.service.ts +112 -0
  81. package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
  82. package/apps/server/src/modules/sessions/session.controller.ts +43 -0
  83. package/apps/server/src/modules/sessions/session.module.ts +14 -0
  84. package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
  85. package/apps/server/src/modules/sessions/session.service.ts +35 -0
  86. package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
  87. package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
  88. package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
  89. package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
  90. package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
  91. package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
  92. package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
  93. package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
  94. package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
  95. package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
  96. package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
  97. package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
  98. package/apps/server/test/app.e2e-spec.ts +28 -0
  99. package/apps/server/test/jest-e2e.json +9 -0
  100. package/apps/server/tsconfig.build.json +4 -0
  101. package/apps/server/tsconfig.json +13 -0
  102. package/apps/web/AGENTS.md +7 -0
  103. package/apps/web/CLAUDE.md +1 -0
  104. package/apps/web/README.md +36 -0
  105. package/apps/web/eslint.config.mjs +21 -0
  106. package/apps/web/next-env.d.ts +6 -0
  107. package/apps/web/next.config.ts +7 -0
  108. package/apps/web/package.json +49 -0
  109. package/apps/web/postcss.config.mjs +7 -0
  110. package/apps/web/public/file.svg +1 -0
  111. package/apps/web/public/globe.svg +1 -0
  112. package/apps/web/public/next.svg +1 -0
  113. package/apps/web/public/vercel.svg +1 -0
  114. package/apps/web/public/window.svg +1 -0
  115. package/apps/web/src/app/claude/page.tsx +5 -0
  116. package/apps/web/src/app/codex/page.tsx +126 -0
  117. package/apps/web/src/app/favicon.ico +0 -0
  118. package/apps/web/src/app/gemini/page.tsx +130 -0
  119. package/apps/web/src/app/globals.css +149 -0
  120. package/apps/web/src/app/layout.tsx +40 -0
  121. package/apps/web/src/app/login/page.tsx +67 -0
  122. package/apps/web/src/app/page.tsx +497 -0
  123. package/apps/web/src/app/task/[id]/page.tsx +11 -0
  124. package/apps/web/src/app/test/page.tsx +298 -0
  125. package/apps/web/src/components/ui/Modal.tsx +78 -0
  126. package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
  127. package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
  128. package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
  129. package/apps/web/src/features/auth/api/auth.api.ts +81 -0
  130. package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
  131. package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
  132. package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
  133. package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
  134. package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
  135. package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
  136. package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
  137. package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
  138. package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
  139. package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
  140. package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
  141. package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
  142. package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
  143. package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
  144. package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
  145. package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
  146. package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
  147. package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
  148. package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
  149. package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
  150. package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
  151. package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
  152. package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
  153. package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
  154. package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
  155. package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
  156. package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
  157. package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
  158. package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
  159. package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
  160. package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
  161. package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
  162. package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
  163. package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
  164. package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
  165. package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
  166. package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
  167. package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
  168. package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
  169. package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
  170. package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
  171. package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
  172. package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
  173. package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
  174. package/apps/web/src/features/fs/api/fs.api.ts +14 -0
  175. package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
  176. package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
  177. package/apps/web/src/features/harness/api/harness.api.ts +46 -0
  178. package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
  179. package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
  180. package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
  181. package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
  182. package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
  183. package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
  184. package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
  185. package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
  186. package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
  187. package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
  188. package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
  189. package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
  190. package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
  191. package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
  192. package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
  193. package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
  194. package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
  195. package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
  196. package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
  197. package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
  198. package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
  199. package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
  200. package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
  201. package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
  202. package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
  203. package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
  204. package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
  205. package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
  206. package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
  207. package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
  208. package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
  209. package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
  210. package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
  211. package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
  212. package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
  213. package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
  214. package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
  215. package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
  216. package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
  217. package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
  218. package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
  219. package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
  220. package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
  221. package/apps/web/src/hooks/useWorkingDir.ts +28 -0
  222. package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
  223. package/apps/web/src/lib/ansi.ts +105 -0
  224. package/apps/web/src/lib/constants.ts +4 -0
  225. package/apps/web/src/lib/quota.ts +22 -0
  226. package/apps/web/src/lib/theme.tsx +78 -0
  227. package/apps/web/src/lib/toast.tsx +175 -0
  228. package/apps/web/src/store/agentStatusStore.ts +38 -0
  229. package/apps/web/tsconfig.json +18 -0
  230. package/apps/web/vitest.config.ts +25 -0
  231. package/apps/web/vitest.setup.ts +10 -0
  232. package/package.json +85 -0
  233. package/packages/cli/dist/commands/check.d.ts +1 -0
  234. package/packages/cli/dist/commands/check.js +89 -0
  235. package/packages/cli/dist/commands/init.d.ts +5 -0
  236. package/packages/cli/dist/commands/init.js +183 -0
  237. package/packages/cli/dist/commands/start.d.ts +4 -0
  238. package/packages/cli/dist/commands/start.js +188 -0
  239. package/packages/cli/dist/index.d.ts +2 -0
  240. package/packages/cli/dist/index.js +71 -0
  241. package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
  242. package/packages/cli/dist/utils/agent-tools.js +193 -0
  243. package/packages/cli/dist/utils/project-init.d.ts +12 -0
  244. package/packages/cli/dist/utils/project-init.js +258 -0
  245. package/packages/cli/dist/utils/proxy.d.ts +8 -0
  246. package/packages/cli/dist/utils/proxy.js +138 -0
  247. package/packages/cli/package.json +30 -0
  248. package/packages/cli/src/commands/check.ts +77 -0
  249. package/packages/cli/src/commands/init.ts +209 -0
  250. package/packages/cli/src/commands/start.ts +183 -0
  251. package/packages/cli/src/index.ts +91 -0
  252. package/packages/cli/src/utils/agent-tools.ts +201 -0
  253. package/packages/cli/src/utils/project-init.ts +252 -0
  254. package/packages/cli/src/utils/proxy.ts +123 -0
  255. package/packages/cli/tsconfig.json +14 -0
  256. package/packages/eslint-config/base.mjs +31 -0
  257. package/packages/eslint-config/nest.mjs +55 -0
  258. package/packages/eslint-config/next.mjs +23 -0
  259. package/packages/eslint-config/package.json +20 -0
  260. package/packages/typescript-config/base.json +16 -0
  261. package/packages/typescript-config/nestjs.json +17 -0
  262. package/packages/typescript-config/nextjs.json +15 -0
  263. package/packages/typescript-config/package.json +11 -0
  264. package/turbo.json +28 -0
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+
5
+ import { updateTask } from "../api/tasks.api";
6
+ import type { AgentRole, Task, UpdateTaskPayload } from "../api/tasks.api";
7
+ import type { AgentDraft, RequirementDraft } from "./useTaskCreate";
8
+ import type { AgentId } from "@/features/chat/ui/AgentSelectModal";
9
+
10
+ let draftId = 1000; // useTaskCreate와 충돌 방지
11
+ const nextDraftId = () => String(++draftId);
12
+
13
+ export interface EditFormState {
14
+ title: string;
15
+ workingDir: string;
16
+ requirements: RequirementDraft[];
17
+ agents: AgentDraft[];
18
+ }
19
+
20
+ function toRequirementDrafts(reqs: Task["requirements"]): RequirementDraft[] {
21
+ return [...reqs]
22
+ .sort((a, b) => a.orderIndex - b.orderIndex)
23
+ .map((r) => ({ id: nextDraftId(), content: r.content }));
24
+ }
25
+
26
+ function toAgentDrafts(agents: Task["agents"]): AgentDraft[] {
27
+ return agents.map((a) => ({
28
+ id: nextDraftId(),
29
+ agentType: a.agentType as AgentId,
30
+ role: a.role,
31
+ customRole: a.customRole ?? "",
32
+ }));
33
+ }
34
+
35
+ export function useTaskEdit(task: Task, onSuccess?: (updated: Task) => void) {
36
+ const [form, setForm] = useState<EditFormState>(() => ({
37
+ title: task.title,
38
+ workingDir: task.workingDir ?? "",
39
+ requirements: toRequirementDrafts(task.requirements),
40
+ agents: toAgentDrafts(task.agents),
41
+ }));
42
+ const [submitting, setSubmitting] = useState(false);
43
+ const [error, setError] = useState<string | null>(null);
44
+
45
+ // task prop이 바뀌면 폼 동기화
46
+ useEffect(() => {
47
+ setForm({
48
+ title: task.title,
49
+ workingDir: task.workingDir ?? "",
50
+ requirements: toRequirementDrafts(task.requirements),
51
+ agents: toAgentDrafts(task.agents),
52
+ });
53
+ }, [task.id]); // eslint-disable-line react-hooks/exhaustive-deps
54
+
55
+ // ─── title / workingDir ──────────────────────────────────────────────
56
+
57
+ const setTitle = useCallback((v: string) => setForm((f) => ({ ...f, title: v })), []);
58
+ const setWorkingDir = useCallback((v: string) => setForm((f) => ({ ...f, workingDir: v })), []);
59
+
60
+ // ─── requirements ────────────────────────────────────────────────────
61
+
62
+ const addRequirement = useCallback(() => {
63
+ setForm((f) => ({ ...f, requirements: [...f.requirements, { id: nextDraftId(), content: "" }] }));
64
+ }, []);
65
+
66
+ const updateRequirement = useCallback((id: string, content: string) => {
67
+ setForm((f) => ({ ...f, requirements: f.requirements.map((r) => (r.id === id ? { ...r, content } : r)) }));
68
+ }, []);
69
+
70
+ const removeRequirement = useCallback((id: string) => {
71
+ setForm((f) => ({ ...f, requirements: f.requirements.filter((r) => r.id !== id) }));
72
+ }, []);
73
+
74
+ // ─── agents ──────────────────────────────────────────────────────────
75
+
76
+ const addAgent = useCallback((agentType: AgentId, role: AgentRole = "frontend") => {
77
+ setForm((f) => ({ ...f, agents: [...f.agents, { id: nextDraftId(), agentType, role, customRole: "" }] }));
78
+ }, []);
79
+
80
+ const updateAgent = useCallback((id: string, patch: Partial<Omit<AgentDraft, "id">>) => {
81
+ setForm((f) => ({ ...f, agents: f.agents.map((a) => (a.id === id ? { ...a, ...patch } : a)) }));
82
+ }, []);
83
+
84
+ const removeAgent = useCallback((id: string) => {
85
+ setForm((f) => ({ ...f, agents: f.agents.filter((a) => a.id !== id) }));
86
+ }, []);
87
+
88
+ // ─── submit ──────────────────────────────────────────────────────────
89
+
90
+ const submit = useCallback(async () => {
91
+ if (!form.title.trim()) { setError("작업 제목을 입력하세요."); return; }
92
+ setError(null);
93
+ setSubmitting(true);
94
+ try {
95
+ const payload: UpdateTaskPayload = {
96
+ title: form.title.trim(),
97
+ workingDir: form.workingDir.trim() || undefined,
98
+ requirements: form.requirements
99
+ .filter((r) => r.content.trim())
100
+ .map((r, i) => ({ content: r.content.trim(), orderIndex: i })),
101
+ agents: form.agents.map((a) => ({
102
+ agentType: a.agentType,
103
+ role: a.role,
104
+ customRole: a.role === "other" && a.customRole.trim() ? a.customRole.trim() : undefined,
105
+ })),
106
+ };
107
+ const updated = await updateTask(task.id, payload);
108
+ onSuccess?.(updated);
109
+ } catch (e) {
110
+ setError(e instanceof Error ? e.message : "수정 실패");
111
+ } finally {
112
+ setSubmitting(false);
113
+ }
114
+ }, [form, task.id, onSuccess]);
115
+
116
+ return {
117
+ form,
118
+ submitting,
119
+ error,
120
+ setTitle,
121
+ setWorkingDir,
122
+ addRequirement,
123
+ updateRequirement,
124
+ removeRequirement,
125
+ addAgent,
126
+ updateAgent,
127
+ removeAgent,
128
+ submit,
129
+ };
130
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { io, type Socket } from "socket.io-client";
5
+
6
+ import { SERVER_URL } from "@/lib/constants";
7
+ import type { AgentStatus, TaskStatus } from "../api/tasks.api";
8
+
9
+ const TASKS_WS_NAMESPACE = "/tasks";
10
+
11
+ // ─── 서버 이벤트 타입 ─────────────────────────────────────────────────────────
12
+
13
+ interface AgentOutputEvent { taskId: string; agentId: number; text: string }
14
+ interface AgentToolEvent { taskId: string; agentId: number; tool: string; input: Record<string, unknown> }
15
+ interface AgentDoneEvent { taskId: string; agentId: number; result: string; isError: boolean; durationMs: number; costUsd: number }
16
+ interface AgentErrorEvent { taskId: string; agentId: number; message: string }
17
+ interface TaskStatusEvent { taskId: string; status: TaskStatus }
18
+
19
+ interface BufferedAgentLog {
20
+ agentId: number;
21
+ status: string;
22
+ output: string;
23
+ durationMs?: number;
24
+ costUsd?: number;
25
+ errorMessage?: string;
26
+ }
27
+
28
+ // ─── 클라이언트 상태 ──────────────────────────────────────────────────────────
29
+
30
+ export interface AgentLog {
31
+ agentId: number;
32
+ status: AgentStatus;
33
+ output: string;
34
+ durationMs?: number;
35
+ costUsd?: number;
36
+ errorMessage?: string;
37
+ }
38
+
39
+ // ─── Hook ─────────────────────────────────────────────────────────────────────
40
+
41
+ export function useTaskExecution(
42
+ taskId: string | null,
43
+ onTaskStatusChange?: (taskId: string, status: TaskStatus) => void,
44
+ ) {
45
+ const [agentLogs, setAgentLogs] = useState<Record<number, AgentLog>>({});
46
+ const [connected, setConnected] = useState(false);
47
+ const socketRef = useRef<Socket | null>(null);
48
+
49
+ const upsertAgent = useCallback((agentId: number, patch: Partial<AgentLog>) => {
50
+ setAgentLogs((prev) => {
51
+ const base: AgentLog = prev[agentId] ?? { agentId, status: "running", output: "" };
52
+ return { ...prev, [agentId]: { ...base, ...patch } };
53
+ });
54
+ }, []);
55
+
56
+ const appendOutput = useCallback((agentId: number, text: string) => {
57
+ setAgentLogs((prev) => {
58
+ const base: AgentLog = prev[agentId] ?? { agentId, status: "running", output: "" };
59
+ return { ...prev, [agentId]: { ...base, output: base.output + text } };
60
+ });
61
+ }, []);
62
+
63
+ useEffect(() => {
64
+ if (!taskId) return;
65
+
66
+ setAgentLogs({});
67
+ const socket = io(`${SERVER_URL}${TASKS_WS_NAMESPACE}`, { transports: ["websocket"] });
68
+ socketRef.current = socket;
69
+
70
+ socket.on("connect", () => {
71
+ setConnected(true);
72
+ // 1. 룸 구독
73
+ socket.emit("task:subscribe", { taskId });
74
+ // 2. 이미 실행 중이거나 완료된 경우 버퍼 요청 (타이밍 경쟁 조건 해결)
75
+ socket.emit("task:get-logs", { taskId });
76
+ });
77
+
78
+ socket.on("disconnect", () => setConnected(false));
79
+
80
+ // 늦은 구독 — 서버가 저장해 둔 버퍼를 한 번에 리플레이
81
+ socket.on("task:buffered-logs", ({ logs }: { taskId: string; logs: BufferedAgentLog[] }) => {
82
+ setAgentLogs((prev) => {
83
+ const next = { ...prev };
84
+ for (const log of logs) {
85
+ const existing = next[log.agentId];
86
+ // 이미 라이브 이벤트로 받은 내용이 있으면 버퍼가 더 짧을 수 있으므로 긴 것 유지
87
+ if (!existing || existing.output.length < log.output.length) {
88
+ next[log.agentId] = {
89
+ agentId: log.agentId,
90
+ status: (log.status as AgentStatus) ?? "running",
91
+ output: log.output,
92
+ durationMs: log.durationMs,
93
+ costUsd: log.costUsd,
94
+ errorMessage: log.errorMessage,
95
+ };
96
+ }
97
+ }
98
+ return next;
99
+ });
100
+ });
101
+
102
+ // 라이브 이벤트
103
+ socket.on("agent:output", ({ agentId, text }: AgentOutputEvent) => {
104
+ appendOutput(agentId, text);
105
+ });
106
+
107
+ socket.on("agent:tool", ({ agentId, tool, input }: AgentToolEvent) => {
108
+ const line = `\n⚙ ${tool}(${JSON.stringify(input).slice(0, 120)})\n`;
109
+ appendOutput(agentId, line);
110
+ });
111
+
112
+ socket.on("agent:done", ({ agentId, isError, durationMs, costUsd }: AgentDoneEvent) => {
113
+ upsertAgent(agentId, {
114
+ status: isError ? "error" : "completed",
115
+ durationMs,
116
+ costUsd,
117
+ });
118
+ });
119
+
120
+ socket.on("agent:error", ({ agentId, message }: AgentErrorEvent) => {
121
+ upsertAgent(agentId, { status: "error", errorMessage: message });
122
+ });
123
+
124
+ socket.on("task:status", ({ taskId: tid, status }: TaskStatusEvent) => {
125
+ onTaskStatusChange?.(tid, status);
126
+ });
127
+
128
+ return () => {
129
+ socket.emit("task:unsubscribe", { taskId });
130
+ socket.disconnect();
131
+ socketRef.current = null;
132
+ setConnected(false);
133
+ };
134
+ }, [taskId, upsertAgent, appendOutput, onTaskStatusChange]);
135
+
136
+ return { agentLogs, connected };
137
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+
5
+ import { archiveTask, deleteTask, executeTask, fetchTasks, rerunTask, rerunTaskAgent, stopTask } from "../api/tasks.api";
6
+ import type { AgentTestCodePreference, Task, TaskStatus } from "../api/tasks.api";
7
+
8
+ export function useTaskList(open: boolean) {
9
+ const [tasks, setTasks] = useState<Task[]>([]);
10
+ const [loading, setLoading] = useState(false);
11
+ const [actioningId, setActioningId] = useState<string | null>(null);
12
+ const [error, setError] = useState<string | null>(null);
13
+
14
+ // 편집 대상 태스크
15
+ const [editingTask, setEditingTask] = useState<Task | null>(null);
16
+
17
+ // 폴링: running 상태인 작업이 있으면 3초마다 새로고침
18
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
19
+
20
+ const loadTasks = useCallback(async () => {
21
+ setLoading(true);
22
+ try {
23
+ const data = await fetchTasks();
24
+ setTasks(data);
25
+ } catch (e) {
26
+ setError(e instanceof Error ? e.message : "목록 조회 실패");
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ }, []);
31
+
32
+ // 모달 열릴 때 로드
33
+ useEffect(() => {
34
+ if (!open) return;
35
+ void loadTasks();
36
+ }, [open, loadTasks]);
37
+
38
+ // running 작업이 있으면 폴링
39
+ useEffect(() => {
40
+ const hasRunning = tasks.some((t) => t.status === "running");
41
+ if (hasRunning && !pollRef.current) {
42
+ pollRef.current = setInterval(() => void loadTasks(), 3000);
43
+ }
44
+ if (!hasRunning && pollRef.current) {
45
+ clearInterval(pollRef.current);
46
+ pollRef.current = null;
47
+ }
48
+ return () => {
49
+ if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
50
+ };
51
+ }, [tasks, loadTasks]);
52
+
53
+ // ─── 액션 ────────────────────────────────────────────────────────────
54
+
55
+ const execute = useCallback(async (id: string, agentTestCodePreferences: AgentTestCodePreference[] = []) => {
56
+ setActioningId(id);
57
+ try {
58
+ const updated = await executeTask(id, agentTestCodePreferences);
59
+ setTasks((prev) => prev.map((t) => (t.id === id ? updated : t)));
60
+ } catch (e) {
61
+ setError(e instanceof Error ? e.message : "실행 실패");
62
+ } finally {
63
+ setActioningId(null);
64
+ }
65
+ }, []);
66
+
67
+ const stop = useCallback(async (id: string) => {
68
+ setActioningId(id);
69
+ try {
70
+ const updated = await stopTask(id);
71
+ setTasks((prev) => prev.map((t) => (t.id === id ? updated : t)));
72
+ } catch (e) {
73
+ setError(e instanceof Error ? e.message : "중지 실패");
74
+ } finally {
75
+ setActioningId(null);
76
+ }
77
+ }, []);
78
+
79
+ const rerun = useCallback(async (id: string, supplementNote?: string, writeTestCode?: boolean) => {
80
+ setActioningId(id);
81
+ try {
82
+ const updated = await rerunTask(id, supplementNote, writeTestCode);
83
+ setTasks((prev) => prev.map((t) => (t.id === id ? updated : t)));
84
+ } catch (e) {
85
+ setError(e instanceof Error ? e.message : "재 실행 실패");
86
+ } finally {
87
+ setActioningId(null);
88
+ }
89
+ }, []);
90
+
91
+ const rerunAgent = useCallback(async (id: string, agentId: number, supplementNote?: string, writeTestCode?: boolean) => {
92
+ setActioningId(id);
93
+ try {
94
+ const updated = await rerunTaskAgent(id, agentId, supplementNote, writeTestCode);
95
+ setTasks((prev) => prev.map((t) => (t.id === id ? updated : t)));
96
+ } catch (e) {
97
+ setError(e instanceof Error ? e.message : "에이전트 재실행 실패");
98
+ } finally {
99
+ setActioningId(null);
100
+ }
101
+ }, []);
102
+
103
+ const archive = useCallback(async (id: string) => {
104
+ setActioningId(id);
105
+ try {
106
+ await archiveTask(id);
107
+ setTasks((prev) => prev.filter((t) => t.id !== id));
108
+ } catch (e) {
109
+ setError(e instanceof Error ? e.message : "보관 실패");
110
+ } finally {
111
+ setActioningId(null);
112
+ }
113
+ }, []);
114
+
115
+ const remove = useCallback(async (id: string) => {
116
+ setActioningId(id);
117
+ try {
118
+ await deleteTask(id);
119
+ setTasks((prev) => prev.filter((t) => t.id !== id));
120
+ } catch (e) {
121
+ setError(e instanceof Error ? e.message : "삭제 실패");
122
+ } finally {
123
+ setActioningId(null);
124
+ }
125
+ }, []);
126
+
127
+ const onEditDone = useCallback(
128
+ (updated: Task) => {
129
+ setTasks((prev) => prev.map((t) => (t.id === updated.id ? updated : t)));
130
+ setEditingTask(null);
131
+ },
132
+ [],
133
+ );
134
+
135
+ // WS 이벤트로 task status가 바뀔 때 로컬 state 반영
136
+ const updateTaskStatus = useCallback((taskId: string, status: TaskStatus) => {
137
+ setTasks((prev) =>
138
+ prev.map((t) => (t.id === taskId ? { ...t, status } : t)),
139
+ );
140
+ }, []);
141
+
142
+ return {
143
+ tasks,
144
+ loading,
145
+ actioningId,
146
+ error,
147
+ editingTask,
148
+ setEditingTask,
149
+ loadTasks,
150
+ execute,
151
+ stop,
152
+ rerun,
153
+ rerunAgent,
154
+ archive,
155
+ remove,
156
+ onEditDone,
157
+ updateTaskStatus,
158
+ };
159
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { io, type Socket } from "socket.io-client";
5
+
6
+ import { SERVER_URL } from "@/lib/constants";
7
+ import { useToast } from "@/lib/toast";
8
+ import { fetchTasks } from "../api/tasks.api";
9
+
10
+ const TASKS_WS_NAMESPACE = "/tasks";
11
+
12
+ interface TaskStatusEvent {
13
+ taskId: string;
14
+ status: string;
15
+ title?: string;
16
+ }
17
+
18
+ export function useTaskNotification() {
19
+ const { addToast } = useToast();
20
+ const [hasNew, setHasNew] = useState(false);
21
+
22
+ // 이전 상태 추적 — running → completed/error 전환 감지
23
+ const prevStatusRef = useRef<Map<string, string>>(new Map());
24
+ const titleRef = useRef<Map<string, string>>(new Map());
25
+
26
+ useEffect(() => {
27
+ // 초기 상태를 기준점으로 저장 (toast 미발행)
28
+ void fetchTasks().then((tasks) => {
29
+ tasks.forEach((t) => {
30
+ prevStatusRef.current.set(t.id, t.status);
31
+ titleRef.current.set(t.id, t.title);
32
+ });
33
+ });
34
+
35
+ const socket: Socket = io(`${SERVER_URL}${TASKS_WS_NAMESPACE}`, {
36
+ transports: ["websocket"],
37
+ });
38
+
39
+ socket.on("connect", () => {
40
+ // 전역 알림 룸 구독 — 모든 task:status 이벤트 수신
41
+ socket.emit("task:watch-all");
42
+ });
43
+
44
+ socket.on("task:status", ({ taskId, status, title }: TaskStatusEvent) => {
45
+ const prev = prevStatusRef.current.get(taskId);
46
+
47
+ // 서버가 title을 내려주면 갱신, 없으면 초기 로드 값 사용
48
+ const taskTitle = title ?? titleRef.current.get(taskId) ?? "작업";
49
+ if (title) titleRef.current.set(taskId, title);
50
+
51
+ // pending/running/undefined → completed/error/stopped 전환 시 알림
52
+ // (prev === "running" 만 체크하면 초기 로드 이전 신규 작업을 놓침)
53
+ const isFinished = (s: string) =>
54
+ s === "completed" || s === "error" || s === "stopped";
55
+
56
+ if (!isFinished(prev ?? "")) {
57
+ if (status === "completed") {
58
+ addToast({ type: "success", title: "작업 완료", message: taskTitle });
59
+ setHasNew(true);
60
+ } else if (status === "error") {
61
+ addToast({ type: "error", title: "작업 오류", message: taskTitle });
62
+ setHasNew(true);
63
+ } else if (status === "stopped") {
64
+ addToast({ type: "info", title: "작업 중지됨", message: taskTitle });
65
+ }
66
+ }
67
+
68
+ prevStatusRef.current.set(taskId, status);
69
+ });
70
+
71
+ return () => {
72
+ socket.emit("task:unwatch-all");
73
+ socket.disconnect();
74
+ };
75
+ }, [addToast]);
76
+
77
+ const clearNew = useCallback(() => setHasNew(false), []);
78
+
79
+ return { hasNew, clearNew };
80
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ import { fetchTaskRuns } from "../api/tasks.api";
6
+ import type { TaskRun } from "../api/tasks.api";
7
+
8
+ export function useTaskRuns(taskId: string | null) {
9
+ const [runs, setRuns] = useState<TaskRun[]>([]);
10
+ const [loading, setLoading] = useState(false);
11
+ const [error, setError] = useState<string | null>(null);
12
+
13
+ useEffect(() => {
14
+ if (!taskId) {
15
+ setRuns([]);
16
+ return;
17
+ }
18
+
19
+ const controller = new AbortController();
20
+ setLoading(true);
21
+ setError(null);
22
+
23
+ fetchTaskRuns(taskId)
24
+ .then((data) => { if (!controller.signal.aborted) setRuns(data); })
25
+ .catch((e: Error) => { if (!controller.signal.aborted) setError(e.message); })
26
+ .finally(() => { if (!controller.signal.aborted) setLoading(false); });
27
+
28
+ return () => { controller.abort(); };
29
+ }, [taskId]);
30
+
31
+ return { runs, loading, error };
32
+ }