@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,162 @@
1
+ "use client";
2
+
3
+ import { type KeyboardEvent, useEffect, useRef, useState } from "react";
4
+
5
+ // ─── 슬래시 커맨드 목록 ──────────────────────────────────────────────────────
6
+
7
+ const SLASH_COMMANDS = [
8
+ { command: "/status", description: "계정 및 시스템 상태" },
9
+ { command: "/help", description: "도움말 표시" },
10
+ { command: "/clear", description: "대화 내역 지우기" },
11
+ { command: "/compact", description: "대화 요약 후 압축" },
12
+ { command: "/config", description: "설정 보기 / 편집" },
13
+ { command: "/cost", description: "현재 세션 비용 표시" },
14
+ { command: "/doctor", description: "Claude 환경 진단" },
15
+ { command: "/exit", description: "세션 종료" },
16
+ { command: "/memory", description: "메모리 파일 관리" },
17
+ { command: "/model", description: "모델 변경" },
18
+ { command: "/permissions", description: "도구 권한 관리" },
19
+ { command: "/pr_comments", description: "PR 코멘트 가져오기" },
20
+ { command: "/release-notes", description: "릴리즈 노트 표시" },
21
+ { command: "/review", description: "코드 리뷰" },
22
+ { command: "/terminal-setup",description: "터미널 키 바인딩 설정" },
23
+ { command: "/vim", description: "Vim 모드 전환" },
24
+ ] as const;
25
+
26
+ // ─── SlashMenu ───────────────────────────────────────────────────────────────
27
+
28
+ interface SlashMenuProps {
29
+ query: string;
30
+ activeIndex: number;
31
+ onSelect: (command: string) => void;
32
+ }
33
+
34
+ function SlashMenu({ query, activeIndex, onSelect }: SlashMenuProps) {
35
+ const filtered = SLASH_COMMANDS.filter((c) =>
36
+ c.command.toLowerCase().startsWith("/" + query.toLowerCase()),
37
+ );
38
+ if (filtered.length === 0) return null;
39
+
40
+ return (
41
+ <ul className="absolute bottom-full left-0 right-0 mb-2 overflow-hidden rounded-xl border border-gray-900/[0.08] bg-gray-50 shadow-[0_8px_32px_-4px_rgba(0,0,0,0.12)] dark:border-white/[0.08] dark:bg-[#0d1117] dark:shadow-[0_8px_32px_-4px_rgba(0,0,0,0.5)]">
42
+ {filtered.map((item, i) => (
43
+ <li key={item.command}>
44
+ <button
45
+ type="button"
46
+ onMouseDown={(e) => { e.preventDefault(); onSelect(item.command); }}
47
+ className={[
48
+ "flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm transition-colors",
49
+ i === activeIndex
50
+ ? "bg-gray-900/[0.06] text-gray-900/90 dark:bg-white/[0.06] dark:text-white/90"
51
+ : "text-gray-900/55 hover:bg-gray-900/[0.03] hover:text-gray-900/80 dark:text-white/55 dark:hover:bg-white/[0.03] dark:hover:text-white/80",
52
+ ].join(" ")}
53
+ >
54
+ <span className="font-mono text-orange-500 dark:text-orange-400">{item.command}</span>
55
+ <span className="text-xs text-gray-900/25 dark:text-white/25">{item.description}</span>
56
+ </button>
57
+ </li>
58
+ ))}
59
+ </ul>
60
+ );
61
+ }
62
+
63
+ // ─── ChatInput ───────────────────────────────────────────────────────────────
64
+
65
+ interface Props {
66
+ onSend: (text: string) => void;
67
+ disabled?: boolean;
68
+ placeholder?: string;
69
+ }
70
+
71
+ export function ChatInput({ onSend, disabled = false, placeholder = "메시지 입력…" }: Props) {
72
+ const [value, setValue] = useState("");
73
+ const [slashQuery, setSlashQuery] = useState<string | null>(null);
74
+ const [activeIndex, setActiveIndex] = useState(0);
75
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
76
+
77
+ const filteredCount =
78
+ slashQuery !== null
79
+ ? SLASH_COMMANDS.filter((c) =>
80
+ c.command.toLowerCase().startsWith("/" + slashQuery.toLowerCase()),
81
+ ).length
82
+ : 0;
83
+
84
+ const submit = () => {
85
+ const trimmed = value.trim();
86
+ if (!trimmed || disabled) return;
87
+ onSend(trimmed);
88
+ setValue("");
89
+ setSlashQuery(null);
90
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
91
+ };
92
+
93
+ const handleChange = (raw: string) => {
94
+ setValue(raw);
95
+ const match = raw.match(/^\/(\S*)$/);
96
+ if (match) { setSlashQuery(match[1]); setActiveIndex(0); }
97
+ else setSlashQuery(null);
98
+ };
99
+
100
+ const selectCommand = (command: string) => {
101
+ setValue(command + " ");
102
+ setSlashQuery(null);
103
+ textareaRef.current?.focus();
104
+ };
105
+
106
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
107
+ if (slashQuery !== null && filteredCount > 0) {
108
+ if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((i) => (i + 1) % filteredCount); return; }
109
+ if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((i) => (i - 1 + filteredCount) % filteredCount); return; }
110
+ if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) {
111
+ e.preventDefault();
112
+ const filtered = SLASH_COMMANDS.filter((c) =>
113
+ c.command.toLowerCase().startsWith("/" + slashQuery.toLowerCase()),
114
+ );
115
+ if (filtered[activeIndex]) selectCommand(filtered[activeIndex].command);
116
+ return;
117
+ }
118
+ if (e.key === "Escape") { e.preventDefault(); setSlashQuery(null); return; }
119
+ }
120
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
121
+ };
122
+
123
+ const handleInput = () => {
124
+ const el = textareaRef.current;
125
+ if (!el) return;
126
+ el.style.height = "auto";
127
+ el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
128
+ };
129
+
130
+ useEffect(() => { if (value === "") setSlashQuery(null); }, [value]);
131
+
132
+ return (
133
+ <div className="relative">
134
+ {slashQuery !== null && (
135
+ <SlashMenu query={slashQuery} activeIndex={activeIndex} onSelect={selectCommand} />
136
+ )}
137
+ <div className="flex items-end gap-3 rounded-2xl border border-gray-900/[0.08] bg-gray-900/[0.03] px-4 py-3 transition-colors focus-within:border-gray-900/[0.14] dark:border-white/[0.08] dark:bg-white/[0.03] dark:focus-within:border-white/[0.14]">
138
+ <textarea
139
+ ref={textareaRef}
140
+ rows={1}
141
+ value={value}
142
+ onChange={(e) => handleChange(e.target.value)}
143
+ onKeyDown={handleKeyDown}
144
+ onInput={handleInput}
145
+ disabled={disabled}
146
+ placeholder={placeholder}
147
+ className="max-h-[200px] flex-1 resize-none bg-transparent text-sm text-gray-900/85 placeholder-gray-900/25 outline-none disabled:cursor-not-allowed dark:text-white/85 dark:placeholder-white/25"
148
+ />
149
+ <button
150
+ onClick={submit}
151
+ disabled={disabled || !value.trim()}
152
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-orange-600 transition-colors hover:bg-orange-500 disabled:cursor-not-allowed disabled:opacity-25"
153
+ aria-label="전송"
154
+ >
155
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4 rotate-90 text-white">
156
+ <path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" />
157
+ </svg>
158
+ </button>
159
+ </div>
160
+ </div>
161
+ );
162
+ }
@@ -0,0 +1,204 @@
1
+ import rehypeHighlight from "rehype-highlight";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+
5
+ import { isQuotaExceeded } from "@/lib/quota";
6
+ import type { ChatMessage as ChatMessageType, ToolUseBlock } from "../hooks/useClaudeSessions";
7
+ import { AGENT_AVATAR } from "./AgentSelectModal";
8
+ import type { AgentId } from "./AgentSelectModal";
9
+
10
+ // ─── Markdown ────────────────────────────────────────────────────────────────
11
+
12
+ function MarkdownContent({ content }: { content: string }) {
13
+ return (
14
+ <ReactMarkdown
15
+ remarkPlugins={[remarkGfm]}
16
+ rehypePlugins={[rehypeHighlight]}
17
+ components={{
18
+ p({ children }) { return <p className="mb-3 leading-7 last:mb-0">{children}</p>; },
19
+ h1({ children }) { return <h1 className="mt-5 mb-3 text-xl font-bold first:mt-0">{children}</h1>; },
20
+ h2({ children }) { return <h2 className="mt-4 mb-2 text-lg font-semibold first:mt-0">{children}</h2>; },
21
+ h3({ children }) { return <h3 className="mt-3 mb-2 text-base font-semibold first:mt-0">{children}</h3>; },
22
+ pre({ children }) {
23
+ return (
24
+ <pre className="my-3 overflow-x-auto rounded-lg border border-gray-900/[0.06] bg-gray-900/[0.03] p-4 text-xs leading-relaxed dark:border-white/[0.06] dark:bg-white/[0.03]">
25
+ {children}
26
+ </pre>
27
+ );
28
+ },
29
+ code({ children, className }) {
30
+ const isBlock = className?.startsWith("language-");
31
+ if (isBlock) return <code className={className}>{children}</code>;
32
+ return (
33
+ <code className="rounded bg-gray-900/[0.07] px-1.5 py-0.5 font-mono text-xs text-orange-600 dark:bg-white/[0.07] dark:text-orange-300">
34
+ {children}
35
+ </code>
36
+ );
37
+ },
38
+ ul({ children }) { return <ul className="mb-3 ml-4 list-disc space-y-1">{children}</ul>; },
39
+ ol({ children }) { return <ol className="mb-3 ml-4 list-decimal space-y-1">{children}</ol>; },
40
+ li({ children }) { return <li className="leading-7">{children}</li>; },
41
+ blockquote({ children }) {
42
+ return (
43
+ <blockquote className="my-3 border-l-2 border-gray-900/[0.15] pl-4 text-gray-900/45 italic dark:border-white/[0.15] dark:text-white/45">
44
+ {children}
45
+ </blockquote>
46
+ );
47
+ },
48
+ hr() { return <hr className="my-4 border-gray-900/[0.08] dark:border-white/[0.08]" />; },
49
+ table({ children }) {
50
+ return <div className="my-3 overflow-x-auto"><table className="w-full border-collapse text-sm">{children}</table></div>;
51
+ },
52
+ th({ children }) {
53
+ return (
54
+ <th className="border border-gray-900/[0.08] bg-gray-900/[0.04] px-3 py-2 text-left font-semibold dark:border-white/[0.08] dark:bg-white/[0.04]">
55
+ {children}
56
+ </th>
57
+ );
58
+ },
59
+ td({ children }) {
60
+ return (
61
+ <td className="border border-gray-900/[0.07] px-3 py-2 dark:border-white/[0.07]">
62
+ {children}
63
+ </td>
64
+ );
65
+ },
66
+ strong({ children }) {
67
+ return (
68
+ <strong className="font-semibold text-gray-900/90 dark:text-white/90">
69
+ {children}
70
+ </strong>
71
+ );
72
+ },
73
+ a({ href, children }) {
74
+ return (
75
+ <a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline underline-offset-2 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
76
+ {children}
77
+ </a>
78
+ );
79
+ },
80
+ }}
81
+ >
82
+ {content}
83
+ </ReactMarkdown>
84
+ );
85
+ }
86
+
87
+ // ─── ToolUseCard ─────────────────────────────────────────────────────────────
88
+
89
+ function ToolUseCard({ toolUse }: { toolUse: ToolUseBlock }) {
90
+ const primaryInput = Object.values(toolUse.input)[0];
91
+ const preview = typeof primaryInput === "string" ? primaryInput : JSON.stringify(toolUse.input);
92
+ return (
93
+ <div className="mb-2 overflow-hidden rounded-lg border border-gray-900/[0.07] text-xs dark:border-white/[0.07]">
94
+ <div className="flex items-center gap-1.5 border-b border-gray-900/[0.07] bg-gray-900/[0.03] px-3 py-1.5 dark:border-white/[0.07] dark:bg-white/[0.03]">
95
+ <span className="text-gray-900/30 dark:text-white/30">⚙</span>
96
+ <span className="font-mono font-medium text-gray-900/55 dark:text-white/55">{toolUse.tool}</span>
97
+ </div>
98
+ <pre className="overflow-x-auto bg-gray-900/[0.02] px-3 py-2 font-mono text-emerald-700 dark:bg-white/[0.02] dark:text-emerald-400/80 whitespace-pre-wrap break-all">
99
+ {preview}
100
+ </pre>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ // ─── QuotaBadge ──────────────────────────────────────────────────────────────
106
+
107
+ function QuotaBadge() {
108
+ return (
109
+ <span className="inline-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">
110
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-2.5 w-2.5 shrink-0">
111
+ <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" />
112
+ </svg>
113
+ 한도 초과
114
+ </span>
115
+ );
116
+ }
117
+
118
+ // ─── AgentAvatar ─────────────────────────────────────────────────────────────
119
+
120
+ function AgentAvatar({ agentId }: { agentId?: AgentId }) {
121
+ const cfg = agentId ? AGENT_AVATAR[agentId] : AGENT_AVATAR.claude;
122
+ return (
123
+ <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full overflow-hidden ${cfg.bg}`}>
124
+ {cfg.icon}
125
+ </div>
126
+ );
127
+ }
128
+
129
+ // ─── ChatMessage ─────────────────────────────────────────────────────────────
130
+
131
+ export function ChatMessage({ message, agentId }: { message: ChatMessageType; agentId?: AgentId }) {
132
+ const isUser = message.role === "user";
133
+
134
+ if (isUser) {
135
+ return (
136
+ <div className="flex w-full justify-end gap-3">
137
+ <div className="max-w-[75%] rounded-2xl rounded-tr-sm bg-blue-600/90 px-4 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap text-white">
138
+ {message.content}
139
+ </div>
140
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-600/80 text-xs font-bold text-white">U</div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ const quota = isQuotaExceeded(message.content);
146
+
147
+ return (
148
+ <div className="flex w-full justify-start gap-3">
149
+ <AgentAvatar agentId={agentId} />
150
+ <div className="max-w-[75%] min-w-0 rounded-2xl rounded-tl-sm border border-gray-900/[0.06] bg-gray-900/[0.04] px-4 py-3 text-sm text-gray-900/80 dark:border-white/[0.06] dark:bg-white/[0.04] dark:text-white/80">
151
+ {message.toolUses?.map((t, i) => <ToolUseCard key={i} toolUse={t} />)}
152
+ {message.content && <MarkdownContent content={message.content} />}
153
+ {quota && <div className="mt-2"><QuotaBadge /></div>}
154
+ {!quota && message.meta && (
155
+ <p className="mt-2 text-right text-xs text-gray-900/20 dark:text-white/20">
156
+ {(message.meta.durationMs / 1000).toFixed(1)}s · ${message.meta.costUsd.toFixed(4)}
157
+ </p>
158
+ )}
159
+ </div>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ // ─── StreamingMessage ────────────────────────────────────────────────────────
165
+
166
+ export function StreamingMessage({ content, agentId }: { content: string; agentId?: AgentId }) {
167
+ const quota = isQuotaExceeded(content);
168
+
169
+ return (
170
+ <div className="flex w-full justify-start gap-3">
171
+ <AgentAvatar agentId={agentId} />
172
+ <div className="max-w-[75%] min-w-0 rounded-2xl rounded-tl-sm border border-gray-900/[0.06] bg-gray-900/[0.04] px-4 py-3 text-sm text-gray-900/80 dark:border-white/[0.06] dark:bg-white/[0.04] dark:text-white/80">
173
+ {content ? (
174
+ <>
175
+ <MarkdownContent content={content} />
176
+ {quota && <div className="mt-2"><QuotaBadge /></div>}
177
+ </>
178
+ ) : (
179
+ <span className="flex gap-1 py-1">
180
+ <span className="animate-bounce text-gray-900/40 dark:text-white/40">●</span>
181
+ <span className="animate-bounce text-gray-900/40 [animation-delay:0.15s] dark:text-white/40">●</span>
182
+ <span className="animate-bounce text-gray-900/40 [animation-delay:0.3s] dark:text-white/40">●</span>
183
+ </span>
184
+ )}
185
+ </div>
186
+ </div>
187
+ );
188
+ }
189
+
190
+ // ─── SystemMessage ───────────────────────────────────────────────────────────
191
+
192
+ export function SystemMessage({ content }: { content: string }) {
193
+ return (
194
+ <div className="flex w-full justify-center">
195
+ <div className="w-full max-w-[90%] overflow-hidden rounded-xl border border-gray-900/[0.07] bg-gray-900/[0.02] text-xs dark:border-white/[0.07] dark:bg-white/[0.02]">
196
+ <div className="flex items-center gap-1.5 border-b border-gray-900/[0.07] px-3 py-1.5 dark:border-white/[0.07]">
197
+ <span className="text-gray-900/25 dark:text-white/25">⚡</span>
198
+ <span className="font-mono font-medium text-gray-900/35 dark:text-white/35">system</span>
199
+ </div>
200
+ <div className="px-4 py-3 font-mono text-gray-900/55 dark:text-white/55 whitespace-pre-wrap">{content}</div>
201
+ </div>
202
+ </div>
203
+ );
204
+ }
@@ -0,0 +1,207 @@
1
+ "use client";
2
+
3
+ import type { RefObject } from "react";
4
+
5
+ import { WorkingDirPicker } from "@/components/ui/WorkingDirPicker";
6
+ import type { PermissionPrompt } from "@/lib/ansi";
7
+ import type { ConnectionStatus, UnifiedSessionState } from "../hooks/useUnifiedSessions";
8
+ import type { AgentModelSettings, AgentModelSettingsByAgent } from "../lib/agentModelOptions";
9
+ import { AGENT_META } from "./AgentSelectModal";
10
+ import type { AgentId } from "./AgentSelectModal";
11
+ import { AgentModelPicker } from "./AgentModelPicker";
12
+ import { ChatInput } from "./ChatInput";
13
+ import { ChatMessage, StreamingMessage, SystemMessage } from "./ChatMessage";
14
+ import { PermissionCard } from "./PermissionCard";
15
+
16
+ interface ChatWorkspaceProps {
17
+ selectedSession: UnifiedSessionState | null;
18
+ selectedSessionDir: string;
19
+ overallConnectionStatus: ConnectionStatus;
20
+ currentDir: string;
21
+ error: string | null;
22
+ inputDisabled: boolean;
23
+ bottomRef: RefObject<HTMLDivElement | null>;
24
+ modelSettingsByAgent: AgentModelSettingsByAgent;
25
+ onTerminateSession: (sessionId: string) => void;
26
+ onSend: (text: string) => void;
27
+ onSendMessage: (sessionId: string, text: string) => void;
28
+ onDirChange: (path: string) => void;
29
+ onModelSettingsChange: (agentId: AgentId, settings: AgentModelSettings) => void;
30
+ }
31
+
32
+ function parsePermissionPrompt(content: string): PermissionPrompt | null {
33
+ try {
34
+ const parsed = JSON.parse(content) as Partial<PermissionPrompt>;
35
+ if (typeof parsed.tool !== "string" || typeof parsed.command !== "string") {
36
+ return null;
37
+ }
38
+ return {
39
+ tool: parsed.tool,
40
+ command: parsed.command,
41
+ warning: typeof parsed.warning === "string" ? parsed.warning : undefined,
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ export function ChatWorkspace({
49
+ selectedSession,
50
+ selectedSessionDir,
51
+ overallConnectionStatus,
52
+ currentDir,
53
+ error,
54
+ inputDisabled,
55
+ bottomRef,
56
+ modelSettingsByAgent,
57
+ onTerminateSession,
58
+ onSend,
59
+ onSendMessage,
60
+ onDirChange,
61
+ onModelSettingsChange,
62
+ }: ChatWorkspaceProps) {
63
+ return (
64
+ <div className="flex flex-1 flex-col overflow-hidden">
65
+ {!selectedSession ? (
66
+ <div className="relative flex flex-1 flex-col items-center justify-center gap-4">
67
+ <div className="pointer-events-none absolute inset-0 overflow-hidden">
68
+ <div className="absolute left-1/2 top-1/2 h-[500px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-orange-500/[0.04] blur-[120px]" />
69
+ </div>
70
+ <div className="relative flex h-14 w-14 items-center justify-center rounded-2xl border border-gray-900/[0.08] bg-orange-500/[0.08] dark:border-white/[0.08]">
71
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-7 w-7 text-[#D97757]">
72
+ <path d="M13.827 3.816L20.05 20.2h-3.672l-1.234-3.365H8.856L7.622 20.2H3.95L10.173 3.816h3.654zm-1.827 4.91l-1.989 5.453h3.978l-1.989-5.453z" />
73
+ </svg>
74
+ </div>
75
+ <div className="relative text-center">
76
+ <p className="font-semibold text-gray-900/75 dark:text-white/75">JI CLI</p>
77
+ <p className="mt-1 text-sm text-gray-900/30 dark:text-white/30">
78
+ {overallConnectionStatus !== "connected"
79
+ ? "서버에 연결 중…"
80
+ : "왼쪽에서 세션을 선택하거나 새 세션을 생성하세요"}
81
+ </p>
82
+ </div>
83
+ </div>
84
+ ) : (
85
+ <>
86
+ <header className="flex shrink-0 items-center justify-between border-b border-gray-900/[0.07] px-5 py-3 dark:border-white/[0.07]">
87
+ <div className="flex min-w-0 flex-col gap-0.5">
88
+ <div className="flex items-center gap-2">
89
+ <span className={`h-2 w-2 shrink-0 rounded-full ${AGENT_META[selectedSession.agentId].dotColor}`} />
90
+ <span className="text-sm font-medium text-gray-900/80 dark:text-white/80">{selectedSession.info.title}</span>
91
+ <span className="rounded-md border border-gray-900/[0.06] bg-gray-900/[0.03] px-1.5 py-0.5 text-[10px] font-medium text-gray-900/35 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-white/35">
92
+ {AGENT_META[selectedSession.agentId].label}
93
+ </span>
94
+ </div>
95
+ <div className="flex items-center gap-2 pl-4">
96
+ <span className="font-mono text-[10px] text-gray-900/20 dark:text-white/20">{selectedSession.info.id.slice(0, 8)}…</span>
97
+ {selectedSessionDir && (
98
+ <span className="flex items-center gap-1 rounded-md border border-gray-900/[0.06] bg-gray-900/[0.03] px-1.5 py-0.5 font-mono text-[10px] text-gray-900/40 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-white/40">
99
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-2.5 w-2.5 shrink-0">
100
+ <path d="M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3H7.5a.25.25 0 01-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75z" />
101
+ </svg>
102
+ <span className="max-w-[200px] truncate">{selectedSessionDir}</span>
103
+ </span>
104
+ )}
105
+ </div>
106
+ </div>
107
+ <button
108
+ type="button"
109
+ onClick={() => onTerminateSession(selectedSession.info.id)}
110
+ className="shrink-0 rounded-lg border border-gray-900/[0.08] bg-gray-900/[0.03] px-3 py-1.5 text-xs font-medium text-gray-900/45 transition-colors hover:border-gray-900/[0.14] hover:bg-gray-900/[0.06] hover:text-gray-900/75 dark:border-white/[0.08] dark:bg-white/[0.03] dark:text-white/45 dark:hover:border-white/[0.14] dark:hover:bg-white/[0.06] dark:hover:text-white/75"
111
+ >
112
+ 종료
113
+ </button>
114
+ </header>
115
+
116
+ {error && (
117
+ <div className="shrink-0 border-b border-red-200 bg-red-50 px-5 py-2 text-xs text-red-600 dark:border-red-900/50 dark:bg-red-950/40 dark:text-red-400">
118
+ {error}
119
+ </div>
120
+ )}
121
+
122
+ <main className="flex-1 overflow-y-auto px-4 py-6">
123
+ <div className="mx-auto flex max-w-2xl flex-col gap-5">
124
+ {!selectedSession.messagesLoaded && (
125
+ <div className="flex items-center justify-center gap-2 py-12 text-sm text-gray-900/25 dark:text-white/25">
126
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-900/[0.07] border-t-gray-900/40 dark:border-white/[0.07] dark:border-t-white/40" />
127
+ 이전 대화를 불러오는 중…
128
+ </div>
129
+ )}
130
+
131
+ {selectedSession.messagesLoaded &&
132
+ selectedSession.messages.length === 0 &&
133
+ !selectedSession.isWaiting && (
134
+ <div className="flex flex-col items-center justify-center gap-3 py-24 text-center">
135
+ <p className="text-sm text-gray-900/25 dark:text-white/25">
136
+ {AGENT_META[selectedSession.agentId].label}이 준비되었습니다. 메시지를 보내보세요.
137
+ </p>
138
+ </div>
139
+ )}
140
+
141
+ {selectedSession.messages.map((message) => {
142
+ if (message.role === "permission") {
143
+ const prompt = parsePermissionPrompt(message.content);
144
+ if (!prompt) {
145
+ return (
146
+ <SystemMessage
147
+ key={message.id}
148
+ content={`권한 요청을 표시할 수 없습니다.\n${message.content}`}
149
+ />
150
+ );
151
+ }
152
+ return (
153
+ <PermissionCard
154
+ key={message.id}
155
+ tool={prompt.tool}
156
+ command={prompt.command}
157
+ warning={prompt.warning}
158
+ onAllow={() => onSendMessage(selectedSession.info.id, "1")}
159
+ onDeny={() => onSendMessage(selectedSession.info.id, "2")}
160
+ />
161
+ );
162
+ }
163
+ if (message.role === "system") {
164
+ return <SystemMessage key={message.id} content={message.content} />;
165
+ }
166
+ return <ChatMessage key={message.id} message={message} agentId={selectedSession.agentId} />;
167
+ })}
168
+
169
+ {selectedSession.isWaiting && (
170
+ <StreamingMessage content={selectedSession.streaming} agentId={selectedSession.agentId} />
171
+ )}
172
+
173
+ <div ref={bottomRef} />
174
+ </div>
175
+ </main>
176
+
177
+ <footer className="shrink-0 border-t border-gray-900/[0.07] px-4 pb-4 pt-3 dark:border-white/[0.07]">
178
+ <div className="mx-auto max-w-2xl space-y-2">
179
+ <div className="flex items-center justify-between gap-3 border-b border-gray-900/[0.05] pb-2 dark:border-white/[0.05]">
180
+ <div className="flex min-w-0 flex-1 items-center gap-2">
181
+ <span className="shrink-0 text-[10px] font-medium uppercase tracking-wider text-gray-900/25 dark:text-white/25">
182
+ cwd
183
+ </span>
184
+ <WorkingDirPicker value={currentDir} onChange={onDirChange} variant="inline" />
185
+ </div>
186
+ <AgentModelPicker
187
+ agentId={selectedSession.agentId}
188
+ value={modelSettingsByAgent[selectedSession.agentId]}
189
+ onChange={onModelSettingsChange}
190
+ />
191
+ </div>
192
+ <ChatInput
193
+ onSend={onSend}
194
+ disabled={inputDisabled}
195
+ placeholder={
196
+ selectedSession.isWaiting
197
+ ? "응답을 기다리는 중…"
198
+ : "메시지 입력 (Enter 전송 / Shift+Enter 줄바꿈)"
199
+ }
200
+ />
201
+ </div>
202
+ </footer>
203
+ </>
204
+ )}
205
+ </div>
206
+ );
207
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ export function CheckingSkeleton() {
4
+ return (
5
+ <div className="flex h-screen bg-[#faf8f5] dark:bg-[#07090e]">
6
+ <aside className="flex w-64 flex-shrink-0 flex-col border-r border-gray-900/[0.07] dark:border-white/[0.07]">
7
+ <div className="relative overflow-hidden border-b border-gray-900/[0.07] px-4 py-3.5 dark:border-white/[0.07]">
8
+ <div className="animate-shimmer-bg absolute inset-0" />
9
+ <div className="relative flex items-center gap-2">
10
+ <div className="h-3.5 w-3.5 rounded bg-gray-900/[0.07] dark:bg-white/[0.07]" />
11
+ <div className="h-[14px] w-20 rounded bg-gray-900/[0.07] dark:bg-white/[0.07]" />
12
+ </div>
13
+ </div>
14
+ <div className="flex flex-col gap-2 p-3">
15
+ <div className="h-9 rounded-lg bg-orange-500/[0.08]" />
16
+ <div className="flex gap-2">
17
+ <div className="h-9 flex-1 rounded-lg bg-gray-900/[0.04] dark:bg-white/[0.04]" />
18
+ <div className="h-9 w-9 shrink-0 rounded-lg bg-gray-900/[0.04] dark:bg-white/[0.04]" />
19
+ </div>
20
+ </div>
21
+ <div className="flex flex-col gap-0.5 px-2">
22
+ {[0, 1, 2].map((i) => (
23
+ <div key={i} className="relative overflow-hidden rounded-lg p-3">
24
+ <div
25
+ className="animate-shimmer-bg absolute inset-0 rounded-lg"
26
+ style={{ animationDelay: `${i * 80}ms` }}
27
+ />
28
+ <div className="relative flex flex-col gap-1.5">
29
+ <div className="h-[13px] w-28 rounded bg-gray-900/[0.07] dark:bg-white/[0.07]" />
30
+ <div className="h-2.5 w-16 rounded bg-gray-900/[0.04] dark:bg-white/[0.04]" />
31
+ <div className="h-2.5 w-32 rounded bg-gray-900/[0.03] dark:bg-white/[0.03]" />
32
+ </div>
33
+ </div>
34
+ ))}
35
+ </div>
36
+ </aside>
37
+
38
+ <div className="flex flex-1 flex-col items-center justify-center gap-3">
39
+ <span className="h-6 w-6 animate-spin rounded-full border-2 border-gray-900/[0.08] border-t-orange-500 dark:border-white/[0.08]" />
40
+ <span className="text-xs text-gray-900/20 dark:text-white/20">인증 확인 중…</span>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+
5
+ import type { LoginState } from "@/features/auth/hooks/useClaudeAuth";
6
+ import { LoginPanel } from "@/features/auth/ui/LoginPanel";
7
+ import { ThemeToggle } from "@/lib/theme";
8
+
9
+ interface ClaudeLoginViewProps {
10
+ loginState: LoginState;
11
+ loginOutput: string;
12
+ loginUrls: string[];
13
+ onStart: () => void;
14
+ onCancel: () => void;
15
+ }
16
+
17
+ export function ClaudeLoginView({
18
+ loginState,
19
+ loginOutput,
20
+ loginUrls,
21
+ onStart,
22
+ onCancel,
23
+ }: ClaudeLoginViewProps) {
24
+ return (
25
+ <div className="flex h-screen flex-col bg-[#faf8f5] text-gray-900 dark:bg-[#07090e] dark:text-white">
26
+ <header className="flex items-center gap-2 border-b border-gray-900/[0.07] px-4 py-3 dark:border-white/[0.07]">
27
+ <Link href="/" className="text-gray-900/30 transition-colors hover:text-gray-900/60 dark:text-white/30 dark:hover:text-white/60">
28
+
29
+ </Link>
30
+ <span className="text-sm font-semibold text-gray-900/80 dark:text-white/80">JI CLI</span>
31
+ <div className="ml-auto">
32
+ <ThemeToggle />
33
+ </div>
34
+ </header>
35
+ <LoginPanel
36
+ loginState={loginState}
37
+ loginOutput={loginOutput}
38
+ loginUrls={loginUrls}
39
+ onStart={onStart}
40
+ onCancel={onCancel}
41
+ />
42
+ </div>
43
+ );
44
+ }