@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,591 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useCallback, useState } from "react";
5
+
6
+ import { Modal } from "@/components/ui/Modal";
7
+ import { isQuotaExceeded } from "@/lib/quota";
8
+ import type { AgentTestCodePreference, Task, TaskStatus } from "../api/tasks.api";
9
+ import { useTaskExecution } from "../hooks/useTaskExecution";
10
+ import { useTaskList } from "../hooks/useTaskList";
11
+ import { AgentRoleBadge } from "./AgentRoleSelect";
12
+ import { AgentOutputPanel } from "./AgentOutputPanel";
13
+ import { ChangelogPanel } from "./ChangelogPanel";
14
+ import { RunHistoryPanel } from "./RunHistoryPanel";
15
+ import { TaskEditModal } from "./TaskEditModal";
16
+
17
+ const TEST_CODE_SUPPLEMENT_NOTE =
18
+ "기존 구현은 유지하면서 이 에이전트가 담당한 변경 사항에 대한 테스트 코드를 작성해주세요. 프로젝트의 기존 테스트 패턴을 따르고 가능한 경우 테스트 실행 결과도 남겨주세요.";
19
+
20
+ // ─── 상태 뱃지 ───────────────────────────────────────────────────────────────
21
+
22
+ const STATUS_CONFIG: Record<
23
+ TaskStatus,
24
+ { label: string; className: string; dot?: string }
25
+ > = {
26
+ pending: { label: "대기", className: "border-gray-900/[0.1] text-gray-900/35 dark:border-white/[0.1] dark:text-white/35", dot: "bg-gray-900/25 dark:bg-white/25" },
27
+ running: { label: "실행 중", className: "border-emerald-500/40 text-emerald-600 bg-emerald-500/[0.08] dark:text-emerald-400", dot: "bg-emerald-500 animate-pulse dark:bg-emerald-400" },
28
+ stopped: { label: "중지됨", className: "border-red-500/40 text-red-600 bg-red-500/[0.08] dark:text-red-400", dot: "bg-red-500 dark:bg-red-400" },
29
+ completed: { label: "완료", className: "border-blue-500/40 text-blue-600 bg-blue-500/[0.08] dark:text-blue-400", dot: "bg-blue-500 dark:bg-blue-400" },
30
+ error: { label: "오류", className: "border-red-500/40 text-red-600 bg-red-500/[0.08] dark:text-red-400", dot: "bg-red-500" },
31
+ };
32
+
33
+ function StatusBadge({ status }: { status: TaskStatus }) {
34
+ const cfg = STATUS_CONFIG[status] ?? STATUS_CONFIG.pending;
35
+ return (
36
+ <span className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[11px] font-medium ${cfg.className}`}>
37
+ {cfg.dot && <span className={`h-1.5 w-1.5 rounded-full ${cfg.dot}`} />}
38
+ {cfg.label}
39
+ </span>
40
+ );
41
+ }
42
+
43
+ // ─── TaskCard ─────────────────────────────────────────────────────────────────
44
+
45
+ interface TaskCardProps {
46
+ task: Task;
47
+ isActioning: boolean;
48
+ expanded: boolean;
49
+ onToggleExpand: () => void;
50
+ onExecute: (preferences: AgentTestCodePreference[]) => void;
51
+ onStop: () => void;
52
+ onEdit: () => void;
53
+ onRerun: (note: string) => void;
54
+ onRerunAgent: (agentId: number, note: string) => void;
55
+ onWriteTestsAgent: (agentId: number) => void;
56
+ onArchive: () => void;
57
+ onDelete: () => void;
58
+ onTaskStatusChange: (taskId: string, status: TaskStatus) => void;
59
+ }
60
+
61
+ function TaskCard({
62
+ task,
63
+ isActioning,
64
+ expanded,
65
+ onToggleExpand,
66
+ onExecute,
67
+ onStop,
68
+ onEdit,
69
+ onRerun,
70
+ onRerunAgent,
71
+ onWriteTestsAgent,
72
+ onArchive,
73
+ onDelete,
74
+ onTaskStatusChange,
75
+ }: TaskCardProps) {
76
+ const isRunning = task.status === "running";
77
+ const isFinished = task.status === "completed" || task.status === "error" || task.status === "stopped";
78
+ const canExecute = task.status === "pending" || task.status === "stopped";
79
+ const canStop = isRunning;
80
+ const canRerun = isFinished;
81
+ const showLogs = expanded && (isRunning || isFinished);
82
+
83
+ const [rerunMode, setRerunMode] = useState(false);
84
+ const [rerunAgentId, setRerunAgentId] = useState<number | null>(null);
85
+ const [supplementNote, setSupplementNote] = useState("");
86
+ const [activeTab, setActiveTab] = useState<"logs" | "changelog" | "history">("logs");
87
+ const [changelogMounted, setChangelogMounted] = useState(false);
88
+ const [historyMounted, setHistoryMounted] = useState(false);
89
+ const [testCodeByAgent, setTestCodeByAgent] = useState<Record<number, boolean>>({});
90
+
91
+ const handleRerunConfirm = () => {
92
+ if (rerunAgentId != null) {
93
+ onRerunAgent(rerunAgentId, supplementNote);
94
+ } else {
95
+ onRerun(supplementNote);
96
+ }
97
+ setRerunMode(false);
98
+ setRerunAgentId(null);
99
+ setSupplementNote("");
100
+ };
101
+
102
+ const handleRerunCancel = () => {
103
+ setRerunMode(false);
104
+ setRerunAgentId(null);
105
+ setSupplementNote("");
106
+ };
107
+
108
+ const openAgentRerun = (agentId: number) => {
109
+ setRerunAgentId(agentId);
110
+ setRerunMode(true);
111
+ };
112
+
113
+ const handleExecute = () => {
114
+ onExecute(
115
+ task.agents.map((agent) => ({
116
+ agentId: agent.id,
117
+ writeTestCode: Boolean(testCodeByAgent[agent.id]),
118
+ })),
119
+ );
120
+ };
121
+
122
+ const toggleTestCode = (agentId: number) => {
123
+ setTestCodeByAgent((prev) => ({ ...prev, [agentId]: !prev[agentId] }));
124
+ };
125
+
126
+ const { agentLogs, connected } = useTaskExecution(
127
+ showLogs ? task.id : null,
128
+ onTaskStatusChange,
129
+ );
130
+
131
+ const hasQuotaError = Object.values(agentLogs).some((log) =>
132
+ isQuotaExceeded((log.output ?? "") + (log.errorMessage ?? "")),
133
+ );
134
+ const selectedRerunAgent = rerunAgentId != null ? task.agents.find((agent) => agent.id === rerunAgentId) : null;
135
+ const selectedRerunLabel = selectedRerunAgent
136
+ ? selectedRerunAgent.customRole ?? selectedRerunAgent.role
137
+ : null;
138
+
139
+ return (
140
+ <article className={[
141
+ "flex flex-col rounded-xl border transition-colors",
142
+ isRunning
143
+ ? "border-emerald-500/20 bg-emerald-500/[0.03]"
144
+ : "border-gray-900/[0.07] bg-gray-900/[0.02] hover:border-gray-900/[0.11] dark:border-white/[0.07] dark:bg-white/[0.02] dark:hover:border-white/[0.11]",
145
+ ].join(" ")}>
146
+ {/* 카드 헤더 */}
147
+ <button
148
+ type="button"
149
+ onClick={onToggleExpand}
150
+ className="flex items-start justify-between gap-3 rounded-xl p-4 text-left"
151
+ >
152
+ <div className="flex min-w-0 flex-col gap-1">
153
+ <div className="flex items-center gap-2">
154
+ <h3 className="truncate text-sm font-medium text-gray-900/80 dark:text-white/80">{task.title}</h3>
155
+ {task.agents.length > 0 && (
156
+ <span className="shrink-0 rounded-full border border-gray-900/[0.08] bg-gray-900/[0.04] px-1.5 py-0.5 text-[10px] text-gray-900/30 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-white/30">
157
+ 에이전트 {task.agents.length}
158
+ </span>
159
+ )}
160
+ </div>
161
+ <p className="font-mono text-[10px] text-gray-900/20 dark:text-white/20">{task.id.slice(0, 8)}…</p>
162
+ </div>
163
+ <div className="flex items-center gap-2">
164
+ <StatusBadge status={task.status as TaskStatus} />
165
+ {hasQuotaError && (
166
+ <span className="flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/[0.08] px-2 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400">
167
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-2.5 w-2.5">
168
+ <path fillRule="evenodd" d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z" clipRule="evenodd" />
169
+ </svg>
170
+ 한도 초과
171
+ </span>
172
+ )}
173
+ <svg
174
+ viewBox="0 0 16 16"
175
+ fill="currentColor"
176
+ className={`h-3.5 w-3.5 shrink-0 text-gray-900/20 transition-transform dark:text-white/20 ${expanded ? "rotate-180" : ""}`}
177
+ >
178
+ <path fillRule="evenodd" d="M4.22 6.22a.75.75 0 011.06 0L8 8.94l2.72-2.72a.75.75 0 111.06 1.06l-3.25 3.25a.75.75 0 01-1.06 0L4.22 7.28a.75.75 0 010-1.06z" clipRule="evenodd" />
179
+ </svg>
180
+ </div>
181
+ </button>
182
+
183
+ {/* 확장 영역 — grid-rows 트랜지션으로 height fade + opacity fade */}
184
+ <div
185
+ className={[
186
+ "grid transition-all duration-200 ease-out",
187
+ expanded ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
188
+ ].join(" ")}
189
+ >
190
+ <div className="overflow-hidden">
191
+ <div className="flex flex-col gap-3 px-4 pb-4">
192
+ {/* 요구사항 */}
193
+ {task.requirements.length > 0 && (
194
+ <ul className="flex flex-col gap-0.5">
195
+ {[...task.requirements]
196
+ .sort((a, b) => a.orderIndex - b.orderIndex)
197
+ .map((r) => (
198
+ <li key={r.id} className="flex items-start gap-1.5 text-xs text-gray-900/35 dark:text-white/35">
199
+ <span className="mt-0.5 text-gray-900/15 dark:text-white/15">•</span>
200
+ <span>{r.content}</span>
201
+ </li>
202
+ ))}
203
+ </ul>
204
+ )}
205
+
206
+ {/* 에이전트 역할 */}
207
+ {task.agents.length > 0 && !showLogs && (
208
+ <div className="flex flex-wrap gap-1.5">
209
+ {task.agents.map((a) => (
210
+ <AgentRoleBadge key={a.id} role={a.role} customRole={a.customRole} />
211
+ ))}
212
+ </div>
213
+ )}
214
+
215
+ {/* 실행 전 테스트 코드 작성 여부 */}
216
+ {canExecute && task.agents.length > 0 && (
217
+ <div className="flex flex-col gap-2 rounded-xl border border-gray-900/[0.06] bg-white/40 p-3 dark:border-white/[0.06] dark:bg-white/[0.03]">
218
+ <div className="flex items-center justify-between gap-3">
219
+ <p className="text-xs font-medium text-gray-900/55 dark:text-white/55">실행 전 테스트 코드 작성 여부</p>
220
+ <span className="text-[10px] text-gray-900/25 dark:text-white/25">에이전트별 선택</span>
221
+ </div>
222
+ <div className="grid gap-1.5 sm:grid-cols-2">
223
+ {task.agents.map((agent) => (
224
+ <label
225
+ key={agent.id}
226
+ className="flex min-h-9 cursor-pointer items-center justify-between gap-2 rounded-lg border border-gray-900/[0.06] bg-gray-900/[0.02] px-2.5 py-1.5 transition-colors hover:border-emerald-500/25 hover:bg-emerald-500/[0.05] dark:border-white/[0.06] dark:bg-white/[0.02]"
227
+ >
228
+ <span className="min-w-0">
229
+ <AgentRoleBadge role={agent.role} customRole={agent.customRole} />
230
+ </span>
231
+ <span className="flex shrink-0 items-center gap-1.5 text-[11px] font-medium text-gray-900/45 dark:text-white/45">
232
+ <input
233
+ type="checkbox"
234
+ checked={Boolean(testCodeByAgent[agent.id])}
235
+ onChange={() => toggleTestCode(agent.id)}
236
+ className="h-3.5 w-3.5 accent-emerald-600"
237
+ />
238
+ 테스트 코드
239
+ </span>
240
+ </label>
241
+ ))}
242
+ </div>
243
+ </div>
244
+ )}
245
+
246
+ {/* 실행 로그 / 변경사항 탭 */}
247
+ {showLogs && (
248
+ <div className="flex flex-col gap-2">
249
+ {isFinished && (
250
+ <div className="flex gap-1 rounded-lg border border-gray-900/[0.06] bg-gray-900/[0.02] p-0.5 dark:border-white/[0.06] dark:bg-white/[0.02]">
251
+ {(["logs", "changelog", "history"] as const).map((tab) => (
252
+ <button
253
+ key={tab}
254
+ type="button"
255
+ onClick={() => {
256
+ setActiveTab(tab);
257
+ if (tab === "changelog") setChangelogMounted(true);
258
+ if (tab === "history") setHistoryMounted(true);
259
+ }}
260
+ className={[
261
+ "flex-1 rounded-md px-3 py-1 text-xs font-medium transition-colors",
262
+ activeTab === tab
263
+ ? "bg-white text-gray-900/70 shadow-sm dark:bg-white/[0.08] dark:text-white/70"
264
+ : "text-gray-900/35 hover:text-gray-900/60 dark:text-white/35 dark:hover:text-white/60",
265
+ ].join(" ")}
266
+ >
267
+ {tab === "logs" ? "실행 로그" : tab === "changelog" ? "변경사항" : "실행 기록"}
268
+ </button>
269
+ ))}
270
+ </div>
271
+ )}
272
+
273
+ <div className={activeTab !== "logs" ? "hidden" : ""}>
274
+ <AgentOutputPanel
275
+ agents={task.agents}
276
+ agentLogs={agentLogs}
277
+ connected={connected}
278
+ taskStatus={task.status as TaskStatus}
279
+ canRerun={canRerun}
280
+ rerunDisabled={isActioning}
281
+ onRerunAgent={openAgentRerun}
282
+ onWriteTestsAgent={onWriteTestsAgent}
283
+ />
284
+ </div>
285
+ {changelogMounted && isFinished && (
286
+ <div className={activeTab !== "changelog" ? "hidden" : ""}>
287
+ <ChangelogPanel taskId={task.id} agents={task.agents} />
288
+ </div>
289
+ )}
290
+ {historyMounted && isFinished && (
291
+ <div className={activeTab !== "history" ? "hidden" : ""}>
292
+ <RunHistoryPanel
293
+ taskId={task.id}
294
+ agents={task.agents}
295
+ canRerunAgent={canRerun}
296
+ rerunDisabled={isActioning}
297
+ onRerunAgent={openAgentRerun}
298
+ />
299
+ </div>
300
+ )}
301
+ </div>
302
+ )}
303
+
304
+ {/* 재 실행 보완 입력 패널 */}
305
+ {rerunMode && (
306
+ <div className="flex flex-col gap-2 rounded-xl border border-blue-500/20 bg-blue-500/[0.05] p-3">
307
+ <label className="text-xs font-medium text-blue-600 dark:text-blue-400/80">
308
+ {selectedRerunLabel ? `${selectedRerunLabel} 에이전트 재실행` : "전체 재실행"}
309
+ 보완할 점 입력
310
+ <span className="ml-1 text-[10px] font-normal text-gray-900/25 dark:text-white/25">(선택 - 비워두면 동일 조건으로 재실행)</span>
311
+ </label>
312
+ <textarea
313
+ rows={3}
314
+ value={supplementNote}
315
+ onChange={(e) => setSupplementNote(e.target.value)}
316
+ placeholder="예: 에러 핸들링이 빠져 있습니다. 로딩 상태도 추가해주세요."
317
+ autoFocus
318
+ className="w-full resize-none rounded-lg border border-gray-900/[0.08] bg-gray-900/[0.03] px-3 py-2 text-xs text-gray-900/70 placeholder-gray-900/20 outline-none focus:border-blue-500/50 dark:border-white/[0.08] dark:bg-white/[0.03] dark:text-white/70 dark:placeholder-white/20"
319
+ />
320
+ <div className="flex justify-end gap-2">
321
+ <button
322
+ type="button"
323
+ onClick={handleRerunCancel}
324
+ className="rounded-lg px-3 py-1.5 text-xs text-gray-900/35 transition-colors hover:text-gray-900/70 dark:text-white/35 dark:hover:text-white/70"
325
+ >
326
+ 취소
327
+ </button>
328
+ <button
329
+ type="button"
330
+ onClick={handleRerunConfirm}
331
+ disabled={isActioning}
332
+ className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-40"
333
+ >
334
+ {isActioning ? (
335
+ <span className="h-3 w-3 animate-spin rounded-full border border-white/30 border-t-white" />
336
+ ) : (
337
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
338
+ <path fillRule="evenodd" d="M13.836 2.477a.75.75 0 01.75.75v3.182a.75.75 0 01-.75.75h-3.182a.75.75 0 010-1.5h1.37A5.995 5.995 0 008 4a6 6 0 100 12 6 6 0 005.812-4.5h1.539A7.5 7.5 0 118 2.5c1.373 0 2.663.372 3.772 1.021l.314-.814a.75.75 0 01.75-.23z" clipRule="evenodd" />
339
+ </svg>
340
+ )}
341
+ 재 실행
342
+ </button>
343
+ </div>
344
+ </div>
345
+ )}
346
+
347
+ {/* 액션 버튼 */}
348
+ <div className="flex items-center justify-between border-t border-gray-900/[0.05] pt-3 dark:border-white/[0.05]">
349
+ <span className="text-[10px] text-gray-900/20 dark:text-white/20">
350
+ {new Date(task.createdAt).toLocaleString("ko-KR", {
351
+ month: "2-digit", day: "2-digit",
352
+ hour: "2-digit", minute: "2-digit",
353
+ })}
354
+ </span>
355
+ <div className="flex items-center gap-1.5">
356
+ {/* 실행 (pending/stopped) */}
357
+ {canExecute && (
358
+ <button
359
+ onClick={handleExecute}
360
+ disabled={isActioning}
361
+ className="flex items-center gap-1.5 rounded-lg bg-emerald-600/80 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-emerald-500 disabled:opacity-40"
362
+ >
363
+ {isActioning ? (
364
+ <span className="h-3 w-3 animate-spin rounded-full border border-white/30 border-t-white" />
365
+ ) : (
366
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
367
+ <path d="M6.3 2.841A1.5 1.5 0 004 4.11v7.78a1.5 1.5 0 002.3 1.269l5.773-3.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
368
+ </svg>
369
+ )}
370
+ 실행
371
+ </button>
372
+ )}
373
+
374
+ {/* 중지 (running) */}
375
+ {canStop && (
376
+ <button
377
+ onClick={onStop}
378
+ disabled={isActioning}
379
+ className="flex items-center gap-1.5 rounded-lg bg-red-600/80 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-500 disabled:opacity-40"
380
+ >
381
+ {isActioning ? (
382
+ <span className="h-3 w-3 animate-spin rounded-full border border-white/30 border-t-white" />
383
+ ) : (
384
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
385
+ <path d="M5.25 3A2.25 2.25 0 003 5.25v5.5A2.25 2.25 0 005.25 13h5.5A2.25 2.25 0 0013 10.75v-5.5A2.25 2.25 0 0010.75 3h-5.5z" />
386
+ </svg>
387
+ )}
388
+ 중지
389
+ </button>
390
+ )}
391
+
392
+ {/* 재 실행 (completed/error) */}
393
+ {canRerun && !rerunMode && (
394
+ <button
395
+ onClick={() => setRerunMode(true)}
396
+ disabled={isActioning}
397
+ className="flex items-center gap-1.5 rounded-lg border border-blue-500/30 px-3 py-1.5 text-xs font-medium text-blue-600 transition-colors hover:border-blue-400/50 hover:bg-blue-500/[0.08] hover:text-blue-500 disabled:opacity-40 dark:text-blue-400 dark:hover:text-blue-300"
398
+ >
399
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
400
+ <path fillRule="evenodd" d="M13.836 2.477a.75.75 0 01.75.75v3.182a.75.75 0 01-.75.75h-3.182a.75.75 0 010-1.5h1.37A5.995 5.995 0 008 4a6 6 0 100 12 6 6 0 005.812-4.5h1.539A7.5 7.5 0 118 2.5c1.373 0 2.663.372 3.772 1.021l.314-.814a.75.75 0 01.75-.23z" clipRule="evenodd" />
401
+ </svg>
402
+ 재 실행
403
+ </button>
404
+ )}
405
+
406
+ {/* 수정 */}
407
+ {!isFinished && (
408
+ <button
409
+ onClick={onEdit}
410
+ disabled={isActioning || isRunning}
411
+ className="rounded-lg border border-gray-900/[0.08] px-3 py-1.5 text-xs font-medium text-gray-900/40 transition-colors hover:border-gray-900/[0.15] hover:text-gray-900/75 disabled:cursor-not-allowed disabled:opacity-30 dark:border-white/[0.08] dark:text-white/40 dark:hover:border-white/[0.15] dark:hover:text-white/75"
412
+ title={isRunning ? "실행 중에는 수정할 수 없습니다" : "수정"}
413
+ >
414
+ 수정
415
+ </button>
416
+ )}
417
+
418
+ <Link
419
+ href={`/task/${task.id}`}
420
+ target="_blank"
421
+ rel="noopener noreferrer"
422
+ className="rounded-lg border border-gray-900/[0.08] px-3 py-1.5 text-xs font-medium text-gray-900/40 transition-colors hover:border-blue-500/30 hover:text-blue-600 dark:border-white/[0.08] dark:text-white/40 dark:hover:text-blue-400"
423
+ >
424
+ 상세
425
+ </Link>
426
+
427
+ {/* 보관 */}
428
+ <button
429
+ onClick={onArchive}
430
+ disabled={isActioning || isRunning}
431
+ className="rounded-lg border border-gray-900/[0.08] px-3 py-1.5 text-xs font-medium text-gray-900/25 transition-colors hover:border-gray-900/[0.18] hover:text-gray-900/55 disabled:cursor-not-allowed disabled:opacity-30 dark:border-white/[0.08] dark:text-white/25 dark:hover:border-white/[0.18] dark:hover:text-white/55"
432
+ title={isRunning ? "실행 중에는 보관할 수 없습니다" : "보관"}
433
+ >
434
+ 보관
435
+ </button>
436
+
437
+ {/* 삭제 */}
438
+ <button
439
+ onClick={onDelete}
440
+ disabled={isActioning || isRunning}
441
+ className="rounded-lg border border-gray-900/[0.08] px-3 py-1.5 text-xs font-medium text-gray-900/25 transition-colors hover:border-red-500/30 hover:text-red-500 disabled:cursor-not-allowed disabled:opacity-30 dark:border-white/[0.08] dark:text-white/25 dark:hover:text-red-400"
442
+ title={isRunning ? "실행 중에는 삭제할 수 없습니다" : "삭제"}
443
+ >
444
+ 삭제
445
+ </button>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ </article>
452
+ );
453
+ }
454
+
455
+ // ─── TaskListModal ────────────────────────────────────────────────────────────
456
+
457
+ interface Props {
458
+ open: boolean;
459
+ onClose: () => void;
460
+ }
461
+
462
+ export function TaskListModal({ open, onClose }: Props) {
463
+ const {
464
+ tasks,
465
+ loading,
466
+ actioningId,
467
+ error,
468
+ editingTask,
469
+ setEditingTask,
470
+ loadTasks,
471
+ execute,
472
+ stop,
473
+ rerun,
474
+ rerunAgent,
475
+ archive,
476
+ remove,
477
+ onEditDone,
478
+ updateTaskStatus,
479
+ } = useTaskList(open);
480
+
481
+ const [expandedId, setExpandedId] = useState<string | null>(null);
482
+
483
+ const handleToggleExpand = useCallback((taskId: string) => {
484
+ setExpandedId((prev) => (prev === taskId ? null : taskId));
485
+ }, []);
486
+
487
+ if (editingTask) {
488
+ return (
489
+ <TaskEditModal
490
+ task={editingTask}
491
+ onClose={() => setEditingTask(null)}
492
+ onSaved={onEditDone}
493
+ />
494
+ );
495
+ }
496
+
497
+ return (
498
+ <Modal open={open} onClose={onClose} title="작업 목록" maxWidth="max-w-2xl">
499
+ <div className="flex flex-col gap-4">
500
+ {/* 툴바 */}
501
+ <div className="flex items-center justify-between">
502
+ <p className="text-xs text-gray-900/25 dark:text-white/25">
503
+ {loading ? "불러오는 중…" : `총 ${tasks.length}개`}
504
+ </p>
505
+ <button
506
+ onClick={() => void loadTasks()}
507
+ disabled={loading}
508
+ className="flex items-center gap-1.5 rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.02] px-2.5 py-1 text-xs text-gray-900/35 transition-colors hover:border-gray-900/[0.13] hover:bg-gray-900/[0.05] hover:text-gray-900/70 disabled:opacity-40 dark:border-white/[0.07] dark:bg-white/[0.02] dark:text-white/35 dark:hover:border-white/[0.13] dark:hover:bg-white/[0.05] dark:hover:text-white/70"
509
+ >
510
+ <svg viewBox="0 0 16 16" fill="currentColor" className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`}>
511
+ <path fillRule="evenodd" d="M13.836 2.477a.75.75 0 01.75.75v3.182a.75.75 0 01-.75.75h-3.182a.75.75 0 010-1.5h1.37A5.995 5.995 0 008 4a6 6 0 100 12 6 6 0 005.812-4.5h1.539A7.5 7.5 0 118 2.5c1.373 0 2.663.372 3.772 1.021l.314-.814a.75.75 0 01.75-.23z" clipRule="evenodd" />
512
+ </svg>
513
+ 새로고침
514
+ </button>
515
+ </div>
516
+
517
+ {error && (
518
+ <p className="rounded-xl border border-red-200 bg-red-50 px-3 py-2.5 text-xs text-red-600 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-400">
519
+ {error}
520
+ </p>
521
+ )}
522
+
523
+ {/* 로딩 스켈레톤 */}
524
+ {loading && tasks.length === 0 && (
525
+ <div className="flex flex-col gap-2">
526
+ {[0, 1, 2].map((i) => (
527
+ <div
528
+ key={i}
529
+ className="h-[68px] 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]"
530
+ style={{ animationDelay: `${i * 80}ms` }}
531
+ />
532
+ ))}
533
+ </div>
534
+ )}
535
+
536
+ {/* 빈 상태 */}
537
+ {!loading && tasks.length === 0 && (
538
+ <div className="flex flex-col items-center justify-center gap-3 py-16 text-center">
539
+ <div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-gray-900/[0.07] bg-gray-900/[0.03] dark:border-white/[0.07] dark:bg-white/[0.03]">
540
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="h-6 w-6 text-gray-900/20 dark:text-white/20">
541
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V19.5a2.25 2.25 0 002.25 2.25h.75" />
542
+ </svg>
543
+ </div>
544
+ <div>
545
+ <p className="text-sm text-gray-900/35 dark:text-white/35">생성된 작업이 없습니다</p>
546
+ <p className="mt-1 text-xs text-gray-900/20 dark:text-white/20">
547
+ 사이드바의 &ldquo;+ 작업 추가&rdquo; 버튼으로 작업을 만들어보세요
548
+ </p>
549
+ </div>
550
+ </div>
551
+ )}
552
+
553
+ {tasks.length > 0 && (
554
+ <ul className="flex flex-col gap-2">
555
+ {tasks.map((task) => (
556
+ <li key={task.id}>
557
+ <TaskCard
558
+ task={task}
559
+ isActioning={actioningId === task.id}
560
+ expanded={expandedId === task.id}
561
+ onToggleExpand={() => handleToggleExpand(task.id)}
562
+ onExecute={(preferences) => {
563
+ void execute(task.id, preferences);
564
+ setExpandedId(task.id);
565
+ }}
566
+ onStop={() => void stop(task.id)}
567
+ onEdit={() => setEditingTask(task)}
568
+ onRerun={(note) => {
569
+ void rerun(task.id, note || undefined);
570
+ setExpandedId(task.id);
571
+ }}
572
+ onRerunAgent={(agentId, note) => {
573
+ void rerunAgent(task.id, agentId, note || undefined);
574
+ setExpandedId(task.id);
575
+ }}
576
+ onWriteTestsAgent={(agentId) => {
577
+ void rerunAgent(task.id, agentId, TEST_CODE_SUPPLEMENT_NOTE, true);
578
+ setExpandedId(task.id);
579
+ }}
580
+ onArchive={() => void archive(task.id)}
581
+ onDelete={() => void remove(task.id)}
582
+ onTaskStatusChange={updateTaskStatus}
583
+ />
584
+ </li>
585
+ ))}
586
+ </ul>
587
+ )}
588
+ </div>
589
+ </Modal>
590
+ );
591
+ }