feat: Add close confirmation dialog to ScreenModal and enhance SelectedItemsDetailInputComponent

- Implemented a confirmation dialog in ScreenModal to prevent accidental closure, allowing users to confirm before exiting and potentially losing unsaved data.
- Enhanced SelectedItemsDetailInputComponent by ensuring that base records are created even when detail data is absent, maintaining item-client mapping.
- Improved logging for better traceability during the UPSERT process and refined the handling of parent data mappings for more robust data management.
This commit is contained in:
DDD1542
2026-02-09 13:22:48 +09:00
parent bb4d90fd58
commit 2e500f066f
3 changed files with 454 additions and 116 deletions

View File

@@ -40,32 +40,21 @@ const server = new Server(
);
/**
* Cursor Agent CLI를 통해 에이전트 호출
* Cursor Team Plan 사용 - API 키 불필요!
*
* spawn + stdin 직접 전달 방식으로 쉘 이스케이프 문제 완전 해결
*
* 크로스 플랫폼 지원:
* - Windows: agent (PATH에서 검색)
* - Mac/Linux: ~/.local/bin/agent
* 유틸: ms만큼 대기
*/
async function callAgentCLI(
agentType: AgentType,
task: string,
context?: string
): Promise<string> {
const config = AGENT_CONFIGS[agentType];
// 모델 선택: PM은 opus, 나머지는 sonnet
const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5';
logger.info(`Calling ${agentType} agent via CLI (spawn)`, { model, task: task.substring(0, 100) });
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const userMessage = context
? `${task}\n\n배경 정보:\n${context}`
: task;
const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`;
/**
* Cursor Agent CLI 단일 호출 (내부용)
* spawn + stdin 직접 전달
*/
function spawnAgentOnce(
agentType: AgentType,
fullPrompt: string,
model: string
): Promise<string> {
const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`;
return new Promise<string>((resolve, reject) => {
@@ -93,7 +82,6 @@ async function callAgentCLI(
child.on('error', (err: Error) => {
if (!settled) {
settled = true;
logger.error(`${agentType} agent spawn error`, err);
reject(err);
}
});
@@ -103,7 +91,6 @@ async function callAgentCLI(
settled = true;
if (stderr) {
// 경고/정보 레벨 stderr는 무시
const significantStderr = stderr
.split('\n')
.filter((line: string) => line && !line.includes('warning') && !line.includes('info') && !line.includes('debug'))
@@ -114,13 +101,11 @@ async function callAgentCLI(
}
if (code === 0 || stdout.trim().length > 0) {
// 정상 종료이거나, 에러 코드여도 stdout에 결과가 있으면 성공 처리
logger.info(`${agentType} agent completed via CLI (exit code: ${code})`);
resolve(stdout.trim());
} else {
const errorMsg = `Agent exited with code ${code}. stderr: ${stderr.substring(0, 1000)}`;
logger.error(`${agentType} agent CLI error`, { code, stderr: stderr.substring(0, 1000) });
reject(new Error(errorMsg));
reject(new Error(
`Agent exited with code ${code}. stderr: ${stderr.substring(0, 1000)}`
));
}
});
@@ -129,22 +114,69 @@ async function callAgentCLI(
if (!settled) {
settled = true;
child.kill('SIGTERM');
logger.error(`${agentType} agent timed out after 5 minutes`);
reject(new Error(`${agentType} agent timed out after 5 minutes`));
}
}, 300000);
// 프로세스 종료 시 타이머 클리어
child.on('close', () => clearTimeout(timeout));
// stdin으로 프롬프트 직접 전달 (쉘 이스케이프 문제 없음!)
// stdin으로 프롬프트 직접 전달
child.stdin.write(fullPrompt);
child.stdin.end();
logger.debug(`Prompt sent to ${agentType} agent via stdin (${fullPrompt.length} chars)`);
});
}
/**
* Cursor Agent CLI를 통해 에이전트 호출 (재시도 포함)
*
* - 최대 2회 재시도 (총 3회 시도)
* - 재시도 간 2초 대기 (Cursor CLI 동시 실행 제한 대응)
*/
async function callAgentCLI(
agentType: AgentType,
task: string,
context?: string
): Promise<string> {
const config = AGENT_CONFIGS[agentType];
const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5';
const maxRetries = 2;
logger.info(`Calling ${agentType} agent via CLI (spawn+retry)`, {
model,
task: task.substring(0, 100),
});
const userMessage = context
? `${task}\n\n배경 정보:\n${context}`
: task;
const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`;
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
const delay = attempt * 2000; // 2초, 4초
logger.info(`${agentType} agent retry ${attempt}/${maxRetries} (waiting ${delay}ms)`);
await sleep(delay);
}
const result = await spawnAgentOnce(agentType, fullPrompt, model);
logger.info(`${agentType} agent completed (attempt ${attempt + 1})`);
return result;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
logger.warn(`${agentType} agent attempt ${attempt + 1} failed`, {
error: lastError.message.substring(0, 200),
});
}
}
// 모든 재시도 실패
logger.error(`${agentType} agent failed after ${maxRetries + 1} attempts`);
throw lastError!;
}
/**
* 도구 목록 핸들러
*/
@@ -310,12 +342,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}>;
};
logger.info(`Parallel ask to ${requests.length} agents (TRUE PARALLEL!)`);
logger.info(`Parallel ask to ${requests.length} agents (STAGGERED PARALLEL)`);
// 시차 병렬 실행: 각 에이전트를 500ms 간격으로 시작
// Cursor Agent CLI 동시 실행 제한 대응
const STAGGER_DELAY = 500; // ms
// 진짜 병렬 실행! 모든 에이전트가 동시에 작업
const results: ParallelResult[] = await Promise.all(
requests.map(async (req) => {
requests.map(async (req, index) => {
try {
// 시차 적용 (첫 번째는 즉시, 이후 500ms 간격)
if (index > 0) {
await sleep(index * STAGGER_DELAY);
}
const result = await callAgentCLI(req.agent, req.task, req.context);
return { agent: req.agent, result };
} catch (error) {