@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,243 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import type { Task } from "../../api/tasks.api";
6
+ import { useTaskExecution } from "../../hooks/useTaskExecution";
7
+ import { useTaskList } from "../../hooks/useTaskList";
8
+ import { TaskListModal } from "../TaskListModal";
9
+
10
+ vi.mock("../../hooks/useTaskList", () => ({
11
+ useTaskList: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("../../hooks/useTaskExecution", () => ({
15
+ useTaskExecution: vi.fn(),
16
+ }));
17
+
18
+ vi.mock("../AgentOutputPanel", () => ({
19
+ AgentOutputPanel: ({
20
+ connected,
21
+ onWriteTestsAgent,
22
+ }: {
23
+ connected: boolean;
24
+ onWriteTestsAgent?: (agentId: number) => void;
25
+ }) => (
26
+ <div data-testid="agent-output">
27
+ connected:{String(connected)}
28
+ <button type="button" onClick={() => onWriteTestsAgent?.(1)}>
29
+ 테스트 작성
30
+ </button>
31
+ </div>
32
+ ),
33
+ }));
34
+
35
+ vi.mock("../ChangelogPanel", () => ({
36
+ ChangelogPanel: ({ taskId }: { taskId: string }) => <div data-testid="changelog">changes:{taskId}</div>,
37
+ }));
38
+
39
+ vi.mock("../RunHistoryPanel", () => ({
40
+ RunHistoryPanel: ({ taskId }: { taskId: string }) => <div data-testid="history">history:{taskId}</div>,
41
+ }));
42
+
43
+ const mockUseTaskList = vi.mocked(useTaskList);
44
+ const mockUseTaskExecution = vi.mocked(useTaskExecution);
45
+
46
+ function task(overrides: Partial<Task> = {}): Task {
47
+ return {
48
+ id: "task-123456789",
49
+ title: "Build feature",
50
+ status: "pending",
51
+ workingDir: null,
52
+ requirements: [{ id: 1, content: "Requirement", status: "pending", orderIndex: 0 }],
53
+ agents: [{ id: 1, agentType: "claude", role: "frontend", customRole: null, status: "pending" }],
54
+ createdAt: "2024-01-01T00:00:00.000Z",
55
+ updatedAt: "2024-01-01T00:00:00.000Z",
56
+ ...overrides,
57
+ };
58
+ }
59
+
60
+ function setup(overrides: Partial<ReturnType<typeof useTaskList>> = {}) {
61
+ const defaults = {
62
+ tasks: [task()],
63
+ loading: false,
64
+ actioningId: null,
65
+ error: null,
66
+ editingTask: null,
67
+ setEditingTask: vi.fn(),
68
+ loadTasks: vi.fn(),
69
+ execute: vi.fn(),
70
+ stop: vi.fn(),
71
+ rerun: vi.fn(),
72
+ rerunAgent: vi.fn(),
73
+ archive: vi.fn(),
74
+ remove: vi.fn(),
75
+ onEditDone: vi.fn(),
76
+ updateTaskStatus: vi.fn(),
77
+ };
78
+ const value = { ...defaults, ...overrides };
79
+ mockUseTaskList.mockReturnValue(value);
80
+ mockUseTaskExecution.mockReturnValue({ agentLogs: {}, connected: true });
81
+ render(<TaskListModal open={true} onClose={vi.fn()} />);
82
+ return value;
83
+ }
84
+
85
+ afterEach(() => { vi.clearAllMocks(); });
86
+
87
+ describe("TaskListModal", () => {
88
+ it("does not render when closed", () => {
89
+ mockUseTaskList.mockReturnValue({
90
+ tasks: [],
91
+ loading: false,
92
+ actioningId: null,
93
+ error: null,
94
+ editingTask: null,
95
+ setEditingTask: vi.fn(),
96
+ loadTasks: vi.fn(),
97
+ execute: vi.fn(),
98
+ stop: vi.fn(),
99
+ rerun: vi.fn(),
100
+ rerunAgent: vi.fn(),
101
+ archive: vi.fn(),
102
+ remove: vi.fn(),
103
+ onEditDone: vi.fn(),
104
+ updateTaskStatus: vi.fn(),
105
+ });
106
+ render(<TaskListModal open={false} onClose={vi.fn()} />);
107
+ expect(screen.queryByText("작업 목록")).not.toBeInTheDocument();
108
+ });
109
+
110
+ it("renders empty, loading, and error states", () => {
111
+ const { rerender, container } = render(<TaskListModal open={true} onClose={vi.fn()} />);
112
+ mockUseTaskList.mockReturnValue({
113
+ tasks: [],
114
+ loading: true,
115
+ actioningId: null,
116
+ error: null,
117
+ editingTask: null,
118
+ setEditingTask: vi.fn(),
119
+ loadTasks: vi.fn(),
120
+ execute: vi.fn(),
121
+ stop: vi.fn(),
122
+ rerun: vi.fn(),
123
+ rerunAgent: vi.fn(),
124
+ archive: vi.fn(),
125
+ remove: vi.fn(),
126
+ onEditDone: vi.fn(),
127
+ updateTaskStatus: vi.fn(),
128
+ });
129
+ rerender(<TaskListModal open={true} onClose={vi.fn()} />);
130
+ expect(container.querySelectorAll(".animate-pulse")).toHaveLength(3);
131
+
132
+ mockUseTaskList.mockReturnValue({
133
+ tasks: [],
134
+ loading: false,
135
+ actioningId: null,
136
+ error: "load failed",
137
+ editingTask: null,
138
+ setEditingTask: vi.fn(),
139
+ loadTasks: vi.fn(),
140
+ execute: vi.fn(),
141
+ stop: vi.fn(),
142
+ rerun: vi.fn(),
143
+ rerunAgent: vi.fn(),
144
+ archive: vi.fn(),
145
+ remove: vi.fn(),
146
+ onEditDone: vi.fn(),
147
+ updateTaskStatus: vi.fn(),
148
+ });
149
+ rerender(<TaskListModal open={true} onClose={vi.fn()} />);
150
+ expect(screen.getByText("load failed")).toBeInTheDocument();
151
+ expect(screen.getByText("생성된 작업이 없습니다")).toBeInTheDocument();
152
+ });
153
+
154
+ it("refreshes, expands a task, executes, edits, archives, and deletes", async () => {
155
+ const user = userEvent.setup();
156
+ const api = setup();
157
+
158
+ await user.click(screen.getByRole("button", { name: /새로고침/ }));
159
+ expect(api.loadTasks).toHaveBeenCalledTimes(1);
160
+
161
+ await user.click(screen.getByRole("button", { name: /Build feature/ }));
162
+ expect(screen.getByText("Requirement")).toBeInTheDocument();
163
+ expect(screen.getAllByText("Frontend").length).toBeGreaterThan(0);
164
+
165
+ await user.click(screen.getByRole("button", { name: "실행" }));
166
+ expect(api.execute).toHaveBeenCalledWith("task-123456789", [{ agentId: 1, writeTestCode: false }]);
167
+
168
+ await user.click(screen.getByRole("button", { name: "수정" }));
169
+ expect(api.setEditingTask).toHaveBeenCalledWith(expect.objectContaining({ id: "task-123456789" }));
170
+
171
+ await user.click(screen.getByRole("button", { name: "보관" }));
172
+ expect(api.archive).toHaveBeenCalledWith("task-123456789");
173
+
174
+ await user.click(screen.getByRole("button", { name: "삭제" }));
175
+ expect(api.remove).toHaveBeenCalledWith("task-123456789");
176
+ });
177
+
178
+ it("passes checked test-code preferences when executing", async () => {
179
+ const user = userEvent.setup();
180
+ const api = setup({
181
+ tasks: [
182
+ task({
183
+ agents: [
184
+ { id: 1, agentType: "claude", role: "frontend", customRole: null, status: "pending" },
185
+ { id: 2, agentType: "codex", role: "backend", customRole: null, status: "pending" },
186
+ ],
187
+ }),
188
+ ],
189
+ });
190
+
191
+ await user.click(screen.getByRole("button", { name: /Build feature/ }));
192
+ await user.click(screen.getAllByRole("checkbox")[1]);
193
+ await user.click(screen.getByRole("button", { name: "실행" }));
194
+
195
+ expect(api.execute).toHaveBeenCalledWith("task-123456789", [
196
+ { agentId: 1, writeTestCode: false },
197
+ { agentId: 2, writeTestCode: true },
198
+ ]);
199
+ });
200
+
201
+ it("renders running stop button and logs when expanded", async () => {
202
+ const user = userEvent.setup();
203
+ const api = setup({ tasks: [task({ status: "running" })] });
204
+
205
+ await user.click(screen.getByRole("button", { name: /Build feature/ }));
206
+
207
+ expect(screen.getByTestId("agent-output")).toHaveTextContent("connected:true");
208
+ await user.click(screen.getByRole("button", { name: "중지" }));
209
+ expect(api.stop).toHaveBeenCalledWith("task-123456789");
210
+ });
211
+
212
+ it("switches completed task tabs and submits rerun note", async () => {
213
+ const user = userEvent.setup();
214
+ const api = setup({ tasks: [task({ status: "completed" })] });
215
+
216
+ await user.click(screen.getByRole("button", { name: /Build feature/ }));
217
+ await user.click(screen.getByRole("button", { name: "변경사항" }));
218
+ expect(screen.getByTestId("changelog")).toHaveTextContent("changes:task-123456789");
219
+ await user.click(screen.getByRole("button", { name: "실행 기록" }));
220
+ expect(screen.getByTestId("history")).toHaveTextContent("history:task-123456789");
221
+
222
+ await user.click(screen.getByRole("button", { name: "재 실행" }));
223
+ await user.type(screen.getByPlaceholderText(/에러 핸들링/), "more tests");
224
+ await user.click(screen.getByRole("button", { name: "재 실행" }));
225
+
226
+ await waitFor(() => expect(api.rerun).toHaveBeenCalledWith("task-123456789", "more tests"));
227
+ });
228
+
229
+ it("can request test code for a completed agent", async () => {
230
+ const user = userEvent.setup();
231
+ const api = setup({ tasks: [task({ status: "completed" })] });
232
+
233
+ await user.click(screen.getByRole("button", { name: /Build feature/ }));
234
+ await user.click(screen.getByRole("button", { name: "테스트 작성" }));
235
+
236
+ expect(api.rerunAgent).toHaveBeenCalledWith(
237
+ "task-123456789",
238
+ 1,
239
+ expect.stringContaining("테스트 코드를 작성"),
240
+ true,
241
+ );
242
+ });
243
+ });
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { useRef, useState } from "react";
4
+
5
+ export function useWorkingDir(initial = "") {
6
+ const [path, setPath] = useState(initial);
7
+ const pickerRef = useRef<HTMLInputElement>(null);
8
+
9
+ const openPicker = () => pickerRef.current?.click();
10
+
11
+ const onPickerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
12
+ const file = e.target.files?.[0];
13
+ if (file) {
14
+ // Electron 환경에서는 file.path로 전체 경로 획득
15
+ const fullPath = (file as File & { path?: string }).path;
16
+ if (fullPath) {
17
+ // file.path = /full/path/to/dir/some-file → 파일명 제거해서 디렉토리 경로 추출
18
+ setPath(fullPath.slice(0, fullPath.lastIndexOf("/")));
19
+ } else {
20
+ // 일반 브라우저: webkitRelativePath의 최상위 폴더명만 획득
21
+ setPath(file.webkitRelativePath.split("/")[0]);
22
+ }
23
+ }
24
+ e.target.value = "";
25
+ };
26
+
27
+ return { path, setPath, pickerRef, openPicker, onPickerChange };
28
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { cleanCliOutput, detectPermissionPrompt, stripAnsi } from "../ansi";
4
+
5
+ describe("stripAnsi", () => {
6
+ it("removes CSI sequences", () => {
7
+ expect(stripAnsi("\x1B[32mHello\x1B[0m")).toBe("Hello");
8
+ });
9
+
10
+ it("removes bold/color codes", () => {
11
+ expect(stripAnsi("\x1B[1;34mtext\x1B[0m")).toBe("text");
12
+ });
13
+
14
+ it("normalises CRLF to LF", () => {
15
+ expect(stripAnsi("line1\r\nline2")).toBe("line1\nline2");
16
+ });
17
+
18
+ it("normalises bare CR to LF", () => {
19
+ expect(stripAnsi("line1\rline2")).toBe("line1\nline2");
20
+ });
21
+
22
+ it("passes through plain text unchanged", () => {
23
+ expect(stripAnsi("plain text")).toBe("plain text");
24
+ });
25
+ });
26
+
27
+ describe("cleanCliOutput", () => {
28
+ it("strips ANSI from output", () => {
29
+ expect(cleanCliOutput("\x1B[32mok\x1B[0m")).toBe("ok");
30
+ });
31
+
32
+ it("removes separator lines (─ characters)", () => {
33
+ const raw = "result\n──────────────────────────\nother";
34
+ const cleaned = cleanCliOutput(raw);
35
+ expect(cleaned).not.toContain("──");
36
+ expect(cleaned).toContain("result");
37
+ });
38
+
39
+ it("removes spinner-only lines", () => {
40
+ const raw = "✽ thinking\nactual output";
41
+ const cleaned = cleanCliOutput(raw);
42
+ expect(cleaned).not.toContain("✽");
43
+ expect(cleaned).toContain("actual output");
44
+ });
45
+
46
+ it("removes timing inline noise", () => {
47
+ // "thinking" triggers NOISE_LINE_PATTERNS so the whole line is dropped
48
+ const raw = "response text (3s · ↓ 109 tokens · thinking)";
49
+ expect(cleanCliOutput(raw)).toBe("");
50
+ });
51
+
52
+ it("removes inline timing without noise keyword", () => {
53
+ // Only the timing bracket is present — no token counter or 'thinking'
54
+ const raw = "final response (5s · done)";
55
+ const cleaned = cleanCliOutput(raw);
56
+ expect(cleaned).not.toMatch(/\(\d+s/);
57
+ expect(cleaned).toContain("final response");
58
+ });
59
+
60
+ it("collapses 3+ consecutive blank lines into 2", () => {
61
+ const raw = "a\n\n\n\n\nb";
62
+ expect(cleanCliOutput(raw)).toBe("a\n\nb");
63
+ });
64
+
65
+ it("removes ⎿ output markers", () => {
66
+ expect(cleanCliOutput("⎿ output here")).toBe("output here");
67
+ });
68
+ });
69
+
70
+ describe("detectPermissionPrompt", () => {
71
+ it("detects a bash permission prompt", () => {
72
+ const text = "Bash command\ngit status\nDo you want to proceed? ❯ 1. Yes";
73
+ const result = detectPermissionPrompt(text);
74
+ expect(result).not.toBeNull();
75
+ expect(result?.tool).toBe("Bash");
76
+ expect(result?.command).toBe("git status");
77
+ });
78
+
79
+ it("captures warning line when present", () => {
80
+ const text = "Bash command\nrm -rf /tmp\nContains shell expansion\nDo you want to proceed? ❯ 1. Yes";
81
+ const result = detectPermissionPrompt(text);
82
+ expect(result?.warning).toBe("Contains shell expansion");
83
+ });
84
+
85
+ it("returns null for non-permission text", () => {
86
+ expect(detectPermissionPrompt("Just a normal response")).toBeNull();
87
+ });
88
+ });
@@ -0,0 +1,105 @@
1
+ // ─── ANSI 이스케이프 제거 ────────────────────────────────────────────────────
2
+
3
+ // eslint-disable-next-line no-control-regex
4
+ const ANSI_RE = /\x1B\[[0-9;?]*[A-Za-z]|\x1B\][^\x07]*\x07|\x1B[^[\]]/g;
5
+
6
+ export function stripAnsi(raw: string): string {
7
+ return raw
8
+ .replace(ANSI_RE, "")
9
+ .replace(/\r\n/g, "\n")
10
+ .replace(/\r/g, "\n");
11
+ }
12
+
13
+ // ─── Claude CLI 노이즈 패턴 ──────────────────────────────────────────────────
14
+
15
+ /** 한 줄 전체가 노이즈인 경우 */
16
+ const NOISE_LINE_PATTERNS: RegExp[] = [
17
+ // 스피너 · 진행 표시
18
+ /Skedaddling…/,
19
+ /thinking/i,
20
+ // 토큰 카운터
21
+ /[↑↓]\s*\d+\s*tokens/,
22
+ // 키보드 힌트
23
+ /esc to interrupt/i,
24
+ /ctrl\+[a-z] to/i,
25
+ /Tab to amend/i,
26
+ // 구분선 (─ 계열 문자가 줄의 60% 이상)
27
+ /^[─━═\-\s❯◀▶]+$/,
28
+ // 스피너 아이콘만 있는 줄
29
+ /^[✽✶✢✻·●\s]+$/,
30
+ // 메모리 알림
31
+ /Recalled \d+ memor/i,
32
+ ];
33
+
34
+ /** 줄 내 특정 패턴 제거 (줄 자체는 유지) */
35
+ const INLINE_NOISE_PATTERNS: RegExp[] = [
36
+ // 타이밍 정보: (3s · ↓ 109 tokens · thinking)
37
+ /\(\d+s\s*·[^)]+\)/g,
38
+ // ⎿ 출력 마커
39
+ /⎿\s*/g,
40
+ // 특수 스피너 아이콘
41
+ /[✽✶✢✻]/g,
42
+ ];
43
+
44
+ /** 구분선인지 판단 (─ 계열 50% 이상) */
45
+ function isSeparatorLine(line: string): boolean {
46
+ if (line.trim().length < 5) return false;
47
+ const dashCount = (line.match(/[─━═\-]/g) ?? []).length;
48
+ return dashCount / line.length > 0.5;
49
+ }
50
+
51
+ // ─── 권한 요청 감지 ──────────────────────────────────────────────────────────
52
+
53
+ export interface PermissionPrompt {
54
+ tool: string;
55
+ command: string;
56
+ warning?: string;
57
+ }
58
+
59
+ /**
60
+ * Claude CLI 권한 요청 블록을 감지한다.
61
+ *
62
+ * 패턴:
63
+ * Bash command
64
+ * <command>
65
+ * Contains simple_expansion ← 선택적 경고
66
+ * Do you want to proceed? ❯ 1. Yes …
67
+ */
68
+ export function detectPermissionPrompt(text: string): PermissionPrompt | null {
69
+ const match = text.match(
70
+ /(\w+)\s+command\s*\n([^\n]+)\n(?:([^\n]+)\n)?Do you want to proceed\?/i,
71
+ );
72
+ if (!match) return null;
73
+ return { tool: match[1], command: match[2].trim(), warning: match[3]?.trim() };
74
+ }
75
+
76
+ // ─── 메인 클리너 ─────────────────────────────────────────────────────────────
77
+
78
+ export function cleanCliOutput(raw: string): string {
79
+ const text = stripAnsi(raw);
80
+ const lines = text.split("\n");
81
+ const result: string[] = [];
82
+
83
+ for (let line of lines) {
84
+ // 구분선 전체 제거
85
+ if (isSeparatorLine(line)) continue;
86
+
87
+ // 노이즈 줄 전체 제거
88
+ if (NOISE_LINE_PATTERNS.some((p) => p.test(line.trim()))) continue;
89
+
90
+ // 줄 내 노이즈 패턴 제거
91
+ for (const p of INLINE_NOISE_PATTERNS) {
92
+ line = line.replace(p, "");
93
+ }
94
+
95
+ result.push(line);
96
+ }
97
+
98
+ return (
99
+ result
100
+ .join("\n")
101
+ // 3줄 이상 연속 빈 줄 → 2줄로 축소
102
+ .replace(/\n{3,}/g, "\n\n")
103
+ .trim()
104
+ );
105
+ }
@@ -0,0 +1,4 @@
1
+ export const SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL ?? "http://localhost:3001";
2
+ export const CLAUDE_WS_NAMESPACE = "/agents/claude";
3
+ export const GEMINI_WS_NAMESPACE = "/agents/gemini";
4
+ export const CODEX_WS_NAMESPACE = "/agents/codex";
@@ -0,0 +1,22 @@
1
+ const QUOTA_PATTERNS = [
2
+ // Gemini
3
+ /exhausted your daily quota/i,
4
+ /you exceeded your current quota/i,
5
+ /TerminalQuotaError/,
6
+ /daily quota/i,
7
+ /quota exceeded/i,
8
+ /generativelanguage.*free_tier/i,
9
+ // Claude
10
+ /usage limit reached/i,
11
+ /rate.?limit/i,
12
+ /insufficient.?credit/i,
13
+ /credit balance/i,
14
+ /overloaded_error/i,
15
+ /too many requests/i,
16
+ // HTTP 429
17
+ /\b429\b/,
18
+ ];
19
+
20
+ export function isQuotaExceeded(text: string): boolean {
21
+ return QUOTA_PATTERNS.some((p) => p.test(text));
22
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useEffect, useState } from "react";
4
+
5
+ type Theme = "light" | "dark";
6
+
7
+ interface ThemeCtx {
8
+ theme: Theme;
9
+ toggle: () => void;
10
+ }
11
+
12
+ const ThemeContext = createContext<ThemeCtx | null>(null);
13
+
14
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
15
+ const [theme, setTheme] = useState<Theme>("dark");
16
+
17
+ useEffect(() => {
18
+ const saved = localStorage.getItem("theme") as Theme | null;
19
+ const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches
20
+ ? "dark"
21
+ : "light";
22
+ const initial = saved ?? preferred;
23
+ setTheme(initial);
24
+ document.documentElement.classList.toggle("dark", initial === "dark");
25
+ }, []);
26
+
27
+ const toggle = () => {
28
+ setTheme((prev) => {
29
+ const next: Theme = prev === "dark" ? "light" : "dark";
30
+ localStorage.setItem("theme", next);
31
+ document.documentElement.classList.toggle("dark", next === "dark");
32
+ return next;
33
+ });
34
+ };
35
+
36
+ return (
37
+ <ThemeContext.Provider value={{ theme, toggle }}>
38
+ {children}
39
+ </ThemeContext.Provider>
40
+ );
41
+ }
42
+
43
+ export function useTheme() {
44
+ const ctx = useContext(ThemeContext);
45
+ if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
46
+ return ctx;
47
+ }
48
+
49
+ // ─── Toggle Button ────────────────────────────────────────────────────────────
50
+
51
+ export function ThemeToggle({ className = "" }: { className?: string }) {
52
+ const { theme, toggle } = useTheme();
53
+ return (
54
+ <button
55
+ type="button"
56
+ onClick={toggle}
57
+ aria-label={theme === "dark" ? "라이트 모드로 전환" : "다크 모드로 전환"}
58
+ className={[
59
+ "flex h-8 w-8 items-center justify-center rounded-lg transition-colors",
60
+ "text-gray-900/35 hover:bg-gray-900/[0.05] hover:text-gray-900/70",
61
+ "dark:text-white/35 dark:hover:bg-white/[0.06] dark:hover:text-white/70",
62
+ className,
63
+ ].join(" ")}
64
+ >
65
+ {theme === "dark" ? (
66
+ /* Sun icon */
67
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
68
+ <path d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zm4.47 2.53a.75.75 0 011.06 1.06l-1.06 1.06a.75.75 0 01-1.06-1.06l1.06-1.06zm-10 0l1.06 1.06a.75.75 0 01-1.06 1.06L3.47 5.59a.75.75 0 011.06-1.06zM10 5.5a4.5 4.5 0 100 9 4.5 4.5 0 000-9zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zm-16.25.75a.75.75 0 010-1.5h1.5a.75.75 0 010 1.5h-1.5zm12.78 5.72a.75.75 0 011.06-1.06l1.06 1.06a.75.75 0 01-1.06 1.06l-1.06-1.06zm-10 0l-1.06 1.06a.75.75 0 01-1.06-1.06l1.06-1.06a.75.75 0 011.06 1.06zM10 17.25a.75.75 0 01-.75-.75v-1.5a.75.75 0 011.5 0v1.5a.75.75 0 01-.75.75z" />
69
+ </svg>
70
+ ) : (
71
+ /* Moon icon */
72
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
73
+ <path fillRule="evenodd" d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z" clipRule="evenodd" />
74
+ </svg>
75
+ )}
76
+ </button>
77
+ );
78
+ }