@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,193 @@
1
+ "use client";
2
+
3
+ import type { TaskAgent } from "../api/tasks.api";
4
+ import type { TaskRun } from "../api/tasks.api";
5
+ import { useTaskRuns } from "../hooks/useTaskRuns";
6
+ import { AgentRoleBadge } from "./AgentRoleSelect";
7
+
8
+ // ─── 실행 상태 설정 ───────────────────────────────────────────────────────────
9
+
10
+ const RUN_STATUS_CONFIG: Record<string, { label: string; dot: string; text: string }> = {
11
+ running: { label: "실행 중", dot: "bg-emerald-500 animate-pulse", text: "text-emerald-600 dark:text-emerald-400" },
12
+ completed: { label: "완료", dot: "bg-blue-500", text: "text-blue-600 dark:text-blue-400" },
13
+ error: { label: "오류", dot: "bg-red-500", text: "text-red-600 dark:text-red-400" },
14
+ stopped: { label: "중지됨", dot: "bg-gray-400", text: "text-gray-500 dark:text-gray-400" },
15
+ pending: { label: "대기", dot: "bg-gray-300", text: "text-gray-400" },
16
+ };
17
+
18
+ function RunStatusBadge({ status }: { status: string }) {
19
+ const cfg = RUN_STATUS_CONFIG[status] ?? RUN_STATUS_CONFIG.pending;
20
+ return (
21
+ <span className={`flex items-center gap-1 text-xs ${cfg.text}`}>
22
+ <span className={`h-1.5 w-1.5 shrink-0 rounded-full ${cfg.dot}`} />
23
+ {cfg.label}
24
+ </span>
25
+ );
26
+ }
27
+
28
+ // ─── 단일 실행 행 ─────────────────────────────────────────────────────────────
29
+
30
+ interface RunRowProps {
31
+ run: TaskRun;
32
+ agents: TaskAgent[];
33
+ isLatest: boolean;
34
+ canRerunAgent?: boolean;
35
+ rerunDisabled?: boolean;
36
+ onRerunAgent?: (agentId: number) => void;
37
+ }
38
+
39
+ function RunRow({ run, agents, isLatest, canRerunAgent, rerunDisabled, onRerunAgent }: RunRowProps) {
40
+ const duration = run.completedAt
41
+ ? Math.round((new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) / 1000)
42
+ : null;
43
+
44
+ const totalCost = run.agentRuns.reduce((s, ar) => s + (ar.costUsd ?? 0), 0);
45
+
46
+ return (
47
+ <div className={[
48
+ "flex flex-col gap-2 rounded-xl border p-3 transition-colors",
49
+ isLatest
50
+ ? "border-blue-500/20 bg-blue-500/[0.04] dark:border-blue-500/20"
51
+ : "border-gray-900/[0.06] bg-gray-900/[0.01] dark:border-white/[0.06] dark:bg-white/[0.01]",
52
+ ].join(" ")}>
53
+ {/* 버전 헤더 */}
54
+ <div className="flex items-center justify-between gap-3">
55
+ <div className="flex items-center gap-2">
56
+ <span className={[
57
+ "flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold",
58
+ isLatest
59
+ ? "bg-blue-500/10 text-blue-600 dark:text-blue-400"
60
+ : "bg-gray-900/[0.06] text-gray-900/50 dark:bg-white/[0.06] dark:text-white/50",
61
+ ].join(" ")}>
62
+ v{run.version}
63
+ </span>
64
+ {isLatest && (
65
+ <span className="text-[10px] text-blue-500/70 dark:text-blue-400/70">최신</span>
66
+ )}
67
+ <RunStatusBadge status={run.status} />
68
+ </div>
69
+
70
+ <div className="flex items-center gap-2 text-[10px] text-gray-900/25 dark:text-white/25">
71
+ {duration != null && <span>{duration}s</span>}
72
+ {totalCost > 0 && <span>${totalCost.toFixed(4)}</span>}
73
+ <span>
74
+ {new Date(run.startedAt).toLocaleString("ko-KR", {
75
+ month: "2-digit", day: "2-digit",
76
+ hour: "2-digit", minute: "2-digit",
77
+ })}
78
+ </span>
79
+ </div>
80
+ </div>
81
+
82
+ {/* 보완 사항 */}
83
+ {run.supplementNote && (
84
+ <p className="rounded-lg border border-amber-500/20 bg-amber-500/[0.05] px-2.5 py-1.5 text-[11px] text-amber-700/80 dark:text-amber-400/80">
85
+ <span className="mr-1 font-medium">보완 사항:</span>
86
+ {run.supplementNote}
87
+ </p>
88
+ )}
89
+
90
+ {/* 에이전트별 실행 결과 */}
91
+ {run.agentRuns.length > 0 && (
92
+ <div className="flex flex-wrap gap-1.5">
93
+ {run.agentRuns.map((ar) => {
94
+ const agent = agents.find((a) => a.id === ar.agentId);
95
+ const statusCfg = RUN_STATUS_CONFIG[ar.status] ?? RUN_STATUS_CONFIG.pending;
96
+ return (
97
+ <div
98
+ key={ar.id}
99
+ className="flex items-center gap-1.5 rounded-lg border border-gray-900/[0.06] bg-gray-900/[0.02] px-2 py-1 dark:border-white/[0.06] dark:bg-white/[0.02]"
100
+ >
101
+ <span className={`h-1.5 w-1.5 shrink-0 rounded-full ${statusCfg.dot}`} />
102
+ {agent ? (
103
+ <AgentRoleBadge role={agent.role} customRole={agent.customRole} />
104
+ ) : (
105
+ <span className="text-[10px] text-gray-900/40 dark:text-white/40">Agent {ar.agentId}</span>
106
+ )}
107
+ {ar.durationMs != null && ar.durationMs > 0 && (
108
+ <span className="text-[10px] text-gray-900/20 dark:text-white/20">
109
+ {(ar.durationMs / 1000).toFixed(1)}s
110
+ </span>
111
+ )}
112
+ {canRerunAgent && (
113
+ <button
114
+ type="button"
115
+ onClick={() => onRerunAgent?.(ar.agentId)}
116
+ disabled={rerunDisabled}
117
+ className="ml-1 rounded-md border border-blue-500/25 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 transition-colors hover:border-blue-400/50 hover:bg-blue-500/[0.08] disabled:opacity-40 dark:text-blue-400"
118
+ title="이 에이전트만 재실행"
119
+ >
120
+ 재실행
121
+ </button>
122
+ )}
123
+ </div>
124
+ );
125
+ })}
126
+ </div>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ // ─── RunHistoryPanel ──────────────────────────────────────────────────────────
133
+
134
+ interface Props {
135
+ taskId: string;
136
+ agents: TaskAgent[];
137
+ canRerunAgent?: boolean;
138
+ rerunDisabled?: boolean;
139
+ onRerunAgent?: (agentId: number) => void;
140
+ }
141
+
142
+ export function RunHistoryPanel({ taskId, agents, canRerunAgent, rerunDisabled, onRerunAgent }: Props) {
143
+ const { runs, loading, error } = useTaskRuns(taskId);
144
+
145
+ if (loading) {
146
+ return (
147
+ <div className="flex flex-col gap-2 py-1">
148
+ {[0, 1].map((i) => (
149
+ <div
150
+ key={i}
151
+ className="h-14 animate-pulse rounded-xl border border-gray-900/[0.05] bg-gray-900/[0.02] dark:border-white/[0.05] dark:bg-white/[0.02]"
152
+ style={{ animationDelay: `${i * 60}ms` }}
153
+ />
154
+ ))}
155
+ </div>
156
+ );
157
+ }
158
+
159
+ if (error) {
160
+ return (
161
+ <p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-600 dark:border-red-900/40 dark:bg-red-950/20 dark:text-red-400">
162
+ {error}
163
+ </p>
164
+ );
165
+ }
166
+
167
+ if (runs.length === 0) {
168
+ return (
169
+ <div className="flex flex-col items-center gap-2 py-8 text-center">
170
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="h-8 w-8 text-gray-900/15 dark:text-white/15">
171
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
172
+ </svg>
173
+ <p className="text-xs text-gray-900/30 dark:text-white/30">실행 기록이 없습니다</p>
174
+ </div>
175
+ );
176
+ }
177
+
178
+ return (
179
+ <div className="flex flex-col gap-2">
180
+ {runs.map((run, idx) => (
181
+ <RunRow
182
+ key={run.id}
183
+ run={run}
184
+ agents={agents}
185
+ isLatest={idx === 0}
186
+ canRerunAgent={canRerunAgent}
187
+ rerunDisabled={rerunDisabled}
188
+ onRerunAgent={onRerunAgent}
189
+ />
190
+ ))}
191
+ </div>
192
+ );
193
+ }
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+
5
+ import { Modal } from "@/components/ui/Modal";
6
+ import { WorkingDirPicker } from "@/components/ui/WorkingDirPicker";
7
+ import { AgentSelectModal } from "@/features/chat/ui/AgentSelectModal";
8
+ import type { AgentId } from "@/features/chat/ui/AgentSelectModal";
9
+ import type { Task } from "../api/tasks.api";
10
+ import { useTaskCreate } from "../hooks/useTaskCreate";
11
+ import { AgentRow } from "./AgentRoleSelect";
12
+
13
+ interface Props {
14
+ open: boolean;
15
+ onClose: () => void;
16
+ onCreated?: (task: Task) => void;
17
+ }
18
+
19
+ export function TaskCreateModal({ open, onClose, onCreated }: Props) {
20
+ const {
21
+ form,
22
+ submitting,
23
+ error,
24
+ setTitle,
25
+ setWorkingDir,
26
+ addRequirement,
27
+ updateRequirement,
28
+ removeRequirement,
29
+ addAgent,
30
+ updateAgent,
31
+ removeAgent,
32
+ submit,
33
+ reset,
34
+ } = useTaskCreate(
35
+ useCallback(
36
+ (task: Task) => {
37
+ onCreated?.(task);
38
+ onClose();
39
+ },
40
+ [onCreated, onClose],
41
+ ),
42
+ );
43
+
44
+ const [agentSelectOpen, setAgentSelectOpen] = useState(false);
45
+
46
+ const handleAgentSelect = (agentId: AgentId) => {
47
+ addAgent(agentId);
48
+ setAgentSelectOpen(false);
49
+ };
50
+
51
+ const handleClose = () => { reset(); onClose(); };
52
+
53
+ return (
54
+ <>
55
+ <AgentSelectModal
56
+ open={agentSelectOpen}
57
+ onClose={() => setAgentSelectOpen(false)}
58
+ onSelect={handleAgentSelect}
59
+ />
60
+ <Modal open={open} onClose={handleClose} title="새 작업 추가" maxWidth="max-w-xl">
61
+ <div className="flex flex-col gap-5">
62
+
63
+ {/* ── 작업 제목 ─────────────────────────────────────────────── */}
64
+ <section className="flex flex-col gap-2">
65
+ <label className="text-[11px] font-medium uppercase tracking-wider text-gray-900/30 dark:text-white/30">
66
+ 작업 목표 <span className="text-orange-500">*</span>
67
+ </label>
68
+ <textarea
69
+ rows={2}
70
+ value={form.title}
71
+ onChange={(e) => setTitle(e.target.value)}
72
+ placeholder="예: 로그인 페이지 UI 구현 및 API 연동"
73
+ className="w-full resize-none rounded-xl border border-gray-900/[0.08] bg-gray-900/[0.03] px-3 py-2.5 text-sm text-gray-900/80 placeholder-gray-900/20 outline-none transition-colors focus:border-orange-500/50 focus:bg-gray-900/[0.05] dark:border-white/[0.08] dark:bg-white/[0.03] dark:text-white/80 dark:placeholder-white/20 dark:focus:bg-white/[0.05]"
74
+ />
75
+ </section>
76
+
77
+ {/* ── 워크 디렉토리 ─────────────────────────────────────────── */}
78
+ <section className="flex flex-col gap-2">
79
+ <label className="text-[11px] font-medium uppercase tracking-wider text-gray-900/30 dark:text-white/30">
80
+ 워크 디렉토리 <span className="text-gray-900/20 normal-case tracking-normal dark:text-white/20">(선택)</span>
81
+ </label>
82
+ <WorkingDirPicker value={form.workingDir} onChange={setWorkingDir} />
83
+ </section>
84
+
85
+ {/* ── 요구사항 ──────────────────────────────────────────────── */}
86
+ <section className="flex flex-col gap-2">
87
+ <div className="flex items-center justify-between">
88
+ <label className="text-[11px] font-medium uppercase tracking-wider text-gray-900/30 dark:text-white/30">
89
+ 요구사항
90
+ </label>
91
+ <button
92
+ type="button"
93
+ onClick={addRequirement}
94
+ className="flex items-center gap-1 rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.03] px-2.5 py-1 text-xs text-gray-900/40 transition-colors hover:border-gray-900/[0.14] hover:bg-gray-900/[0.06] hover:text-gray-900/70 dark:border-white/[0.07] dark:bg-white/[0.03] dark:text-white/40 dark:hover:border-white/[0.14] dark:hover:bg-white/[0.06] dark:hover:text-white/70"
95
+ >
96
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
97
+ <path d="M8.75 3.75a.75.75 0 00-1.5 0v3.5h-3.5a.75.75 0 000 1.5h3.5v3.5a.75.75 0 001.5 0v-3.5h3.5a.75.75 0 000-1.5h-3.5v-3.5z" />
98
+ </svg>
99
+ 항목 추가
100
+ </button>
101
+ </div>
102
+
103
+ {form.requirements.length === 0 ? (
104
+ <div className="flex items-center justify-center rounded-xl border border-dashed border-gray-900/[0.07] py-5 dark:border-white/[0.07]">
105
+ <p className="text-xs text-gray-900/20 dark:text-white/20">요구사항을 추가하세요</p>
106
+ </div>
107
+ ) : (
108
+ <ul className="flex flex-col gap-1.5">
109
+ {form.requirements.map((req, idx) => (
110
+ <li key={req.id} className="flex items-center gap-2">
111
+ <span className="w-5 shrink-0 text-center text-xs text-gray-900/20 dark:text-white/20">{idx + 1}</span>
112
+ <input
113
+ type="text"
114
+ value={req.content}
115
+ onChange={(e) => updateRequirement(req.id, e.target.value)}
116
+ placeholder="요구사항 입력"
117
+ className="flex-1 rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.03] px-3 py-1.5 text-xs text-gray-900/70 placeholder-gray-900/20 outline-none transition-colors focus:border-gray-900/[0.15] focus:bg-gray-900/[0.05] dark:border-white/[0.07] dark:bg-white/[0.03] dark:text-white/70 dark:placeholder-white/20 dark:focus:border-white/[0.15] dark:focus:bg-white/[0.05]"
118
+ />
119
+ <button
120
+ type="button"
121
+ onClick={() => removeRequirement(req.id)}
122
+ className="shrink-0 text-gray-900/20 transition-colors hover:text-red-500 dark:text-white/20 dark:hover:text-red-400"
123
+ >
124
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3.5 w-3.5">
125
+ <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
126
+ </svg>
127
+ </button>
128
+ </li>
129
+ ))}
130
+ </ul>
131
+ )}
132
+ </section>
133
+
134
+ {/* ── 서브 에이전트 ─────────────────────────────────────────── */}
135
+ <section className="flex flex-col gap-2">
136
+ <div className="flex items-center justify-between">
137
+ <label className="text-[11px] font-medium uppercase tracking-wider text-gray-900/30 dark:text-white/30">
138
+ 서브 에이전트
139
+ </label>
140
+ <button
141
+ type="button"
142
+ onClick={() => setAgentSelectOpen(true)}
143
+ className="flex items-center gap-1 rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.03] px-2.5 py-1 text-xs text-gray-900/40 transition-colors hover:border-gray-900/[0.14] hover:bg-gray-900/[0.06] hover:text-gray-900/70 dark:border-white/[0.07] dark:bg-white/[0.03] dark:text-white/40 dark:hover:border-white/[0.14] dark:hover:bg-white/[0.06] dark:hover:text-white/70"
144
+ >
145
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
146
+ <path d="M8.75 3.75a.75.75 0 00-1.5 0v3.5h-3.5a.75.75 0 000 1.5h3.5v3.5a.75.75 0 001.5 0v-3.5h3.5a.75.75 0 000-1.5h-3.5v-3.5z" />
147
+ </svg>
148
+ 에이전트 추가
149
+ </button>
150
+ </div>
151
+
152
+ {form.agents.length === 0 ? (
153
+ <div className="flex items-center justify-center rounded-xl border border-dashed border-gray-900/[0.07] py-5 dark:border-white/[0.07]">
154
+ <p className="text-xs text-gray-900/20 dark:text-white/20">서브 에이전트를 추가하세요</p>
155
+ </div>
156
+ ) : (
157
+ <ul className="flex flex-col gap-2">
158
+ {form.agents.map((agent) => (
159
+ <li key={agent.id}>
160
+ <AgentRow
161
+ agent={agent}
162
+ onChange={(patch) => updateAgent(agent.id, patch)}
163
+ onRemove={() => removeAgent(agent.id)}
164
+ />
165
+ </li>
166
+ ))}
167
+ </ul>
168
+ )}
169
+ </section>
170
+
171
+ {/* ── 에러 ──────────────────────────────────────────────────── */}
172
+ {error && (
173
+ <p className="rounded-xl border border-red-900/50 bg-red-950/30 px-3 py-2.5 text-xs text-red-500 dark:text-red-400">
174
+ {error}
175
+ </p>
176
+ )}
177
+
178
+ {/* ── 액션 ──────────────────────────────────────────────────── */}
179
+ <div className="flex justify-end gap-2 border-t border-gray-900/[0.06] pt-4 dark:border-white/[0.06]">
180
+ <button
181
+ type="button"
182
+ onClick={handleClose}
183
+ disabled={submitting}
184
+ className="rounded-xl px-4 py-2 text-sm text-gray-900/35 transition-colors hover:bg-gray-900/[0.05] hover:text-gray-900/70 disabled:opacity-40 dark:text-white/35 dark:hover:bg-white/[0.05] dark:hover:text-white/70"
185
+ >
186
+ 취소
187
+ </button>
188
+ <button
189
+ type="button"
190
+ onClick={submit}
191
+ disabled={submitting || !form.title.trim()}
192
+ className="flex items-center gap-2 rounded-xl bg-orange-600 px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-orange-500 disabled:cursor-not-allowed disabled:opacity-40"
193
+ >
194
+ {submitting && (
195
+ <span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
196
+ )}
197
+ {submitting ? "생성 중…" : "작업 생성"}
198
+ </button>
199
+ </div>
200
+
201
+ </div>
202
+ </Modal>
203
+ </>
204
+ );
205
+ }