@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.
- package/.gitignore +23 -0
- package/.npmignore +21 -0
- package/.prettierignore +6 -0
- package/.prettierrc +26 -0
- package/AGENTS.md +10 -0
- package/CLAUDE.md +10 -0
- package/README.md +384 -0
- package/apps/server/README.md +294 -0
- package/apps/server/eslint.config.mjs +20 -0
- package/apps/server/nest-cli.json +8 -0
- package/apps/server/package.json +89 -0
- package/apps/server/scripts/postinstall.js +53 -0
- package/apps/server/src/__mocks__/glob.js +6 -0
- package/apps/server/src/__mocks__/uuid.js +5 -0
- package/apps/server/src/app.controller.spec.ts +24 -0
- package/apps/server/src/app.controller.ts +13 -0
- package/apps/server/src/app.module.ts +18 -0
- package/apps/server/src/app.service.ts +8 -0
- package/apps/server/src/common/ji-paths.ts +41 -0
- package/apps/server/src/database/database.module.ts +27 -0
- package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
- package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
- package/apps/server/src/database/entities/conversation.entity.ts +41 -0
- package/apps/server/src/database/entities/session.entity.ts +16 -0
- package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
- package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
- package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
- package/apps/server/src/database/entities/task-run.entity.ts +41 -0
- package/apps/server/src/database/entities/task.entity.ts +44 -0
- package/apps/server/src/main.ts +65 -0
- package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
- package/apps/server/src/modules/agents/agents.module.ts +11 -0
- package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
- package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
- package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
- package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
- package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
- package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
- package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
- package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
- package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
- package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
- package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
- package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
- package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
- package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
- package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
- package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
- package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
- package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
- package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
- package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
- package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
- package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
- package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
- package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
- package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
- package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
- package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
- package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
- package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
- package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
- package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
- package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
- package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
- package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
- package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
- package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
- package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
- package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
- package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
- package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
- package/apps/server/src/modules/fs/fs.controller.ts +29 -0
- package/apps/server/src/modules/fs/fs.module.ts +8 -0
- package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
- package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
- package/apps/server/src/modules/harness/harness.controller.ts +35 -0
- package/apps/server/src/modules/harness/harness.module.ts +11 -0
- package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
- package/apps/server/src/modules/harness/harness.service.ts +112 -0
- package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
- package/apps/server/src/modules/sessions/session.controller.ts +43 -0
- package/apps/server/src/modules/sessions/session.module.ts +14 -0
- package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
- package/apps/server/src/modules/sessions/session.service.ts +35 -0
- package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
- package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
- package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
- package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
- package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
- package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
- package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
- package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
- package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
- package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
- package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
- package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
- package/apps/server/test/app.e2e-spec.ts +28 -0
- package/apps/server/test/jest-e2e.json +9 -0
- package/apps/server/tsconfig.build.json +4 -0
- package/apps/server/tsconfig.json +13 -0
- package/apps/web/AGENTS.md +7 -0
- package/apps/web/CLAUDE.md +1 -0
- package/apps/web/README.md +36 -0
- package/apps/web/eslint.config.mjs +21 -0
- package/apps/web/next-env.d.ts +6 -0
- package/apps/web/next.config.ts +7 -0
- package/apps/web/package.json +49 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/claude/page.tsx +5 -0
- package/apps/web/src/app/codex/page.tsx +126 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/gemini/page.tsx +130 -0
- package/apps/web/src/app/globals.css +149 -0
- package/apps/web/src/app/layout.tsx +40 -0
- package/apps/web/src/app/login/page.tsx +67 -0
- package/apps/web/src/app/page.tsx +497 -0
- package/apps/web/src/app/task/[id]/page.tsx +11 -0
- package/apps/web/src/app/test/page.tsx +298 -0
- package/apps/web/src/components/ui/Modal.tsx +78 -0
- package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
- package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
- package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
- package/apps/web/src/features/auth/api/auth.api.ts +81 -0
- package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
- package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
- package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
- package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
- package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
- package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
- package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
- package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
- package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
- package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
- package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
- package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
- package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
- package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
- package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
- package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
- package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
- package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
- package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
- package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
- package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
- package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
- package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
- package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
- package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
- package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
- package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
- package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
- package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
- package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
- package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
- package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
- package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
- package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
- package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
- package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
- package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
- package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
- package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
- package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
- package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
- package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
- package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
- package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
- package/apps/web/src/features/fs/api/fs.api.ts +14 -0
- package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
- package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
- package/apps/web/src/features/harness/api/harness.api.ts +46 -0
- package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
- package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
- package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
- package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
- package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
- package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
- package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
- package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
- package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
- package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
- package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
- package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
- package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
- package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
- package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
- package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
- package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
- package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
- package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
- package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
- package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
- package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
- package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
- package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
- package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
- package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
- package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
- package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
- package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
- package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
- package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
- package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
- package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
- package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
- package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
- package/apps/web/src/hooks/useWorkingDir.ts +28 -0
- package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
- package/apps/web/src/lib/ansi.ts +105 -0
- package/apps/web/src/lib/constants.ts +4 -0
- package/apps/web/src/lib/quota.ts +22 -0
- package/apps/web/src/lib/theme.tsx +78 -0
- package/apps/web/src/lib/toast.tsx +175 -0
- package/apps/web/src/store/agentStatusStore.ts +38 -0
- package/apps/web/tsconfig.json +18 -0
- package/apps/web/vitest.config.ts +25 -0
- package/apps/web/vitest.setup.ts +10 -0
- package/package.json +85 -0
- package/packages/cli/dist/commands/check.d.ts +1 -0
- package/packages/cli/dist/commands/check.js +89 -0
- package/packages/cli/dist/commands/init.d.ts +5 -0
- package/packages/cli/dist/commands/init.js +183 -0
- package/packages/cli/dist/commands/start.d.ts +4 -0
- package/packages/cli/dist/commands/start.js +188 -0
- package/packages/cli/dist/index.d.ts +2 -0
- package/packages/cli/dist/index.js +71 -0
- package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
- package/packages/cli/dist/utils/agent-tools.js +193 -0
- package/packages/cli/dist/utils/project-init.d.ts +12 -0
- package/packages/cli/dist/utils/project-init.js +258 -0
- package/packages/cli/dist/utils/proxy.d.ts +8 -0
- package/packages/cli/dist/utils/proxy.js +138 -0
- package/packages/cli/package.json +30 -0
- package/packages/cli/src/commands/check.ts +77 -0
- package/packages/cli/src/commands/init.ts +209 -0
- package/packages/cli/src/commands/start.ts +183 -0
- package/packages/cli/src/index.ts +91 -0
- package/packages/cli/src/utils/agent-tools.ts +201 -0
- package/packages/cli/src/utils/project-init.ts +252 -0
- package/packages/cli/src/utils/proxy.ts +123 -0
- package/packages/cli/tsconfig.json +14 -0
- package/packages/eslint-config/base.mjs +31 -0
- package/packages/eslint-config/nest.mjs +55 -0
- package/packages/eslint-config/next.mjs +23 -0
- package/packages/eslint-config/package.json +20 -0
- package/packages/typescript-config/base.json +16 -0
- package/packages/typescript-config/nestjs.json +17 -0
- package/packages/typescript-config/nextjs.json +15 -0
- package/packages/typescript-config/package.json +11 -0
- package/turbo.json +28 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
import { isQuotaExceeded } from "@/lib/quota";
|
|
6
|
+
import type { AgentStatus, TaskStatus } from "../api/tasks.api";
|
|
7
|
+
import type { AgentLog } from "../hooks/useTaskExecution";
|
|
8
|
+
import { AgentRoleBadge } from "./AgentRoleSelect";
|
|
9
|
+
import type { AgentRole } from "../api/tasks.api";
|
|
10
|
+
|
|
11
|
+
// ─── 에이전트 상태 아이콘 ─────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function AgentStatusIcon({ status }: { status: AgentStatus }) {
|
|
14
|
+
if (status === "running") {
|
|
15
|
+
return <span className="h-3 w-3 animate-spin rounded-full border border-gray-400 border-t-orange-500 dark:border-gray-500 dark:border-t-orange-400" />;
|
|
16
|
+
}
|
|
17
|
+
if (status === "completed") return <span className="text-emerald-600 dark:text-green-400">✓</span>;
|
|
18
|
+
if (status === "error") return <span className="text-red-500 dark:text-red-400">✕</span>;
|
|
19
|
+
if (status === "stopped") return <span className="text-gray-400 dark:text-gray-500">■</span>;
|
|
20
|
+
return <span className="h-2 w-2 rounded-full bg-gray-300 dark:bg-gray-600" />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── 에이전트 상태 레이블 색상 ────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const STATUS_TEXT: Record<AgentStatus, string> = {
|
|
26
|
+
pending: "text-gray-400",
|
|
27
|
+
running: "text-orange-500 dark:text-orange-400",
|
|
28
|
+
completed: "text-emerald-600 dark:text-green-400",
|
|
29
|
+
error: "text-red-500 dark:text-red-400",
|
|
30
|
+
stopped: "text-gray-400",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const STATUS_LABEL: Record<AgentStatus, string> = {
|
|
34
|
+
pending: "대기",
|
|
35
|
+
running: "실행 중",
|
|
36
|
+
completed: "완료",
|
|
37
|
+
error: "오류",
|
|
38
|
+
stopped: "중지",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ─── 개별 에이전트 로그 패널 ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
interface AgentLogPanelProps {
|
|
44
|
+
agentId: number;
|
|
45
|
+
log: AgentLog;
|
|
46
|
+
role: AgentRole;
|
|
47
|
+
customRole: string | null;
|
|
48
|
+
canRerun?: boolean;
|
|
49
|
+
rerunDisabled?: boolean;
|
|
50
|
+
onRerun?: (agentId: number) => void;
|
|
51
|
+
onWriteTests?: (agentId: number) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function AgentLogPanel({ agentId, log, role, customRole, canRerun, rerunDisabled, onRerun, onWriteTests }: AgentLogPanelProps) {
|
|
55
|
+
const outputRef = useRef<HTMLPreElement>(null);
|
|
56
|
+
const quota = isQuotaExceeded((log.output ?? "") + (log.errorMessage ?? ""));
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const el = outputRef.current;
|
|
60
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
61
|
+
}, [log.output]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex flex-col gap-1.5 rounded-xl border border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950">
|
|
65
|
+
{/* 에이전트 헤더 */}
|
|
66
|
+
<div className="flex items-center justify-between gap-3 border-b border-gray-200 px-3 py-2 dark:border-gray-800">
|
|
67
|
+
<div className="flex items-center gap-2">
|
|
68
|
+
<AgentStatusIcon status={log.status} />
|
|
69
|
+
<AgentRoleBadge role={role} customRole={customRole} />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
{quota ? (
|
|
73
|
+
<span className="flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/[0.08] px-2 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400">
|
|
74
|
+
<svg viewBox="0 0 16 16" fill="currentColor" className="h-2.5 w-2.5">
|
|
75
|
+
<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" />
|
|
76
|
+
</svg>
|
|
77
|
+
한도 초과
|
|
78
|
+
</span>
|
|
79
|
+
) : (
|
|
80
|
+
<span className={`text-xs ${STATUS_TEXT[log.status]}`}>
|
|
81
|
+
{STATUS_LABEL[log.status]}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
{log.durationMs !== undefined && (
|
|
85
|
+
<span className="text-xs text-gray-400 dark:text-gray-600">
|
|
86
|
+
{(log.durationMs / 1000).toFixed(1)}s
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
{log.costUsd !== undefined && (
|
|
90
|
+
<span className="text-xs text-gray-400 dark:text-gray-600">
|
|
91
|
+
${log.costUsd.toFixed(4)}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
{canRerun && (
|
|
95
|
+
<>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => onWriteTests?.(agentId)}
|
|
99
|
+
disabled={rerunDisabled}
|
|
100
|
+
className="flex items-center gap-1 rounded-md border border-emerald-500/25 px-2 py-1 text-[10px] font-medium text-emerald-600 transition-colors hover:border-emerald-400/50 hover:bg-emerald-500/[0.08] disabled:opacity-40 dark:text-emerald-400"
|
|
101
|
+
title="이 에이전트가 테스트 코드만 보완하도록 재실행"
|
|
102
|
+
>
|
|
103
|
+
<svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
|
|
104
|
+
<path fillRule="evenodd" d="M12.416 3.376a.75.75 0 01.208 1.04l-5 7.5a.75.75 0 01-1.154.114l-3-3a.75.75 0 011.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 011.04-.207z" clipRule="evenodd" />
|
|
105
|
+
</svg>
|
|
106
|
+
테스트 작성
|
|
107
|
+
</button>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={() => onRerun?.(agentId)}
|
|
111
|
+
disabled={rerunDisabled}
|
|
112
|
+
className="flex items-center gap-1 rounded-md border border-blue-500/25 px-2 py-1 text-[10px] font-medium text-blue-600 transition-colors hover:border-blue-400/50 hover:bg-blue-500/[0.08] disabled:opacity-40 dark:text-blue-400"
|
|
113
|
+
title="이 에이전트만 재실행"
|
|
114
|
+
>
|
|
115
|
+
<svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
|
|
116
|
+
<path fillRule="evenodd" d="M13.836 2.477a.75.75 0 01.75.75v3.182a.75.75 0 01-.75.75h-3.182a.75.75 0 010-1.5h1.37A5.995 5.995 0 008 4a6 6 0 100 12 6 6 0 005.812-4.5h1.539A7.5 7.5 0 118 2.5c1.373 0 2.663.372 3.772 1.021l.314-.814a.75.75 0 01.75-.23z" clipRule="evenodd" />
|
|
117
|
+
</svg>
|
|
118
|
+
재실행
|
|
119
|
+
</button>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* 출력 영역 */}
|
|
126
|
+
{log.errorMessage ? (
|
|
127
|
+
<p className="px-3 py-2 text-xs text-red-500 dark:text-red-400">{log.errorMessage}</p>
|
|
128
|
+
) : log.output ? (
|
|
129
|
+
<pre
|
|
130
|
+
ref={outputRef}
|
|
131
|
+
className="max-h-52 overflow-y-auto px-3 py-2 font-mono text-xs leading-relaxed whitespace-pre-wrap break-words text-gray-700 dark:text-gray-300"
|
|
132
|
+
>
|
|
133
|
+
{log.output}
|
|
134
|
+
</pre>
|
|
135
|
+
) : (
|
|
136
|
+
<p className="px-3 py-2 text-xs text-gray-400">
|
|
137
|
+
{log.status === "running" ? "출력을 기다리는 중…" : "출력 없음"}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── AgentOutputPanel ────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
interface Agent {
|
|
147
|
+
id: number;
|
|
148
|
+
role: AgentRole;
|
|
149
|
+
customRole: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface AgentOutputPanelProps {
|
|
153
|
+
agents: Agent[];
|
|
154
|
+
agentLogs: Record<number, AgentLog>;
|
|
155
|
+
connected: boolean;
|
|
156
|
+
taskStatus?: TaskStatus;
|
|
157
|
+
canRerun?: boolean;
|
|
158
|
+
rerunDisabled?: boolean;
|
|
159
|
+
onRerunAgent?: (agentId: number) => void;
|
|
160
|
+
onWriteTestsAgent?: (agentId: number) => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function AgentOutputPanel({ agents, agentLogs, connected, taskStatus, canRerun, rerunDisabled, onRerunAgent, onWriteTestsAgent }: AgentOutputPanelProps) {
|
|
164
|
+
const fallbackStatus: AgentStatus =
|
|
165
|
+
taskStatus === "completed" ? "completed" :
|
|
166
|
+
taskStatus === "error" ? "error" :
|
|
167
|
+
taskStatus === "stopped" ? "stopped" :
|
|
168
|
+
"running";
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex flex-col gap-2">
|
|
171
|
+
<div className="flex items-center gap-2">
|
|
172
|
+
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">에이전트 출력</span>
|
|
173
|
+
<span
|
|
174
|
+
className={`h-1.5 w-1.5 rounded-full ${connected ? "animate-pulse bg-emerald-500 dark:bg-green-400" : "bg-gray-300 dark:bg-gray-600"}`}
|
|
175
|
+
/>
|
|
176
|
+
{!connected && (
|
|
177
|
+
<span className="text-[10px] text-gray-400">연결 중…</span>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{agents.map((agent) => {
|
|
182
|
+
const log = agentLogs[agent.id] ?? {
|
|
183
|
+
agentId: agent.id,
|
|
184
|
+
status: fallbackStatus,
|
|
185
|
+
output: "",
|
|
186
|
+
};
|
|
187
|
+
return (
|
|
188
|
+
<AgentLogPanel
|
|
189
|
+
key={agent.id}
|
|
190
|
+
agentId={agent.id}
|
|
191
|
+
log={log}
|
|
192
|
+
role={agent.role}
|
|
193
|
+
customRole={agent.customRole}
|
|
194
|
+
canRerun={canRerun}
|
|
195
|
+
rerunDisabled={rerunDisabled}
|
|
196
|
+
onRerun={onRerunAgent}
|
|
197
|
+
onWriteTests={onWriteTestsAgent}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AGENT_META } from "@/features/chat/ui/AgentSelectModal";
|
|
4
|
+
import type { AgentRole } from "../api/tasks.api";
|
|
5
|
+
import type { AgentDraft } from "../hooks/useTaskCreate";
|
|
6
|
+
|
|
7
|
+
const ROLES: { value: AgentRole; label: string; active: string; badge: string }[] = [
|
|
8
|
+
{ value: "frontend", label: "Frontend", active: "border-blue-500/50 bg-blue-500/[0.12] text-blue-700 dark:text-blue-300", badge: "border-blue-500/40 bg-blue-500/[0.08] text-blue-700 dark:text-blue-300" },
|
|
9
|
+
{ value: "backend", label: "Backend", active: "border-emerald-500/50 bg-emerald-500/[0.12] text-emerald-700 dark:text-emerald-300", badge: "border-emerald-500/40 bg-emerald-500/[0.08] text-emerald-700 dark:text-emerald-300" },
|
|
10
|
+
{ value: "doc", label: "Doc", active: "border-purple-500/50 bg-purple-500/[0.12] text-purple-700 dark:text-purple-300", badge: "border-purple-500/40 bg-purple-500/[0.08] text-purple-700 dark:text-purple-300" },
|
|
11
|
+
{ value: "operation", label: "Operation", active: "border-amber-500/50 bg-amber-500/[0.12] text-amber-700 dark:text-amber-300", badge: "border-amber-500/40 bg-amber-500/[0.08] text-amber-700 dark:text-amber-300" },
|
|
12
|
+
{ value: "other", label: "Other", active: "border-gray-900/[0.2] bg-gray-900/[0.07] text-gray-700 dark:border-white/[0.2] dark:bg-white/[0.07] dark:text-white/70", badge: "border-gray-900/[0.12] bg-gray-900/[0.05] text-gray-600 dark:border-white/[0.12] dark:bg-white/[0.05] dark:text-white/50" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
interface AgentRowProps {
|
|
16
|
+
agent: AgentDraft;
|
|
17
|
+
onChange: (patch: Partial<Omit<AgentDraft, "id">>) => void;
|
|
18
|
+
onRemove: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AgentRow({ agent, onChange, onRemove }: AgentRowProps) {
|
|
22
|
+
const agentMeta = AGENT_META[agent.agentType];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex flex-col gap-2 rounded-xl border border-gray-900/[0.07] bg-gray-900/[0.02] p-3 dark:border-white/[0.07] dark:bg-white/[0.02]">
|
|
26
|
+
{/* AI 에이전트 + 삭제 */}
|
|
27
|
+
<div className="flex items-center justify-between gap-2">
|
|
28
|
+
<div className="flex items-center gap-1.5">
|
|
29
|
+
<span className={`h-2 w-2 rounded-full ${agentMeta.dotColor}`} />
|
|
30
|
+
<span className="text-xs font-semibold text-gray-900/60 dark:text-white/60">
|
|
31
|
+
{agentMeta.label}
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={onRemove}
|
|
37
|
+
className="shrink-0 text-gray-900/20 transition-colors hover:text-red-500 dark:text-white/20 dark:hover:text-red-400"
|
|
38
|
+
aria-label="에이전트 삭제"
|
|
39
|
+
>
|
|
40
|
+
<svg viewBox="0 0 16 16" fill="currentColor" className="h-3.5 w-3.5">
|
|
41
|
+
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* 역할 뱃지 선택 */}
|
|
47
|
+
<div className="flex flex-wrap gap-1.5">
|
|
48
|
+
{ROLES.map((r) => (
|
|
49
|
+
<button
|
|
50
|
+
key={r.value}
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={() => onChange({ role: r.value })}
|
|
53
|
+
className={[
|
|
54
|
+
"rounded-full border px-3 py-0.5 text-xs font-medium transition-all",
|
|
55
|
+
agent.role === r.value
|
|
56
|
+
? r.active
|
|
57
|
+
: "border-gray-900/[0.07] text-gray-900/25 hover:border-gray-900/[0.13] hover:text-gray-900/50 dark:border-white/[0.07] dark:text-white/25 dark:hover:border-white/[0.13] dark:hover:text-white/50",
|
|
58
|
+
].join(" ")}
|
|
59
|
+
>
|
|
60
|
+
{r.label}
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* other 선택 시 커스텀 입력 */}
|
|
66
|
+
{agent.role === "other" && (
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
value={agent.customRole}
|
|
70
|
+
onChange={(e) => onChange({ customRole: e.target.value })}
|
|
71
|
+
placeholder="역할을 직접 입력하세요"
|
|
72
|
+
className="w-full rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.03] px-3 py-1.5 text-xs text-gray-900/70 placeholder-gray-900/20 outline-none transition-colors focus:border-gray-900/[0.15] dark:border-white/[0.07] dark:bg-white/[0.03] dark:text-white/70 dark:placeholder-white/20 dark:focus:border-white/[0.15]"
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* 역할 힌트 */}
|
|
77
|
+
{agent.role === "frontend" && (
|
|
78
|
+
<p className="text-[10px] text-blue-600/60 dark:text-blue-400/50">UI 구현 · 컴포넌트 개발 · 스타일링</p>
|
|
79
|
+
)}
|
|
80
|
+
{agent.role === "backend" && (
|
|
81
|
+
<p className="text-[10px] text-emerald-600/60 dark:text-emerald-400/50">API 개발 · DB 설계 · 서버 로직</p>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 선택된 role의 뱃지만 표시 (읽기 전용)
|
|
88
|
+
export function AgentRoleBadge({ role, customRole }: { role: AgentRole; customRole?: string | null }) {
|
|
89
|
+
const found = ROLES.find((r) => r.value === role);
|
|
90
|
+
const label = role === "other" ? (customRole ?? "Other") : (found?.label ?? role);
|
|
91
|
+
const cls = found?.badge ?? "border-gray-900/[0.12] bg-gray-900/[0.05] text-gray-600 dark:border-white/[0.12] dark:bg-white/[0.05] dark:text-white/50";
|
|
92
|
+
return (
|
|
93
|
+
<span className={`rounded-full border px-2.5 py-0.5 text-xs font-medium ${cls}`}>
|
|
94
|
+
{label}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { TaskAgent } from "../api/tasks.api";
|
|
6
|
+
import type { AgentChangelog, ChangelogFile, ChangeType } from "../api/changelog.api";
|
|
7
|
+
import { mergeAgentAll, mergeAgentFile } from "../api/changelog.api";
|
|
8
|
+
import { useChangelogCodeCopy } from "../hooks/useChangelogCodeCopy";
|
|
9
|
+
import { useTaskChangelog } from "../hooks/useTaskChangelog";
|
|
10
|
+
|
|
11
|
+
// ─── 변경 유형 뱃지 ──────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const CHANGE_TYPE_CONFIG: Record<ChangeType, { label: string; className: string }> = {
|
|
14
|
+
added: { label: "추가", className: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20 dark:text-emerald-400" },
|
|
15
|
+
modified: { label: "수정", className: "bg-blue-500/10 text-blue-600 border-blue-500/20 dark:text-blue-400" },
|
|
16
|
+
deleted: { label: "삭제", className: "bg-red-500/10 text-red-600 border-red-500/20 dark:text-red-400" },
|
|
17
|
+
renamed: { label: "이동", className: "bg-amber-500/10 text-amber-600 border-amber-500/20 dark:text-amber-400" },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function ChangeTypeBadge({ type }: { type: ChangeType }) {
|
|
21
|
+
const cfg = CHANGE_TYPE_CONFIG[type];
|
|
22
|
+
return (
|
|
23
|
+
<span className={`inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium ${cfg.className}`}>
|
|
24
|
+
{cfg.label}
|
|
25
|
+
</span>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── 병합 결과 토스트 ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function MergeResultBadge({ success, message }: { success: boolean; message: string }) {
|
|
32
|
+
return (
|
|
33
|
+
<span className={[
|
|
34
|
+
"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[10px] font-medium",
|
|
35
|
+
success
|
|
36
|
+
? "border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
|
37
|
+
: "border-red-500/20 bg-red-500/10 text-red-600 dark:text-red-400",
|
|
38
|
+
].join(" ")}>
|
|
39
|
+
{success ? "✓" : "✗"} {success ? "병합 완료" : message.slice(0, 60)}
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Diff 렌더러 ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function DiffView({ patch }: { patch: string }) {
|
|
47
|
+
const lines = patch.split("\n");
|
|
48
|
+
const hunkStart = lines.findIndex((l) => l.startsWith("@@"));
|
|
49
|
+
const diffLines = hunkStart >= 0 ? lines.slice(hunkStart) : lines;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<pre className="overflow-x-auto rounded-lg bg-gray-950/[0.03] p-3 text-[11px] leading-relaxed dark:bg-white/[0.03]">
|
|
53
|
+
{diffLines.map((line, i) => {
|
|
54
|
+
if (line.startsWith("@@")) {
|
|
55
|
+
return <div key={i} className="text-purple-500/70 dark:text-purple-400/70">{line}</div>;
|
|
56
|
+
}
|
|
57
|
+
if (line.startsWith("+")) {
|
|
58
|
+
return <div key={i} className="bg-emerald-500/[0.08] text-emerald-700 dark:text-emerald-400">{line}</div>;
|
|
59
|
+
}
|
|
60
|
+
if (line.startsWith("-")) {
|
|
61
|
+
return <div key={i} className="bg-red-500/[0.08] text-red-700 dark:text-red-400">{line}</div>;
|
|
62
|
+
}
|
|
63
|
+
return <div key={i} className="text-gray-900/50 dark:text-white/40">{line}</div>;
|
|
64
|
+
})}
|
|
65
|
+
</pre>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── 파일 행 ─────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
interface FileRowProps {
|
|
72
|
+
file: ChangelogFile;
|
|
73
|
+
taskId: string;
|
|
74
|
+
agentId: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function FileRow({ file, taskId, agentId }: FileRowProps) {
|
|
78
|
+
const [open, setOpen] = useState(false);
|
|
79
|
+
const [merging, setMerging] = useState(false);
|
|
80
|
+
const [mergeResult, setMergeResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
81
|
+
const { copyCode, status: copyStatus } = useChangelogCodeCopy();
|
|
82
|
+
|
|
83
|
+
const fileName = file.filePath.split("/").pop() ?? file.filePath;
|
|
84
|
+
const dirName = file.filePath.includes("/")
|
|
85
|
+
? file.filePath.slice(0, file.filePath.lastIndexOf("/"))
|
|
86
|
+
: "";
|
|
87
|
+
|
|
88
|
+
const handleMergeFile = async (e: React.MouseEvent) => {
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
setMerging(true);
|
|
91
|
+
setMergeResult(null);
|
|
92
|
+
try {
|
|
93
|
+
const result = await mergeAgentFile(taskId, agentId, file.filePath);
|
|
94
|
+
setMergeResult(result);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
setMergeResult({ success: false, message: err instanceof Error ? err.message : "병합 실패" });
|
|
97
|
+
} finally {
|
|
98
|
+
setMerging(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleCopyPatch = async (e: React.MouseEvent) => {
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
if (!file.patch) return;
|
|
105
|
+
await copyCode(file.patch);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="overflow-hidden rounded-lg border border-gray-900/[0.07] dark:border-white/[0.07]">
|
|
110
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
111
|
+
{/* 펼치기 */}
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => setOpen((p) => !p)}
|
|
115
|
+
className="flex flex-1 min-w-0 items-center gap-2 text-left hover:opacity-80"
|
|
116
|
+
>
|
|
117
|
+
<svg
|
|
118
|
+
viewBox="0 0 16 16"
|
|
119
|
+
fill="currentColor"
|
|
120
|
+
className={`h-3 w-3 shrink-0 text-gray-900/20 transition-transform dark:text-white/20 ${open ? "rotate-90" : ""}`}
|
|
121
|
+
>
|
|
122
|
+
<path fillRule="evenodd" d="M6.22 4.22a.75.75 0 011.06 0l3.25 3.25a.75.75 0 010 1.06l-3.25 3.25a.75.75 0 01-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 010-1.06z" clipRule="evenodd" />
|
|
123
|
+
</svg>
|
|
124
|
+
|
|
125
|
+
<ChangeTypeBadge type={file.changeType} />
|
|
126
|
+
|
|
127
|
+
<span className="min-w-0 flex-1 truncate">
|
|
128
|
+
{dirName && <span className="text-gray-900/30 dark:text-white/30">{dirName}/</span>}
|
|
129
|
+
<span className="font-medium text-gray-900/70 dark:text-white/70">{fileName}</span>
|
|
130
|
+
</span>
|
|
131
|
+
|
|
132
|
+
<span className="shrink-0 text-[10px] text-gray-900/20 dark:text-white/20">
|
|
133
|
+
{file.additions > 0 && <span className="text-emerald-600 dark:text-emerald-400">+{file.additions}</span>}
|
|
134
|
+
{file.additions > 0 && file.deletions > 0 && <span className="mx-0.5 text-gray-900/15 dark:text-white/15">/</span>}
|
|
135
|
+
{file.deletions > 0 && <span className="text-red-600 dark:text-red-400">-{file.deletions}</span>}
|
|
136
|
+
</span>
|
|
137
|
+
</button>
|
|
138
|
+
|
|
139
|
+
{/* 단일 병합 */}
|
|
140
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
141
|
+
{mergeResult && <MergeResultBadge {...mergeResult} />}
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={(e) => void handleMergeFile(e)}
|
|
145
|
+
disabled={merging}
|
|
146
|
+
className="flex items-center gap-1 rounded-md border border-gray-900/[0.08] px-2 py-1 text-[10px] font-medium text-gray-900/35 transition-colors hover:border-blue-500/30 hover:text-blue-600 disabled:opacity-40 dark:border-white/[0.08] dark:text-white/35 dark:hover:text-blue-400"
|
|
147
|
+
>
|
|
148
|
+
{merging
|
|
149
|
+
? <span className="h-2.5 w-2.5 animate-spin rounded-full border border-gray-900/20 border-t-blue-500 dark:border-white/20" />
|
|
150
|
+
: <svg viewBox="0 0 16 16" fill="currentColor" className="h-2.5 w-2.5"><path fillRule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" clipRule="evenodd" /></svg>
|
|
151
|
+
}
|
|
152
|
+
병합
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{open && file.patch && (
|
|
158
|
+
<div className="border-t border-gray-900/[0.05] dark:border-white/[0.05]">
|
|
159
|
+
<div className="flex items-center justify-between gap-2 border-b border-gray-900/[0.05] px-3 py-2 dark:border-white/[0.05]">
|
|
160
|
+
<span className="truncate text-[10px] font-medium text-gray-900/35 dark:text-white/35">
|
|
161
|
+
변경 코드
|
|
162
|
+
</span>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={(e) => void handleCopyPatch(e)}
|
|
166
|
+
aria-label={`${file.filePath} 변경 코드 복사`}
|
|
167
|
+
className={[
|
|
168
|
+
"flex shrink-0 items-center gap-1 rounded-md border px-2 py-1 text-[10px] font-medium transition-colors",
|
|
169
|
+
copyStatus === "copied"
|
|
170
|
+
? "border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
|
171
|
+
: copyStatus === "error"
|
|
172
|
+
? "border-red-500/20 bg-red-500/10 text-red-600 dark:text-red-400"
|
|
173
|
+
: "border-gray-900/[0.08] text-gray-900/35 hover:border-blue-500/30 hover:text-blue-600 dark:border-white/[0.08] dark:text-white/35 dark:hover:text-blue-400",
|
|
174
|
+
].join(" ")}
|
|
175
|
+
>
|
|
176
|
+
<svg viewBox="0 0 16 16" fill="currentColor" className="h-2.5 w-2.5" aria-hidden="true">
|
|
177
|
+
<path d="M4.25 2A2.25 2.25 0 006.5 4.25h2.75A2.75 2.75 0 0112 7v4.75A2.25 2.25 0 009.75 14h-5.5A2.25 2.25 0 012 11.75v-7.5A2.25 2.25 0 014.25 2z" />
|
|
178
|
+
<path d="M6.5 2A2.25 2.25 0 004.25 4.25v7.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75V7A1.25 1.25 0 0010 5.75H6.5A2.25 2.25 0 014.25 3.5V3A1 1 0 015.25 2h1.25z" opacity=".35" />
|
|
179
|
+
</svg>
|
|
180
|
+
{copyStatus === "copied" ? "복사됨" : copyStatus === "error" ? "실패" : "복사"}
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
<DiffView patch={file.patch} />
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── 에이전트 섹션 ────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
interface AgentSectionProps {
|
|
193
|
+
changelog: AgentChangelog;
|
|
194
|
+
agent: TaskAgent | undefined;
|
|
195
|
+
taskId: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function AgentSection({ changelog, agent, taskId }: AgentSectionProps) {
|
|
199
|
+
const [merging, setMerging] = useState(false);
|
|
200
|
+
const [mergeResult, setMergeResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
201
|
+
|
|
202
|
+
const totalAdditions = changelog.files.reduce((s, f) => s + f.additions, 0);
|
|
203
|
+
const totalDeletions = changelog.files.reduce((s, f) => s + f.deletions, 0);
|
|
204
|
+
|
|
205
|
+
const agentLabel = agent
|
|
206
|
+
? `${agent.agentType.charAt(0).toUpperCase() + agent.agentType.slice(1)} · ${agent.customRole ?? agent.role}`
|
|
207
|
+
: `Agent ${changelog.agentId}`;
|
|
208
|
+
|
|
209
|
+
const handleMergeAll = async () => {
|
|
210
|
+
setMerging(true);
|
|
211
|
+
setMergeResult(null);
|
|
212
|
+
try {
|
|
213
|
+
const result = await mergeAgentAll(taskId, changelog.agentId);
|
|
214
|
+
setMergeResult(result);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
setMergeResult({ success: false, message: err instanceof Error ? err.message : "병합 실패" });
|
|
217
|
+
} finally {
|
|
218
|
+
setMerging(false);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div className="flex flex-col gap-2">
|
|
224
|
+
{/* 에이전트 헤더 */}
|
|
225
|
+
<div className="flex items-center justify-between">
|
|
226
|
+
<span className="text-xs font-medium text-gray-900/60 dark:text-white/60">
|
|
227
|
+
{agentLabel}
|
|
228
|
+
</span>
|
|
229
|
+
|
|
230
|
+
<div className="flex items-center gap-2">
|
|
231
|
+
<span className="text-[10px] text-gray-900/25 dark:text-white/25">
|
|
232
|
+
{changelog.files.length}개 파일
|
|
233
|
+
{totalAdditions > 0 && <span className="ml-1.5 text-emerald-600 dark:text-emerald-400">+{totalAdditions}</span>}
|
|
234
|
+
{totalDeletions > 0 && <span className="ml-0.5 text-red-600 dark:text-red-400">-{totalDeletions}</span>}
|
|
235
|
+
</span>
|
|
236
|
+
|
|
237
|
+
{/* 전체 병합 */}
|
|
238
|
+
{mergeResult && <MergeResultBadge {...mergeResult} />}
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={() => void handleMergeAll()}
|
|
242
|
+
disabled={merging}
|
|
243
|
+
className="flex items-center gap-1.5 rounded-lg border border-gray-900/[0.08] px-2.5 py-1 text-[11px] font-medium text-gray-900/40 transition-colors hover:border-blue-500/30 hover:bg-blue-500/[0.05] hover:text-blue-600 disabled:opacity-40 dark:border-white/[0.08] dark:text-white/40 dark:hover:text-blue-400"
|
|
244
|
+
>
|
|
245
|
+
{merging
|
|
246
|
+
? <span className="h-3 w-3 animate-spin rounded-full border border-gray-900/20 border-t-blue-500 dark:border-white/20" />
|
|
247
|
+
: <svg viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3"><path fillRule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm11.78-1.72a.75.75 0 010 1.06l-3.25 3.25a.75.75 0 01-1.06 0L5.72 8.84a.75.75 0 011.06-1.06L8 10.5l3.22-3.22a.75.75 0 011.06 0z" clipRule="evenodd" /></svg>
|
|
248
|
+
}
|
|
249
|
+
전체 병합
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* 파일 목록 */}
|
|
255
|
+
<div className="flex flex-col gap-1.5">
|
|
256
|
+
{changelog.files.map((file) => (
|
|
257
|
+
<FileRow key={file.id} file={file} taskId={taskId} agentId={changelog.agentId} />
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── ChangelogPanel ───────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
interface Props {
|
|
267
|
+
taskId: string;
|
|
268
|
+
agents: TaskAgent[];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function ChangelogPanel({ taskId, agents }: Props) {
|
|
272
|
+
const { changelogs, loading, error } = useTaskChangelog(taskId);
|
|
273
|
+
|
|
274
|
+
if (loading) {
|
|
275
|
+
return (
|
|
276
|
+
<div className="flex flex-col gap-2 py-2">
|
|
277
|
+
{[0, 1, 2].map((i) => (
|
|
278
|
+
<div
|
|
279
|
+
key={i}
|
|
280
|
+
className="h-9 animate-pulse rounded-lg border border-gray-900/[0.05] bg-gray-900/[0.02] dark:border-white/[0.05] dark:bg-white/[0.02]"
|
|
281
|
+
style={{ animationDelay: `${i * 60}ms` }}
|
|
282
|
+
/>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (error) {
|
|
289
|
+
return (
|
|
290
|
+
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-600 dark:border-red-900/40 dark:bg-red-950/20 dark:text-red-400">
|
|
291
|
+
{error}
|
|
292
|
+
</p>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (changelogs.length === 0) {
|
|
297
|
+
return (
|
|
298
|
+
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
299
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="h-8 w-8 text-gray-900/15 dark:text-white/15">
|
|
300
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
301
|
+
</svg>
|
|
302
|
+
<p className="text-xs text-gray-900/30 dark:text-white/30">
|
|
303
|
+
변경사항이 없거나 Git 저장소가 아닙니다
|
|
304
|
+
</p>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div className="flex flex-col gap-4">
|
|
311
|
+
{changelogs.map((changelog) => (
|
|
312
|
+
<AgentSection
|
|
313
|
+
key={changelog.agentId}
|
|
314
|
+
changelog={changelog}
|
|
315
|
+
agent={agents.find((a) => a.id === changelog.agentId)}
|
|
316
|
+
taskId={taskId}
|
|
317
|
+
/>
|
|
318
|
+
))}
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|