@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,105 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { LoginPanel } from "../LoginPanel";
6
+ import type { LoginState } from "../../hooks/useClaudeAuth";
7
+
8
+ function setup(state: LoginState, urls: string[] = [], output = "") {
9
+ const onStart = vi.fn();
10
+ const onCancel = vi.fn();
11
+ render(
12
+ <LoginPanel
13
+ loginState={state}
14
+ loginOutput={output}
15
+ loginUrls={urls}
16
+ onStart={onStart}
17
+ onCancel={onCancel}
18
+ />,
19
+ );
20
+ return { onStart, onCancel };
21
+ }
22
+
23
+ describe("LoginPanel — idle state", () => {
24
+ it("shows login button", () => {
25
+ setup("idle");
26
+ expect(screen.getByRole("button", { name: "Claude Code 로그인" })).toBeInTheDocument();
27
+ });
28
+
29
+ it("calls onStart when login button is clicked", async () => {
30
+ const user = userEvent.setup();
31
+ const { onStart } = setup("idle");
32
+ await user.click(screen.getByRole("button", { name: "Claude Code 로그인" }));
33
+ expect(onStart).toHaveBeenCalledTimes(1);
34
+ });
35
+ });
36
+
37
+ describe("LoginPanel — pending state (no URL yet)", () => {
38
+ it("shows spinner when no URLs yet", () => {
39
+ setup("pending");
40
+ expect(screen.getByText(/로그인 프로세스를 시작하는 중/)).toBeInTheDocument();
41
+ });
42
+
43
+ it("shows cancel button", () => {
44
+ setup("pending");
45
+ expect(screen.getByRole("button", { name: "취소" })).toBeInTheDocument();
46
+ });
47
+
48
+ it("calls onCancel when cancel is clicked", async () => {
49
+ const user = userEvent.setup();
50
+ const { onCancel } = setup("pending");
51
+ await user.click(screen.getByRole("button", { name: "취소" }));
52
+ expect(onCancel).toHaveBeenCalledTimes(1);
53
+ });
54
+ });
55
+
56
+ describe("LoginPanel — pending state (with URL)", () => {
57
+ it("renders auth URL as link", () => {
58
+ setup("pending", ["https://claude.ai/auth?code=abc"]);
59
+ const link = screen.getByRole("link", { name: "https://claude.ai/auth?code=abc" });
60
+ expect(link).toHaveAttribute("href", "https://claude.ai/auth?code=abc");
61
+ expect(link).toHaveAttribute("target", "_blank");
62
+ });
63
+
64
+ it("shows login output text", () => {
65
+ setup("pending", [], "Waiting for authentication...");
66
+ expect(screen.getByText("Waiting for authentication...")).toBeInTheDocument();
67
+ });
68
+ });
69
+
70
+ describe("LoginPanel — done state", () => {
71
+ it("shows success message", () => {
72
+ setup("done");
73
+ expect(screen.getByText("로그인 완료")).toBeInTheDocument();
74
+ expect(screen.getByText("잠시 후 자동으로 이동합니다…")).toBeInTheDocument();
75
+ });
76
+
77
+ it("does not show the login button", () => {
78
+ setup("done");
79
+ expect(screen.queryByRole("button", { name: "Claude Code 로그인" })).not.toBeInTheDocument();
80
+ });
81
+ });
82
+
83
+ describe("LoginPanel — error state", () => {
84
+ it("shows error message", () => {
85
+ setup("error");
86
+ expect(screen.getByText(/로그인 중 문제가 발생했습니다/)).toBeInTheDocument();
87
+ });
88
+
89
+ it("shows retry button", () => {
90
+ setup("error");
91
+ expect(screen.getByRole("button", { name: "다시 시도" })).toBeInTheDocument();
92
+ });
93
+
94
+ it("calls onStart when retry is clicked", async () => {
95
+ const user = userEvent.setup();
96
+ const { onStart } = setup("error");
97
+ await user.click(screen.getByRole("button", { name: "다시 시도" }));
98
+ expect(onStart).toHaveBeenCalledTimes(1);
99
+ });
100
+
101
+ it("shows error output when provided", () => {
102
+ setup("error", [], "Error: timeout");
103
+ expect(screen.getByText("Error: timeout")).toBeInTheDocument();
104
+ });
105
+ });
@@ -0,0 +1,187 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ createSession,
5
+ deleteCodexSession,
6
+ deleteGeminiSession,
7
+ deleteSession,
8
+ fetchConversations,
9
+ fetchDBSessions,
10
+ saveConversation,
11
+ createCodexSession,
12
+ } from "../sessions.api";
13
+
14
+ const mockFetch = vi.fn();
15
+
16
+ beforeEach(() => { vi.stubGlobal("fetch", mockFetch); });
17
+ afterEach(() => { vi.unstubAllGlobals(); });
18
+
19
+ function ok(body: unknown) {
20
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(body) } as Response);
21
+ }
22
+ function err(status: number) {
23
+ return Promise.resolve({ ok: false, status } as Response);
24
+ }
25
+
26
+ describe("fetchDBSessions", () => {
27
+ it("returns session list", async () => {
28
+ const sessions = [{ sessionId: "abc", title: "My Session", createdAt: "2024-01-01" }];
29
+ mockFetch.mockReturnValueOnce(ok(sessions));
30
+
31
+ const result = await fetchDBSessions();
32
+
33
+ expect(result).toHaveLength(1);
34
+ expect(result[0].sessionId).toBe("abc");
35
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("/sessions"));
36
+ });
37
+
38
+ it("throws on HTTP error", async () => {
39
+ mockFetch.mockReturnValueOnce(err(500));
40
+ await expect(fetchDBSessions()).rejects.toThrow("HTTP 500");
41
+ });
42
+ });
43
+
44
+ describe("createSession", () => {
45
+ const mockSession = {
46
+ id: "sess-1",
47
+ title: "새 세션",
48
+ createdAt: "2024-01-01",
49
+ };
50
+
51
+ it("sends POST without workingDirectory when omitted", async () => {
52
+ mockFetch.mockReturnValueOnce(ok(mockSession));
53
+
54
+ await createSession();
55
+
56
+ expect(mockFetch).toHaveBeenCalledWith(
57
+ expect.stringContaining("/agents/claude/sessions"),
58
+ expect.objectContaining({ method: "POST", body: JSON.stringify({}) }),
59
+ );
60
+ });
61
+
62
+ it("sends POST with workingDirectory when provided", async () => {
63
+ mockFetch.mockReturnValueOnce(ok(mockSession));
64
+
65
+ await createSession("/home/user/project");
66
+
67
+ expect(mockFetch).toHaveBeenCalledWith(
68
+ expect.any(String),
69
+ expect.objectContaining({
70
+ body: JSON.stringify({ workingDirectory: "/home/user/project" }),
71
+ }),
72
+ );
73
+ });
74
+
75
+ it("sends POST with model and reasoning when provided", async () => {
76
+ mockFetch.mockReturnValueOnce(ok(mockSession));
77
+
78
+ await createSession({ workingDirectory: "/home/user/project", model: "sonnet", reasoning: "high" });
79
+
80
+ expect(mockFetch).toHaveBeenCalledWith(
81
+ expect.any(String),
82
+ expect.objectContaining({
83
+ body: JSON.stringify({ workingDirectory: "/home/user/project", model: "sonnet", reasoning: "high" }),
84
+ }),
85
+ );
86
+ });
87
+
88
+ it("returns session info", async () => {
89
+ mockFetch.mockReturnValueOnce(ok(mockSession));
90
+ const result = await createSession();
91
+ expect(result.id).toBe("sess-1");
92
+ });
93
+
94
+ it("throws on HTTP error", async () => {
95
+ mockFetch.mockReturnValueOnce(err(400));
96
+ await expect(createSession()).rejects.toThrow("HTTP 400");
97
+ });
98
+ });
99
+
100
+ describe("createCodexSession", () => {
101
+ it("sends Codex model settings", async () => {
102
+ mockFetch.mockReturnValueOnce(ok({ id: "codex-1", title: "Codex", createdAt: "2024-01-01" }));
103
+
104
+ await createCodexSession({ model: "gpt-5.5", reasoning: "xhigh" });
105
+
106
+ expect(mockFetch).toHaveBeenCalledWith(
107
+ expect.stringContaining("/agents/codex/sessions"),
108
+ expect.objectContaining({
109
+ body: JSON.stringify({ model: "gpt-5.5", reasoning: "xhigh" }),
110
+ }),
111
+ );
112
+ });
113
+ });
114
+
115
+ describe("deleteSession", () => {
116
+ it("sends DELETE to correct URL", async () => {
117
+ mockFetch.mockReturnValueOnce(Promise.resolve({ ok: true } as Response));
118
+
119
+ await deleteSession("sess-123");
120
+
121
+ expect(mockFetch).toHaveBeenCalledWith(
122
+ expect.stringContaining("/agents/claude/sessions/sess-123"),
123
+ expect.objectContaining({ method: "DELETE" }),
124
+ );
125
+ });
126
+
127
+ it("throws when Claude delete fails", async () => {
128
+ mockFetch.mockReturnValueOnce(err(500));
129
+
130
+ await expect(deleteSession("sess-123")).rejects.toThrow("HTTP 500");
131
+ });
132
+
133
+ it("throws when Gemini delete fails", async () => {
134
+ mockFetch.mockReturnValueOnce(err(404));
135
+
136
+ await expect(deleteGeminiSession("sess-123")).rejects.toThrow("HTTP 404");
137
+ });
138
+
139
+ it("throws when Codex delete fails", async () => {
140
+ mockFetch.mockReturnValueOnce(err(409));
141
+
142
+ await expect(deleteCodexSession("sess-123")).rejects.toThrow("HTTP 409");
143
+ });
144
+ });
145
+
146
+ describe("fetchConversations", () => {
147
+ it("returns conversation list for session", async () => {
148
+ const convos = [
149
+ { id: "c1", sessionId: "s1", promptId: "p1", content: "hello", agentModel: "claude", type: "user_message", createdAt: "2024-01-01" },
150
+ ];
151
+ mockFetch.mockReturnValueOnce(ok(convos));
152
+
153
+ const result = await fetchConversations("s1");
154
+
155
+ expect(result).toHaveLength(1);
156
+ expect(mockFetch).toHaveBeenCalledWith(
157
+ expect.stringContaining("/conversations/session/s1"),
158
+ );
159
+ });
160
+
161
+ it("throws on HTTP error", async () => {
162
+ mockFetch.mockReturnValueOnce(err(404));
163
+ await expect(fetchConversations("bad-id")).rejects.toThrow("HTTP 404");
164
+ });
165
+ });
166
+
167
+ describe("saveConversation", () => {
168
+ it("fires POST without awaiting", () => {
169
+ mockFetch.mockReturnValueOnce(ok({}));
170
+
171
+ saveConversation("s1", "p1", "hello", "user_message");
172
+
173
+ expect(mockFetch).toHaveBeenCalledWith(
174
+ expect.stringContaining("/conversations"),
175
+ expect.objectContaining({
176
+ method: "POST",
177
+ body: JSON.stringify({
178
+ sessionId: "s1",
179
+ promptId: "p1",
180
+ content: "hello",
181
+ agentModel: "claude",
182
+ type: "user_message",
183
+ }),
184
+ }),
185
+ );
186
+ });
187
+ });
@@ -0,0 +1,161 @@
1
+ import { SERVER_URL } from "@/lib/constants";
2
+
3
+ // ─── 타입 ────────────────────────────────────────────────────────────────────
4
+
5
+ export interface SessionInfo {
6
+ id: string;
7
+ title: string;
8
+ claudeSessionId?: string | null;
9
+ status?: string;
10
+ workingDirectory?: string;
11
+ model?: string | null;
12
+ reasoning?: string | null;
13
+ createdAt: string;
14
+ }
15
+
16
+ export interface CreateAgentSessionPayload {
17
+ workingDirectory?: string;
18
+ model?: string;
19
+ reasoning?: string;
20
+ }
21
+
22
+ export interface DBSession {
23
+ sessionId: string;
24
+ title: string;
25
+ createdAt: string;
26
+ }
27
+
28
+ export interface DBConversation {
29
+ id: string;
30
+ sessionId: string;
31
+ promptId: string;
32
+ content: string;
33
+ agentModel: string;
34
+ type: "user_message" | "agent_message";
35
+ createdAt: string;
36
+ }
37
+
38
+ export type ConversationType = "user_message" | "agent_message";
39
+
40
+ // ─── Sessions ────────────────────────────────────────────────────────────────
41
+
42
+ export async function fetchDBSessions(agentType?: string): Promise<DBSession[]> {
43
+ const url = agentType
44
+ ? `${SERVER_URL}/sessions?agentType=${agentType}`
45
+ : `${SERVER_URL}/sessions`;
46
+ const res = await fetch(url);
47
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
48
+ return res.json();
49
+ }
50
+
51
+ function compactPayload(payload?: CreateAgentSessionPayload | string): CreateAgentSessionPayload {
52
+ if (typeof payload === "string") {
53
+ return payload ? { workingDirectory: payload } : {};
54
+ }
55
+ return Object.fromEntries(
56
+ Object.entries(payload ?? {}).filter(([, value]) => value !== undefined && value !== ""),
57
+ ) as CreateAgentSessionPayload;
58
+ }
59
+
60
+ export async function createSession(payload?: CreateAgentSessionPayload | string): Promise<SessionInfo> {
61
+ const res = await fetch(`${SERVER_URL}/agents/claude/sessions`, {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify(compactPayload(payload)),
65
+ });
66
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
67
+ return res.json();
68
+ }
69
+
70
+ export async function deleteSession(id: string): Promise<void> {
71
+ const res = await fetch(`${SERVER_URL}/agents/claude/sessions/${id}`, { method: "DELETE" });
72
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
73
+ }
74
+
75
+ export async function createGeminiSession(workingDirectory?: string): Promise<SessionInfo> {
76
+ const res = await fetch(`${SERVER_URL}/agents/gemini/sessions`, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify(workingDirectory ? { workingDirectory } : {}),
80
+ });
81
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
82
+ return res.json();
83
+ }
84
+
85
+ export async function deleteGeminiSession(id: string): Promise<void> {
86
+ const res = await fetch(`${SERVER_URL}/agents/gemini/sessions/${id}`, { method: "DELETE" });
87
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
88
+ }
89
+
90
+ export function saveGeminiConversation(
91
+ sessionId: string,
92
+ promptId: string,
93
+ content: string,
94
+ type: ConversationType,
95
+ ): void {
96
+ void fetch(`${SERVER_URL}/conversations`, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({ sessionId, promptId, content, agentModel: "gemini", type }),
100
+ }).catch(() => undefined);
101
+ }
102
+
103
+ export async function createCodexSession(payload?: CreateAgentSessionPayload | string): Promise<SessionInfo> {
104
+ const res = await fetch(`${SERVER_URL}/agents/codex/sessions`, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify(compactPayload(payload)),
108
+ });
109
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
110
+ return res.json();
111
+ }
112
+
113
+ export async function deleteCodexSession(id: string): Promise<void> {
114
+ const res = await fetch(`${SERVER_URL}/agents/codex/sessions/${id}`, { method: "DELETE" });
115
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
116
+ }
117
+
118
+ export function saveCodexConversation(
119
+ sessionId: string,
120
+ promptId: string,
121
+ content: string,
122
+ type: ConversationType,
123
+ ): void {
124
+ void fetch(`${SERVER_URL}/conversations`, {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/json" },
127
+ body: JSON.stringify({ sessionId, promptId, content, agentModel: "codex", type }),
128
+ }).catch(() => undefined);
129
+ }
130
+
131
+ // ─── Session Title ───────────────────────────────────────────────────────────
132
+
133
+ export async function updateSessionTitle(sessionId: string, title: string): Promise<void> {
134
+ const res = await fetch(`${SERVER_URL}/sessions/${sessionId}/title`, {
135
+ method: 'PATCH',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ title }),
138
+ });
139
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
140
+ }
141
+
142
+ // ─── Conversations ───────────────────────────────────────────────────────────
143
+
144
+ export async function fetchConversations(sessionId: string): Promise<DBConversation[]> {
145
+ const res = await fetch(`${SERVER_URL}/conversations/session/${sessionId}`);
146
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
147
+ return res.json();
148
+ }
149
+
150
+ export function saveConversation(
151
+ sessionId: string,
152
+ promptId: string,
153
+ content: string,
154
+ type: ConversationType,
155
+ ): void {
156
+ void fetch(`${SERVER_URL}/conversations`, {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ sessionId, promptId, content, agentModel: "claude", type }),
160
+ }).catch(() => undefined);
161
+ }
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ import { useClaudeAuth } from "@/features/auth/hooks/useClaudeAuth";
6
+ import { HarnessModal } from "@/features/harness/ui/HarnessModal";
7
+ import { AgentStatusModal } from "@/features/status/ui/AgentStatusModal";
8
+ import { TaskCreateModal } from "@/features/tasks/ui/TaskCreateModal";
9
+ import { TaskListModal } from "@/features/tasks/ui/TaskListModal";
10
+ import { useTaskNotification } from "@/features/tasks/hooks/useTaskNotification";
11
+ import { AgentSelectModal } from "../ui/AgentSelectModal";
12
+ import type { AgentId } from "../ui/AgentSelectModal";
13
+ import { ChatWorkspace } from "../ui/ChatWorkspace";
14
+ import { CheckingSkeleton } from "../ui/CheckingSkeleton";
15
+ import { ClaudeLoginView } from "../ui/ClaudeLoginView";
16
+ import { SessionSidebar } from "../ui/SessionSidebar";
17
+ import { useAgentModelSettings } from "../hooks/useAgentModelSettings";
18
+ import { useSessionCommand } from "../hooks/useSessionCommand";
19
+ import { useSessionRename } from "../hooks/useSessionRename";
20
+ import { useSessionWorkingDirectories } from "../hooks/useSessionWorkingDirectories";
21
+ import { useUnifiedSessions } from "../hooks/useUnifiedSessions";
22
+
23
+ export function ClaudePageContainer() {
24
+ const { authState, loginState, loginOutput, loginUrls, startLogin, cancelLogin, checkAuth } =
25
+ useClaudeAuth();
26
+
27
+ useEffect(() => {
28
+ if (loginState === "done") void checkAuth();
29
+ }, [loginState, checkAuth]);
30
+
31
+ const {
32
+ sessions,
33
+ selectedSession,
34
+ selectedSessionId,
35
+ selectedConnectionStatus,
36
+ overallConnectionStatus,
37
+ error,
38
+ createSession,
39
+ selectSession,
40
+ sendMessage,
41
+ terminateSession,
42
+ renameSession,
43
+ injectClaudeMessage,
44
+ } = useUnifiedSessions();
45
+
46
+ const { sessionDirs, currentDir, handleDirChange, assignDirectoryToSession } =
47
+ useSessionWorkingDirectories(selectedSessionId);
48
+ const { settingsByAgent, updateSettings } = useAgentModelSettings();
49
+ const rename = useSessionRename(renameSession);
50
+ const handleSend = useSessionCommand({
51
+ selectedSession,
52
+ selectedSessionId,
53
+ sendMessage,
54
+ modelSettingsByAgent: settingsByAgent,
55
+ injectClaudeMessage,
56
+ });
57
+ const { hasNew, clearNew } = useTaskNotification();
58
+
59
+ const bottomRef = useRef<HTMLDivElement>(null);
60
+ const [taskModalOpen, setTaskModalOpen] = useState(false);
61
+ const [taskListOpen, setTaskListOpen] = useState(false);
62
+ const [agentSelectOpen, setAgentSelectOpen] = useState(false);
63
+ const [statusModalOpen, setStatusModalOpen] = useState(false);
64
+ const [harnessModalOpen, setHarnessModalOpen] = useState(false);
65
+
66
+ useEffect(() => {
67
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
68
+ }, [selectedSession?.messages, selectedSession?.streaming]);
69
+
70
+ const handleOpenTaskList = () => {
71
+ setTaskListOpen(true);
72
+ clearNew();
73
+ };
74
+
75
+ const handleAgentSelect = (agentId: AgentId) => {
76
+ const dir = currentDir || undefined;
77
+ createSession(agentId, dir, settingsByAgent[agentId]).then((sessionId) => {
78
+ if (sessionId && currentDir) {
79
+ assignDirectoryToSession(sessionId, currentDir);
80
+ }
81
+ });
82
+ };
83
+
84
+ const inputDisabled =
85
+ !selectedSession || selectedSession.isWaiting || selectedConnectionStatus !== "connected";
86
+
87
+ if (authState === "checking") return <CheckingSkeleton />;
88
+
89
+ if (authState === "unauthenticated") {
90
+ return (
91
+ <ClaudeLoginView
92
+ loginState={loginState}
93
+ loginOutput={loginOutput}
94
+ loginUrls={loginUrls}
95
+ onStart={startLogin}
96
+ onCancel={cancelLogin}
97
+ />
98
+ );
99
+ }
100
+
101
+ return (
102
+ <div className="flex h-screen bg-[#faf8f5] text-gray-900 dark:bg-[#07090e] dark:text-white">
103
+ <AgentSelectModal
104
+ open={agentSelectOpen}
105
+ onClose={() => setAgentSelectOpen(false)}
106
+ onSelect={handleAgentSelect}
107
+ />
108
+ <AgentStatusModal open={statusModalOpen} onClose={() => setStatusModalOpen(false)} />
109
+ <HarnessModal open={harnessModalOpen} onClose={() => setHarnessModalOpen(false)} />
110
+ <TaskCreateModal open={taskModalOpen} onClose={() => setTaskModalOpen(false)} />
111
+ <TaskListModal open={taskListOpen} onClose={() => setTaskListOpen(false)} />
112
+
113
+ <SessionSidebar
114
+ sessions={sessions}
115
+ selectedSessionId={selectedSessionId}
116
+ overallConnectionStatus={overallConnectionStatus}
117
+ hasNewTask={hasNew}
118
+ menuOpenId={rename.menuOpenId}
119
+ renamingId={rename.renamingId}
120
+ renameValue={rename.renameValue}
121
+ menuRef={rename.menuRef}
122
+ onSelectSession={selectSession}
123
+ onOpenAgentSelect={() => setAgentSelectOpen(true)}
124
+ onOpenTaskCreate={() => setTaskModalOpen(true)}
125
+ onOpenTaskList={handleOpenTaskList}
126
+ onOpenStatus={() => setStatusModalOpen(true)}
127
+ onOpenHarness={() => setHarnessModalOpen(true)}
128
+ onSetMenuOpenId={rename.setMenuOpenId}
129
+ onStartRename={rename.startRename}
130
+ onRenameValueChange={rename.setRenameValue}
131
+ onConfirmRename={rename.confirmRename}
132
+ onCancelRename={rename.cancelRename}
133
+ />
134
+
135
+ <ChatWorkspace
136
+ selectedSession={selectedSession}
137
+ selectedSessionDir={selectedSession ? (sessionDirs[selectedSession.info.id] ?? "") : ""}
138
+ overallConnectionStatus={overallConnectionStatus}
139
+ currentDir={currentDir}
140
+ error={error}
141
+ inputDisabled={inputDisabled}
142
+ bottomRef={bottomRef}
143
+ modelSettingsByAgent={settingsByAgent}
144
+ onTerminateSession={terminateSession}
145
+ onSend={handleSend}
146
+ onSendMessage={sendMessage}
147
+ onDirChange={handleDirChange}
148
+ onModelSettingsChange={updateSettings}
149
+ />
150
+ </div>
151
+ );
152
+ }