Release v2.2.1: Autonomous Task Resumption & Engine Resilience

This commit is contained in:
g1nation
2026-05-14 23:27:51 +09:00
parent e86e3177c7
commit cd22da8735
21 changed files with 848 additions and 126 deletions
+48
View File
@@ -691,6 +691,54 @@
border: 1px solid var(--border);
border-radius: 8px;
}
/* 이어서 진행 섹션 — 빈 목록이면 통째로 숨김. 있을 때만 노출돼서
평소 시야 부담이 없고, 사용자가 멈춘 작업을 즉시 식별 가능. */
.company-resumable-section[data-empty="true"] { display: none; }
.company-resumable-list {
list-style: none;
display: flex; flex-direction: column; gap: 8px;
margin-top: 8px; padding: 0;
}
.company-resumable-card {
border: 1px solid var(--warning);
background: linear-gradient(180deg, rgba(210,153,34,0.08), transparent 70%);
border-radius: 8px;
padding: 10px 12px;
display: flex; flex-direction: column; gap: 6px;
}
.company-resumable-head {
display: flex; gap: 10px; align-items: flex-start; justify-content: space-between;
}
.company-resumable-prompt {
color: var(--text-bright); font-size: 12.5px; font-weight: 600;
line-height: 1.4; flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.company-resumable-actions {
display: flex; gap: 6px; flex-shrink: 0;
}
.company-resumable-resume {
font-size: 11px; padding: 5px 11px;
background: var(--accent); color: var(--bg);
border: 0; border-radius: 6px; cursor: pointer; font-weight: 600;
}
.company-resumable-resume:hover { filter: brightness(1.1); }
.company-resumable-resume[disabled] { opacity: 0.55; cursor: progress; }
.company-resumable-discard {
font-size: 11px; padding: 5px 11px;
background: transparent; color: var(--text-dim);
border: 1px solid var(--border); border-radius: 6px; cursor: pointer;
}
.company-resumable-discard:hover {
color: var(--error); border-color: var(--error);
}
.company-resumable-meta {
display: flex; flex-wrap: wrap; gap: 8px;
font-size: 10px; color: var(--text-dim);
}
.company-resumable-meta span { white-space: nowrap; }
.pipeline-summary-head {
display: flex;
gap: 8px;
+13
View File
@@ -146,6 +146,19 @@
<button class="icon-btn" id="closeCompanyOverlayBtn"></button>
</div>
<!-- 이어서 진행할 수 있는 미완 작업. 빈 목록이면 JS가 섹션 자체를 숨겨서
평소에는 시야에 들어오지 않는다. 각 카드는 [이어서 진행] / [버리기]
두 버튼만 노출. -->
<div id="companyResumableSection" class="map-section company-resumable-section" data-empty="true">
<div class="map-section-head">
<div>
<div class="map-section-title">이어서 진행할 수 있는 작업</div>
<div class="map-section-hint">중간에 멈춘 작업이 있을 때만 노출됩니다. 누른 시점부터 다음 단계가 같은 세션 폴더에 이어 기록됩니다.</div>
</div>
</div>
<ul id="companyResumableList" class="map-list company-resumable-list"></ul>
</div>
<div class="map-section">
<div class="map-section-head">
<div>
+116
View File
@@ -994,6 +994,12 @@
}
break;
}
case 'companyResumable': {
if (typeof window.__renderCompanyResumable === 'function') {
window.__renderCompanyResumable(msg.value || {});
}
break;
}
case 'companyPipelineTemplateContent': {
const tpl = msg.value;
if (!tpl) { showToast('템플릿을 찾을 수 없습니다.', 'warn'); break; }
@@ -1034,6 +1040,7 @@
// Triggered by the Command Palette `Manage 1인 기업 Agents`.
document.getElementById('companyOverlay')?.classList.add('visible');
vscode.postMessage({ type: 'getCompanyAgents' });
vscode.postMessage({ type: 'getCompanyResumable' });
break;
}
case 'companyTurnUpdate': {
@@ -1717,6 +1724,7 @@
_companyStatusEl.textContent = '불러오는 중...';
vscode.postMessage({ type: 'getCompanyAgents' });
vscode.postMessage({ type: 'getCompanyPipelines' });
vscode.postMessage({ type: 'getCompanyResumable' });
};
}
for (const btn of _closeCompanyBtns) {
@@ -2368,6 +2376,114 @@
// expose for the message handler below
window.__renderCompanyPipelines = renderCompanyPipelines;
window.__closePipelineEditor = _closePipelineEditor;
// ──────────────────────────────────────────────────────────────────────
// 이어서 진행 가능 세션 렌더링.
//
// 백엔드가 보낸 items 배열을 카드 목록으로 그린다. 비어 있으면 섹션 자체를
// data-empty="true"로 숨겨 평소 시야에서 사라지게 만든다. 카드는 두 액션:
// - 이어서 진행 → resumeCompanyTurn 메시지
// - 버리기 → discardResumableSession (resume 파일을 'failed'로 마킹)
// ──────────────────────────────────────────────────────────────────────
const _formatRelative = (iso) => {
if (!iso) return '';
const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso;
const diff = Date.now() - d.getTime();
const m = Math.round(diff / 60000);
if (m < 1) return '방금 전';
if (m < 60) return `${m}분 전`;
const h = Math.round(m / 60);
if (h < 24) return `${h}시간 전`;
const days = Math.round(h / 24);
return `${days}일 전`;
};
const _formatAbortReason = (reason) => {
if (!reason) return '도중에 멈춤';
const map = {
'signal-aborted': '시작 직전에 중단',
'aborted-after-plan': '계획 직후 중단',
'aborted-mid-dispatch': '실행 중에 중단',
'aborted-mid-pipeline': '단계 진행 중 중단',
'aborted-mid-approval': '승인 대기 중 중단',
'aborted-by-user-at-approval': '승인 단계에서 중단',
'aborted-before-report': '보고서 직전 중단',
};
return map[reason] || reason;
};
const renderCompanyResumable = (payload) => {
const section = document.getElementById('companyResumableSection');
const list = document.getElementById('companyResumableList');
if (!section || !list) return;
const items = (payload && Array.isArray(payload.items)) ? payload.items : [];
list.innerHTML = '';
if (items.length === 0) {
section.setAttribute('data-empty', 'true');
return;
}
section.setAttribute('data-empty', 'false');
for (const it of items) {
const li = document.createElement('li');
li.className = 'company-resumable-card';
li.dataset.timestamp = it.timestamp;
const head = document.createElement('div');
head.className = 'company-resumable-head';
const prompt = document.createElement('div');
prompt.className = 'company-resumable-prompt';
prompt.textContent = it.userPrompt || '(빈 요청)';
prompt.title = it.userPrompt || '';
const actions = document.createElement('div');
actions.className = 'company-resumable-actions';
const resumeBtn = document.createElement('button');
resumeBtn.className = 'primary company-resumable-resume';
resumeBtn.textContent = '이어서 진행';
resumeBtn.title = '이 작업을 멈췄던 다음 단계부터 같은 세션에 이어 기록합니다.';
resumeBtn.onclick = () => {
// 사용자에게 곧 시작될 거라는 시각 피드백.
resumeBtn.disabled = true;
resumeBtn.textContent = '재개 중…';
vscode.postMessage({ type: 'resumeCompanyTurn', timestamp: it.timestamp });
// overlay를 닫아 채팅 화면이 보이게 — 사용자가 진행 상황 즉시 확인.
document.getElementById('companyOverlay')?.classList.remove('visible');
};
const discardBtn = document.createElement('button');
discardBtn.className = 'company-resumable-discard';
discardBtn.textContent = '버리기';
discardBtn.title = '이 작업을 더 이상 이어가지 않습니다. 목록에서만 빠지고 기존 산출물 파일은 그대로 남습니다.';
discardBtn.onclick = () => {
if (!confirm('이 미완 작업을 목록에서 버릴까요? 이미 만들어진 산출물 파일은 사라지지 않습니다.')) return;
vscode.postMessage({ type: 'discardResumableSession', timestamp: it.timestamp });
};
actions.appendChild(resumeBtn);
actions.appendChild(discardBtn);
head.appendChild(prompt);
head.appendChild(actions);
li.appendChild(head);
const meta = document.createElement('div');
meta.className = 'company-resumable-meta';
const pipelineLabel = it.pipelineName
? `📋 ${escAttr(it.pipelineName)}`
: '🧭 대표 분배 모드';
const progress = (it.totalCount > 0)
? `${it.completedCount}/${it.totalCount} 단계 완료`
: '진행도 정보 없음';
const when = _formatRelative(it.lastUpdatedAt);
const why = it.status === 'aborted'
? `· ${_formatAbortReason(it.abortReason)}`
: (it.status === 'in-progress' ? '· 프로세스 중단 추정' : '');
meta.innerHTML = `<span>${pipelineLabel}</span><span>${escAttr(progress)}</span><span>${escAttr(when)} ${escAttr(why)}</span>`;
li.appendChild(meta);
list.appendChild(li);
}
};
window.__renderCompanyResumable = renderCompanyResumable;
// 템플릿 stamp 시 호출 — id/name 제안값 + stages를 카드 에디터에 미리 채움.
window.__openPipelineEditorWithTemplate = (tpl) => {
if (!tpl) return;