Release v2.2.0: Milestone - Human-Centric UI & Workflow Evolution

This commit is contained in:
g1nation
2026-05-14 22:58:45 +09:00
parent d9d89e6db7
commit e86e3177c7
12 changed files with 334 additions and 124 deletions
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1778765910142,
"createdAt": 1778767038020,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1778765910134,
"createdAt": 1778767038017,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"createdAt": 1778765910130,
"createdAt": 1778767038016,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "---\nid: stress_conflict_1778765910115\ndate: 2026-05-14T13:38:30.146Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (4ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (9ms)\n",
"createdAt": 1778765910146,
"result": "---\nid: stress_conflict_1778767038004\ndate: 2026-05-14T13:57:18.021Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (0ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n",
"createdAt": 1778767038021,
"modelVersion": "unknown"
}
@@ -1,8 +1,8 @@
{
"missionId": "stress_conflict_1778765910115",
"missionId": "stress_conflict_1778767038004",
"status": "completed",
"startTime": "2026-05-14T13:38:30.115Z",
"totalElapsedMs": 31,
"startTime": "2026-05-14T13:57:18.005Z",
"totalElapsedMs": 16,
"results": {
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
@@ -18,28 +18,28 @@
"to": "planner",
"durationMs": 11,
"message": "전략 수립 중...",
"ts": "2026-05-14T13:38:30.126Z"
"ts": "2026-05-14T13:57:18.016Z"
},
{
"from": "planner",
"to": "researcher",
"durationMs": 4,
"durationMs": 0,
"message": "핵심 정보 수집 및 분석 중...",
"ts": "2026-05-14T13:38:30.130Z"
"ts": "2026-05-14T13:57:18.016Z"
},
{
"from": "researcher",
"to": "writer",
"durationMs": 9,
"durationMs": 1,
"message": "최종 리포트 작성 및 편집 중...",
"ts": "2026-05-14T13:38:30.139Z"
"ts": "2026-05-14T13:57:18.017Z"
},
{
"from": "writer",
"to": "completed",
"durationMs": 7,
"durationMs": 4,
"message": "미션 완료",
"ts": "2026-05-14T13:38:30.146Z"
"ts": "2026-05-14T13:57:18.021Z"
}
],
"resilienceMetrics": {
+11
View File
@@ -1,5 +1,16 @@
# Astra Patch Notes
## v2.2.0 (2026-05-14)
### 💎 Milestone: Human-Centric UI & Workflow Evolution
- **UI 용어 및 인터랙션 전면 한글화:** '에이전트', '파이프라인' 등 딱딱한 용어를 '팀원', '작업 흐름' 등 직관적인 한글로 순화하여 친숙도를 높였습니다.
- **신규 사용자 가이드 고도화:** 시작 체크리스트와 예시 질문 칩을 통해 첫 사용자가 Astra의 강력한 기능을 즉시 체험할 수 있도록 설계했습니다.
- **작업 흐름(Pipeline) 에디터 개선:** 복잡한 설정 없이도 템플릿을 통해 '대표에게 맡기거나', '사용자가 직접 정한 순서대로' 팀원이 이어서 작업하게 만듭니다.
- **팀원 관리 인터페이스 최적화:** 새로운 팀원을 추가할 때 내부 ID와 테마 색상, 응답 스타일 등을 더욱 세밀하게 설정할 수 있도록 UI를 정교화했습니다.
- **시각적 일관성 강화:** 사이드바 전반의 디자인 토큰을 재검토하여 더욱 프리미엄하고 일관된 룩앤필을 완성했습니다.
---
## v2.1.9 (2026-05-14)
### 🚀 Immersive Onboarding & UX Transformation
- **신규 사용자 온보딩 프로세스 도입:** 사이드바에 3단계 체크리스트(두뇌 연결, 모델 선택, 첫 질문)를 추가하여 초기 설정 과정을 직관적으로 개선했습니다.
Binary file not shown.
+1 -1
View File
@@ -7,5 +7,5 @@
"corePurpose": "",
"detailLevel": "standard",
"createdAt": "2026-05-13T13:09:33.788Z",
"updatedAt": "2026-05-14T13:26:13.750Z"
"updatedAt": "2026-05-14T13:41:02.603Z"
}
+140 -7
View File
@@ -332,7 +332,24 @@
.company-name-input:focus { border-color: var(--accent); outline: none; }
/* Agent cards inside the manage overlay. */
.company-agent-list { display: flex; flex-direction: column; gap: 6px; padding: 0; }
.company-agent-list { display: flex; flex-direction: column; gap: 8px; padding: 0; }
.company-agent-section-label {
list-style: none;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin: 6px 2px -2px;
color: var(--text-bright);
font-size: 11px;
font-weight: 700;
}
.company-agent-section-label small {
color: var(--text-dim);
font-size: 9.5px;
font-weight: 500;
text-align: right;
}
/*
* Agent card layout, rebuilt 2026-05-14 to fix overflow:
* - Card itself stacks its rows VERTICALLY (`flex-direction: column`).
@@ -351,12 +368,14 @@
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 10px;
padding: 10px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
list-style: none;
overflow: hidden;
position: relative;
transition: border-color 0.15s ease, background 0.15s ease, opacity 0.15s ease;
}
/* 비활성 카드는 더 흐릿하게 + 좌측 액센트 바를 떨궈서 한 눈에 그룹 구분되도록. */
.company-agent-card[data-active="false"] {
@@ -365,7 +384,7 @@
border-style: dashed;
}
.company-agent-card[data-active="true"] {
border-left: 3px solid var(--accent);
border-left: 3px solid var(--agent-color, var(--accent));
}
.company-agent-card[data-locked="true"] {
border-left-color: #FACC15; /* CEO는 골드 액센트 */
@@ -423,6 +442,7 @@
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px;
border-radius: 6px; background: var(--bg-secondary);
border: 1px solid var(--border);
}
.company-agent-body {
flex: 1 1 180px; /* prefer ≥180px, shrink down to its content */
@@ -440,20 +460,47 @@
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-top: 1px;
}
.company-agent-meta {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin-top: 5px;
}
.company-agent-chip {
display: inline-flex;
align-items: center;
height: 18px;
padding: 0 7px;
border-radius: 999px;
border: 1px solid var(--border);
color: var(--text-dim);
background: var(--bg-secondary);
font-size: 9.5px;
line-height: 1;
white-space: nowrap;
}
.company-agent-chip.tuned {
color: var(--accent);
border-color: var(--accent);
background: var(--accent-glow);
}
.company-agent-controls {
display: flex; align-items: center; gap: 6px;
flex-shrink: 0;
margin-left: auto; /* push to the right of the head row */
max-width: 100%;
flex-wrap: wrap;
justify-content: flex-end;
}
.company-agent-toggle {
background: transparent; border: 1px solid var(--border);
color: var(--text-dim); font-size: 10px; font-weight: 600;
padding: 3px 8px; border-radius: 999px; cursor: pointer;
padding: 4px 9px; border-radius: 999px; cursor: pointer;
flex-shrink: 0;
}
.company-agent-card[data-active="true"] .company-agent-toggle {
border-color: var(--accent); color: var(--accent);
border-color: var(--agent-color, var(--accent)); color: var(--text-bright);
background: var(--accent-glow);
}
.company-agent-model {
background: var(--input-bg); border: 1px solid var(--border);
@@ -475,7 +522,7 @@
.company-agent-edit {
background: transparent; border: 1px solid var(--border);
color: var(--text-dim); font-size: 10px;
padding: 3px 6px; border-radius: 6px; cursor: pointer;
padding: 4px 8px; border-radius: 6px; cursor: pointer;
flex-shrink: 0;
}
.company-agent-edit:hover { color: var(--accent); border-color: var(--accent); }
@@ -484,6 +531,36 @@
background: var(--accent-glow);
}
.company-agent-settings {
display: none;
margin-top: 2px;
padding: 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-secondary);
}
.company-agent-card[data-settings-expanded="true"] .company-agent-settings { display: block; }
.company-agent-settings-grid {
display: grid;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
gap: 8px;
margin-bottom: 8px;
}
.company-agent-settings-grid label {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
color: var(--text-dim);
font-size: 10px;
}
.company-agent-settings .company-agent-role-select,
.company-agent-settings .company-agent-model {
width: 100%;
max-width: 100%;
height: 28px;
}
/* Per-agent Knowledge Mix slider. Wraps so the slider always has
breathing room — hint + checkbox flow to next line when needed. */
.company-agent-mix-row {
@@ -597,13 +674,69 @@
background: var(--accent); border-color: var(--accent); color: #fff;
}
.pipeline-empty-state {
list-style: none;
color: var(--text-dim);
padding: 12px 10px;
border: 1px dashed var(--border);
border-radius: 8px;
background: var(--bg-secondary);
font-size: 11px;
text-align: center;
}
.pipeline-summary-card {
list-style: none;
padding: 10px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
}
.pipeline-summary-head {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
}
.pipeline-summary-title {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
line-height: 1.25;
}
.pipeline-summary-title strong {
color: var(--text-bright);
font-size: 12px;
}
.pipeline-summary-title span {
color: var(--text-dim);
font-size: 10px;
}
.pipeline-summary-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.pipeline-summary-flow {
margin-top: 8px;
padding: 7px 8px;
border-radius: 6px;
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 10.5px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Pipeline stage cards ───────────────────────────────────── */
.pipeline-stage-list {
list-style: none; margin: 0; padding: 0;
display: flex; flex-direction: column; gap: 8px;
}
.pipeline-stage-list:empty::before {
content: '아직 단계가 없습니다. "+ Stage 추가" 로 첫 단계를 만드세요.';
content: '아직 단계가 없습니다. "+ 단계 추가" 로 첫 작업을 만드세요.';
font-size: 11px; color: var(--text-dim); font-style: italic;
padding: 12px 4px; display: block; text-align: center;
}
+23 -23
View File
@@ -162,11 +162,11 @@
<div class="map-section">
<div class="map-section-head">
<div>
<div class="map-section-title">활성 에이전트 + 모델</div>
<div class="map-section-hint">CEO는 항상 활성. 각 에이전트별로 모델을 따로 지정할 수 있습니다 — 다른 모델을 쓸 때만 LM Studio가 swap합니다.</div>
<div class="map-section-title">AI 팀 구성</div>
<div class="map-section-hint">이번 작업에 참여할 팀원을 고릅니다. 대표는 항상 참여하고, 모델·지식 반영은 필요할 때만 조정하세요.</div>
</div>
<div class="map-btn-group">
<button class="secondary-btn" id="addCompanyAgentBtn" title="새 사용자 에이전트 추가">+ 에이전트 추가</button>
<button class="secondary-btn" id="addCompanyAgentBtn" title="새 사용자 에이전트 추가">+ 팀원 추가</button>
</div>
</div>
<ul id="companyAgentList" class="map-list company-agent-list"></ul>
@@ -177,9 +177,9 @@
(no separate modal) so the user can see existing agents while
editing — easier to spot id collisions. -->
<div id="addCompanyAgentForm" class="map-section company-agent-add-form" data-open="false">
<div class="map-section-title" style="margin-bottom:8px;">에이전트 추가</div>
<div class="map-section-title" style="margin-bottom:8px;">팀원 추가</div>
<div class="company-agent-add-grid">
<label class="field-label">ID (소문자/숫자/-/_, 예: marketer)
<label class="field-label">내부 ID (고급 · 소문자/숫자/-/_, 예: marketer)
<input type="text" id="newAgentId" placeholder="marketer" />
</label>
<label class="field-label">이름
@@ -191,24 +191,24 @@
<label class="field-label">이모지
<input type="text" id="newAgentEmoji" placeholder="📣" maxlength="4" />
</label>
<label class="field-label">색상 (#hex)
<label class="field-label">테마 색상 (#hex)
<input type="text" id="newAgentColor" placeholder="#3B82F6" />
</label>
<label class="field-label" style="grid-column:1/-1;">직군 (담당 역할 분류)
<label class="field-label" style="grid-column:1/-1;">역할 그룹
<select id="newAgentRoleCategory"></select>
</label>
<label class="field-label" style="grid-column:1/-1;">한 줄 태그라인
<input type="text" id="newAgentTagline" placeholder="브랜드 메시지·캠페인을 설계합니다" />
</label>
<label class="field-label" style="grid-column:1/-1;">전문 분야 (CEO가 매칭에 사용)
<label class="field-label" style="grid-column:1/-1;">잘하는 일
<textarea id="newAgentSpecialty" rows="2" placeholder="캠페인 기획, 메시지 설계, 채널 분석"></textarea>
</label>
<label class="field-label" style="grid-column:1/-1;">페르소나 (선택)
<label class="field-label" style="grid-column:1/-1;">응답 스타일 (선택)
<textarea id="newAgentPersona" rows="2" placeholder="데이터 기반·간결한 톤. 가설 검증 사이클을 좋아함."></textarea>
</label>
</div>
<div class="editor-actions" style="margin-top:10px;">
<button id="cancelAddAgentBtn">Cancel</button>
<button id="cancelAddAgentBtn">취소</button>
<button class="primary" id="saveAddAgentBtn">추가</button>
</div>
<div id="addAgentError" class="map-status" style="color:var(--error); margin-top:6px;"></div>
@@ -220,33 +220,33 @@
<div class="map-section">
<div class="map-section-head">
<div>
<div class="map-section-title">워크 파이프라인</div>
<div class="map-section-hint">CEO 자유 분배 대신 사용자가 정한 stage 순서대로 dispatch합니다. loop-back 정규식이 매칭되면 이전 stage로 되돌아갑니다 (최대 maxIterations 회).</div>
<div class="map-section-title">작업 흐름</div>
<div class="map-section-hint">대표에게 맡기거나, 사용자가 정한 순서대로 팀원이 이어서 작업하게 만듭니다.</div>
</div>
<div class="map-btn-group">
<select id="pipelineTemplateSel" title="템플릿에서 새 파이프라인 만들기"
<select id="pipelineTemplateSel" title="템플릿에서 새 작업 흐름 만들기"
style="padding:3px 6px; font-size:10px; background:var(--surface); color:var(--text-primary); border:1px solid var(--border); border-radius:5px;">
<option value="">📋 템플릿에서…</option>
<option value="">템플릿으로 시작</option>
</select>
<button class="secondary-btn" id="addCompanyPipelineBtn" title="빈 파이프라인부터 시작">+ 빈 파이프라인</button>
<button class="secondary-btn" id="addCompanyPipelineBtn" title="빈 작업 흐름부터 시작">+ 직접 만들기</button>
</div>
</div>
<div class="control-row" style="margin-top:8px; gap:8px; align-items:center;">
<label style="font-size:10px; color:var(--text-dim);">활성:</label>
<label style="font-size:10px; color:var(--text-dim);">현재 흐름:</label>
<select id="activePipelineSel" style="flex:1; padding:4px 8px; background:var(--bg); color:var(--text-primary); border:1px solid var(--border); border-radius:6px; font-size:11px;">
<option value="">기본 (CEO 자유 분배)</option>
<option value="">대표가 알아서 분배</option>
</select>
</div>
<ul id="companyPipelineList" class="map-list" style="margin-top:8px;"></ul>
</div>
<!-- Pipeline editor — 카드형 stage 에디터.
stage는 직군 → 담당 cascading + 지시 텍스트 + 재시도 옵션을
<!-- Pipeline editor — 카드형 단계 에디터.
단계는 역할 그룹 → 담당 cascading + 지시 텍스트 + 재시도 옵션을
가진 카드 한 장으로 표현. JSON 직접 편집은 더 이상 불필요. -->
<div id="pipelineEditForm" class="map-section company-agent-add-form" data-open="false">
<div class="map-section-title" style="margin-bottom:8px;">파이프라인 편집</div>
<div class="map-section-title" style="margin-bottom:8px;">작업 흐름 편집</div>
<div class="company-agent-add-grid">
<label class="field-label">ID (한 번 정하면 변경 불가)
<label class="field-label">내부 ID (고급 · 한 번 정하면 변경 불가)
<input type="text" id="pipelineEditId" placeholder="product-dev-v1" />
</label>
<label class="field-label">이름
@@ -254,12 +254,12 @@
</label>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:12px; margin-bottom:6px;">
<div style="font-size:11px; font-weight:700; color:var(--text-bright);">단계 (Stages)</div>
<div style="font-size:11px; font-weight:700; color:var(--text-bright);">작업 단계</div>
<button class="secondary-btn" id="addStageBtn" style="font-size:10px; padding:3px 8px;">+ 단계 추가</button>
</div>
<ul id="pipelineStageList" class="pipeline-stage-list"></ul>
<div class="editor-actions" style="margin-top:10px;">
<button id="cancelPipelineEditBtn">Cancel</button>
<button id="cancelPipelineEditBtn">취소</button>
<button class="primary" id="savePipelineEditBtn">저장</button>
</div>
<div id="pipelineEditError" class="map-status" style="color:var(--error); margin-top:6px;"></div>
+143 -77
View File
@@ -1009,22 +1009,22 @@
if (v.ok) {
if (errEl) errEl.textContent = '';
if (typeof window.__closePipelineEditor === 'function') window.__closePipelineEditor();
showToast('✅ 파이프라인 저장 완료', 'info');
showToast('작업 흐름 저장 완료', 'info');
} else {
if (errEl) errEl.textContent = v.reason || '파이프라인 저장 실패.';
if (errEl) errEl.textContent = v.reason || '작업 흐름 저장 실패.';
}
break;
}
case 'deleteCompanyPipelineResult': {
const v = msg.value || {};
if (v.ok) showToast(`🗑 파이프라인 '${v.pipelineId}' 삭제됨`, 'warn');
if (v.ok) showToast(`작업 흐름 '${v.pipelineId}' 삭제됨`, 'warn');
else showToast(`삭제 실패: ${v.reason || '알 수 없음'}`, 'warn');
break;
}
case 'setActiveCompanyPipelineResult': {
const v = msg.value || {};
if (v.ok) {
showToast(v.pipelineId ? `▶ 파이프라인 '${v.pipelineId}' 활성` : '▶ 기본(CEO 자유 분배)로 복귀', 'info');
showToast(v.pipelineId ? `작업 흐름 '${v.pipelineId}' 사용 중` : '대표가 알아서 분배하도록 변경', 'info');
} else {
showToast(`활성 설정 실패: ${v.reason || '알 수 없음'}`, 'warn');
}
@@ -2021,7 +2021,7 @@
// populateAgentModelSelect는 첫 옵션 라벨이 "default (global)"인데
// stage 맥락에선 "기본 (에이전트 설정 사용)"이 더 정확. 첫 옵션 텍스트만 교체.
if (stageModelSel.options.length > 0 && stageModelSel.options[0].value === '') {
stageModelSel.options[0].text = '기본 (에이전트 설정)';
stageModelSel.options[0].text = '담당자 설정 사용';
}
stageModelSel.onchange = () => { stage.modelOverride = stageModelSel.value || ''; };
row.appendChild(modelLbl); row.appendChild(stageModelSel);
@@ -2040,15 +2040,15 @@
approvalCb.onchange = () => { stage.requiresApproval = approvalCb.checked; };
approvalWrap.appendChild(approvalCb);
const approvalText = document.createElement('span');
approvalText.textContent = '이 단계 후 내 승인 받기';
approvalText.title = '체크하면 stage 완료 후 자동 진행하지 않고 승인/수정요청/중단 버튼이 채팅창에 ';
approvalText.textContent = '이 단계 후 내 승인 받기';
approvalText.title = '체크하면 단계 완료 후 자동 진행하지 않고 승인/수정요청/중단 버튼이 채팅창에 뜹니다';
approvalWrap.appendChild(approvalText);
body.appendChild(approvalWrap);
// 지시 텍스트 + 토큰 버튼
const instrLabelDiv = document.createElement('div');
instrLabelDiv.className = 'psc-field-label';
instrLabelDiv.textContent = '지시 내용 (담당자에게 전달될 메시지)';
instrLabelDiv.textContent = '담당자에게 전달할 요청';
body.appendChild(instrLabelDiv);
const tokens = document.createElement('div');
@@ -2091,20 +2091,20 @@
const summary = document.createElement('summary');
const hasLoop = !!(stage.loopBackPattern && stage.loopBackTo);
summary.textContent = hasLoop
? `🔁 재시도 활성: "${stage.loopBackPattern}" 발견 시 → ${_editStages.find((s) => s.id === stage.loopBackTo)?.label || stage.loopBackTo}`
: '🔁 재시도 조건 설정하기 (선택)';
? `재작업 활성: "${stage.loopBackPattern}" 발견 시 → ${_editStages.find((s) => s.id === stage.loopBackTo)?.label || stage.loopBackTo}`
: '문제가 있으면 이전 단계로 되돌리기 (선택)';
loop.appendChild(summary);
const grid = document.createElement('div');
grid.className = 'psc-loop-grid';
// condition
const condLbl = document.createElement('label'); condLbl.textContent = '조건 (regex):';
const condLbl = document.createElement('label'); condLbl.textContent = '감지할 표현:';
const condInput = document.createElement('input');
condInput.type = 'text';
condInput.placeholder = '예: 버그|오류|fail|재작업';
condInput.value = stage.loopBackPattern || '';
condInput.oninput = () => { stage.loopBackPattern = condInput.value; };
// target
const tgtLbl = document.createElement('label'); tgtLbl.textContent = '돌아갈 단계:';
const tgtLbl = document.createElement('label'); tgtLbl.textContent = '돌아갈 단계:';
const tgtSel = document.createElement('select');
const emptyOpt = document.createElement('option');
emptyOpt.value = ''; emptyOpt.textContent = '(선택 안 함)';
@@ -2296,11 +2296,11 @@
// 템플릿 드롭다운 채우기.
const tplSel = document.getElementById('pipelineTemplateSel');
if (tplSel && payload && Array.isArray(payload.templates)) {
tplSel.innerHTML = '<option value="">📋 템플릿에서…</option>';
tplSel.innerHTML = '<option value="">템플릿으로 시작</option>';
for (const t of payload.templates) {
const opt = document.createElement('option');
opt.value = t.templateId;
opt.textContent = `${t.name} (${t.stageCount}단계)`;
opt.textContent = `${t.name} · ${t.stageCount}단계`;
opt.title = t.description || '';
tplSel.appendChild(opt);
}
@@ -2310,11 +2310,11 @@
_renderStages();
}
// active dropdown
_activePipelineSel.innerHTML = '<option value="">기본 (CEO 자유 분배)</option>';
_activePipelineSel.innerHTML = '<option value="">대표가 알아서 분배</option>';
for (const p of Object.values(pipelines)) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = `${p.name || p.id} (${(p.stages || []).length} stages)`;
opt.textContent = `${p.name || p.id} · ${(p.stages || []).length}단계`;
_activePipelineSel.appendChild(opt);
}
_activePipelineSel.value = activeId;
@@ -2323,32 +2323,31 @@
const entries = Object.values(pipelines);
if (entries.length === 0) {
const li = document.createElement('li');
li.className = 'map-item';
li.style.cssText = 'color: var(--text-dim); font-style: italic; padding: 6px 4px;';
li.textContent = '아직 파이프라인이 없습니다. "+ Pipeline" 으로 새로 만드세요.';
li.className = 'pipeline-empty-state';
li.textContent = '아직 저장된 작업 흐름이 없습니다. 템플릿으로 시작하거나 직접 만들어보세요.';
_pipelineList.appendChild(li);
return;
}
for (const p of entries) {
const li = document.createElement('li');
li.className = 'map-item';
li.style.cssText = 'padding:8px 10px; background:var(--surface); border:1px solid var(--border); border-radius:6px;';
li.className = 'pipeline-summary-card';
const head = document.createElement('div');
head.style.cssText = 'display:flex; gap:8px; align-items:center; justify-content:space-between;';
head.className = 'pipeline-summary-head';
const title = document.createElement('div');
title.innerHTML = `<strong>${escAttr(p.name || p.id)}</strong> <span style="font-size:10px; color:var(--text-dim)">${escAttr(p.id)} · ${(p.stages || []).length} stages${p.id === activeId ? ' · <span style="color:var(--accent)">● 활성</span>' : ''}</span>`;
title.className = 'pipeline-summary-title';
title.innerHTML = `<strong>${escAttr(p.name || p.id)}</strong><span>${(p.stages || []).length}단계${p.id === activeId ? ' · 현재 사용 중' : ''}</span>`;
const actions = document.createElement('div');
actions.style.cssText = 'display:flex; gap:4px;';
actions.className = 'pipeline-summary-actions';
const editBtn = document.createElement('button');
editBtn.className = 'company-agent-edit';
editBtn.textContent = '편집';
editBtn.textContent = '편집';
editBtn.onclick = () => _openPipelineEditor(p);
const delBtn = document.createElement('button');
delBtn.className = 'company-agent-edit';
delBtn.textContent = '🗑';
delBtn.title = '파이프라인 삭제';
delBtn.textContent = '삭제';
delBtn.title = '작업 흐름 삭제';
delBtn.onclick = () => {
if (!confirm(`'${p.name || p.id}' 파이프라인을 삭제할까요?`)) return;
if (!confirm(`'${p.name || p.id}' 작업 흐름을 삭제할까요?`)) return;
vscode.postMessage({ type: 'deleteCompanyPipeline', pipelineId: p.id });
};
actions.appendChild(editBtn);
@@ -2356,6 +2355,13 @@
head.appendChild(title);
head.appendChild(actions);
li.appendChild(head);
const flow = document.createElement('div');
flow.className = 'pipeline-summary-flow';
const stages = Array.isArray(p.stages) ? p.stages : [];
flow.textContent = stages.length > 0
? stages.map((s) => s.label || s.id).join(' → ')
: '단계가 없습니다';
li.appendChild(flow);
_pipelineList.appendChild(li);
}
};
@@ -2400,7 +2406,7 @@
sel.innerHTML = '';
const useDefault = document.createElement('option');
useDefault.value = '';
useDefault.innerText = 'default (global)';
useDefault.innerText = '기본 모델 사용';
sel.appendChild(useDefault);
const seen = new Set();
for (const opt of modelSel.options) {
@@ -2414,7 +2420,7 @@
if (current && !seen.has(current)) {
const o = document.createElement('option');
o.value = current;
o.innerText = `${current} (saved)`;
o.innerText = `${current} (저장됨)`;
sel.appendChild(o);
}
sel.value = current || '';
@@ -2464,12 +2470,30 @@
return x.i - y.i;
})
.map((p) => p.a);
let sectionKey = '';
const appendTeamSection = (key, title, hint) => {
if (sectionKey === key) return;
sectionKey = key;
const section = document.createElement('li');
section.className = 'company-agent-section-label';
// 시각적 그룹핑용 라벨 — 스크린리더는 카드 갯수만 세도록 presentation 처리.
section.setAttribute('role', 'presentation');
section.setAttribute('aria-hidden', 'true');
section.innerHTML = `<span>${escAttr(title)}</span><small>${escAttr(hint)}</small>`;
_companyAgentList.appendChild(section);
};
for (const a of agents) {
if (a.alwaysOn || a.active) {
appendTeamSection('active', '참여 중', '이번 작업에 함께 응답합니다');
} else {
appendTeamSection('standby', '대기 중', '필요할 때 팀에 합류시킬 수 있습니다');
}
const li = document.createElement('li');
li.className = 'company-agent-card';
li.setAttribute('data-active', a.active ? 'true' : 'false');
if (a.alwaysOn) li.setAttribute('data-locked', 'true');
li.dataset.agentId = a.id;
li.style.setProperty('--agent-color', a.color || 'var(--accent)');
// ── Row 1: emoji + name/tagline + controls ──
// CSS handles layout via `.company-agent-head` (flex-wrap,
@@ -2485,13 +2509,30 @@
body.className = 'company-agent-body';
const name = document.createElement('div');
name.className = 'company-agent-name';
name.innerHTML = `${escAttr(a.name)} <span class="company-agent-role">${escAttr(a.role)}</span>`;
name.innerHTML = `<span>${escAttr(a.name)}</span><span class="company-agent-role">${escAttr(a.role)}</span>`;
const tag = document.createElement('div');
tag.className = 'company-agent-tagline';
tag.textContent = a.tagline || '';
tag.title = a.specialty || '';
body.appendChild(name);
body.appendChild(tag);
const meta = document.createElement('div');
meta.className = 'company-agent-meta';
const roleChip = document.createElement('span');
roleChip.className = 'company-agent-chip';
roleChip.textContent = _roleCategoryLabels[a.roleCategory || a.defaultRoleCategory] || a.roleCategory || a.defaultRoleCategory || '역할';
const modelChip = document.createElement('span');
modelChip.className = 'company-agent-chip';
modelChip.textContent = a.modelOverride ? '전용 모델' : '기본 모델';
meta.appendChild(roleChip);
meta.appendChild(modelChip);
if (a.personaOverridden || a.specialtyOverridden || a.taglineOverridden || a.nameOverridden || a.roleOverridden) {
const tunedChip = document.createElement('span');
tunedChip.className = 'company-agent-chip tuned';
tunedChip.textContent = '커스텀';
meta.appendChild(tunedChip);
}
body.appendChild(meta);
const controls = document.createElement('div');
controls.className = 'company-agent-controls';
@@ -2542,57 +2583,79 @@
const editBtn = document.createElement('button');
editBtn.className = 'company-agent-edit';
editBtn.textContent = '✎ 편집';
editBtn.textContent = '프로필';
if (a.personaOverridden || a.specialtyOverridden || a.taglineOverridden) {
editBtn.classList.add('dirty');
editBtn.title = '프롬프트가 사용자에 의해 편집되었습니다 (원본과 다름)';
editBtn.title = '프로필과 응답 방식이 사용자에 의해 조정되었습니다';
} else {
editBtn.title = '이름, 역할, 잘하는 일, 응답 스타일 편집';
}
editBtn.onclick = () => {
const expanded = li.getAttribute('data-expanded') === 'true';
li.setAttribute('data-expanded', expanded ? 'false' : 'true');
// 카드가 갑자기 두 배로 길어지지 않게 다른 패널은 자동으로 접는다.
li.setAttribute('data-settings-expanded', 'false');
};
const settingsBtn = document.createElement('button');
settingsBtn.className = 'company-agent-edit company-agent-settings-btn';
settingsBtn.textContent = '설정';
settingsBtn.title = '역할 그룹, 모델, 참고 지식 반영 조정';
settingsBtn.onclick = () => {
const expanded = li.getAttribute('data-settings-expanded') === 'true';
li.setAttribute('data-settings-expanded', expanded ? 'false' : 'true');
li.setAttribute('data-expanded', 'false');
};
const toggle = document.createElement('button');
toggle.className = 'company-agent-toggle';
toggle.textContent = a.active ? '켜짐' : '꺼짐';
toggle.textContent = a.active ? '참여 중' : '+ 참여';
if (a.alwaysOn) {
toggle.disabled = true;
toggle.textContent = '고정';
toggle.textContent = '항상 참여';
} else {
toggle.onclick = () => {
const wantActive = !(li.getAttribute('data-active') === 'true');
li.setAttribute('data-active', wantActive ? 'true' : 'false');
toggle.textContent = wantActive ? '켜짐' : '꺼짐';
const nextIds = Array.from(_companyAgentList.querySelectorAll('.company-agent-card'))
.filter(el => el.getAttribute('data-active') === 'true')
.map(el => el.dataset.agentId)
// ── Optimistic update ──
// 백엔드 응답이 오기 전에 카드를 옳은 섹션(참여 중 / 대기 중)으로
// 즉시 이동시켜야 라벨과 카드 위치가 어긋난 상태로 머무르지 않는다.
// 캐시된 페이로드의 active 플래그를 갱신한 뒤 같은 페이로드로 한 번 더
// 렌더하면 sort + 섹션 라벨이 새 상태대로 재계산됨.
if (_lastCompanyAgentsPayload && Array.isArray(_lastCompanyAgentsPayload.agents)) {
const found = _lastCompanyAgentsPayload.agents.find((x) => x.id === a.id);
if (found) found.active = wantActive;
}
const nextIds = (_lastCompanyAgentsPayload?.agents || [])
.filter((x) => x.active || x.alwaysOn)
.map((x) => x.id)
.filter(Boolean);
// 백엔드 알림은 먼저 보내고 (네트워크 latency 흡수), 그 직후 동기 재렌더로
// UI를 정렬. 백엔드 응답이 오면 같은 결과로 한 번 더 그려지지만 idempotent.
vscode.postMessage({ type: 'setCompanyActiveAgents', value: nextIds });
if (_lastCompanyAgentsPayload) renderCompanyAgentCards(_lastCompanyAgentsPayload);
};
}
controls.appendChild(roleSelEl);
controls.appendChild(modelSelEl);
controls.appendChild(editBtn);
controls.appendChild(settingsBtn);
// 삭제 버튼 — CEO만 빼고 빌트인/커스텀 모두 노출. 단, 어떤 워크 파이프라인의
// stage라도 이 에이전트를 참조하고 있으면 disabled로 처리하고 tooltip에
// 사용 중인 파이프라인을 적어, 사용자가 어디로 가서 빼야 하는지 보이게 한다.
if (!a.alwaysOn) {
const delBtn = document.createElement('button');
delBtn.className = 'company-agent-edit company-agent-delete';
delBtn.textContent = '🗑';
delBtn.textContent = a.custom ? '삭제' : '숨김';
const usedIn = Array.isArray(a.usedInPipelines) ? a.usedInPipelines : [];
if (usedIn.length > 0) {
delBtn.disabled = true;
delBtn.classList.add('disabled');
delBtn.title = `삭제 불가 — 다음 파이프라인에서 사용 중: ${usedIn.map((n) => `'${n}'`).join(', ')}. 파이프라인에서 먼저 빼거나 다른 에이전트로 교체하세요.`;
delBtn.title = `숨길 수 없음 — 다음 작업 흐름에서 사용 중: ${usedIn.map((n) => `'${n}'`).join(', ')}. 작업 흐름에서 먼저 빼거나 다른 팀원으로 교체하세요.`;
} else {
delBtn.title = a.custom
? `'${a.name}' 에이전트 삭제 (모든 설정 함께 제거)`
: `'${a.name}' 에이전트 숨기기 (기본 에이전트라 복원 가능)`;
? `'${a.name}' 팀원 삭제 (모든 설정 함께 제거)`
: `'${a.name}' 팀원 숨기기 (기본 팀원이라 복원 가능)`;
delBtn.onclick = () => {
const confirmMsg = a.custom
? `'${a.name}' 에이전트를 삭제할까요? 이 에이전트의 모든 설정(모델·프롬프트·지식 비중)도 함께 삭제됩니다.`
: `기본 에이전트 '${a.name}'을(를) 목록에서 숨길까요? 나중에 [목록 맨 아래 → 삭제된 기본 에이전트] 영역에서 복원할 수 있습니다.`;
? `'${a.name}' 팀원을 삭제할까요? 이 팀원의 모든 설정(모델·응답 방식·지식 반영)도 함께 삭제됩니다.`
: `기본 팀원 '${a.name}'을(를) 목록에서 숨길까요? 나중에 [목록 맨 아래 → 숨긴 기본 팀원] 영역에서 복원할 수 있습니다.`;
if (!confirm(confirmMsg)) return;
vscode.postMessage({ type: 'deleteCompanyAgent', agentId: a.id });
};
@@ -2606,14 +2669,25 @@
row.appendChild(controls);
li.appendChild(row);
// ── Row 1.5: per-agent Knowledge Mix slider ──
const settings = document.createElement('div');
settings.className = 'company-agent-settings';
const settingsGrid = document.createElement('div');
settingsGrid.className = 'company-agent-settings-grid';
const roleWrap = document.createElement('label');
roleWrap.textContent = '역할 그룹';
roleWrap.appendChild(roleSelEl);
const modelWrap = document.createElement('label');
modelWrap.textContent = '모델';
modelWrap.appendChild(modelSelEl);
settingsGrid.appendChild(roleWrap);
settingsGrid.appendChild(modelWrap);
settings.appendChild(settingsGrid);
// CEO doesn't dispatch agents itself, it only synthesises,
// so the brain mix for CEO turns is governed by the
// *specialist* it dispatched — exposing the slider for CEO
// would just be a confusing dead control.
// so the brain mix for CEO turns is governed by specialists.
if (a.id !== 'ceo') {
li.appendChild(_buildAgentKnowledgeMixSlider(a, payload.globalKnowledgeMixWeight));
settings.appendChild(_buildAgentKnowledgeMixSlider(a, payload.globalKnowledgeMixWeight));
}
li.appendChild(settings);
// ── Row 2 (collapsed by default): prompt editor ──
li.appendChild(_buildAgentPromptEditor(a));
@@ -2628,7 +2702,7 @@
restoreLi.className = 'company-agent-hidden-section';
const head = document.createElement('div');
head.className = 'company-agent-hidden-head';
head.textContent = `삭제된 기본 에이전트 (${hidden.length}명)`;
head.textContent = `숨긴 기본 팀원 (${hidden.length}명)`;
restoreLi.appendChild(head);
const hint = document.createElement('div');
hint.className = 'company-agent-hidden-hint';
@@ -2664,18 +2738,13 @@
const usingOverride = a.knowledgeMixOverride !== null && a.knowledgeMixOverride !== undefined;
const effective = a.effectiveKnowledgeMixWeight;
// Tight label — "Mix" alone keeps the row narrow. The 🎚 emoji
// signals what it controls without needing the full word.
const label = document.createElement('span');
label.className = 'company-agent-mix-label';
label.textContent = '🎚 Mix';
label.textContent = '참고 지식 반영';
// Source badge ("GLOBAL" / "OVERRIDE") visually communicates
// *which knob* the value is coming from — clearer than packing
// it into the hint text.
const sourceBadge = document.createElement('span');
sourceBadge.className = 'company-agent-mix-source' + (usingOverride ? ' override' : '');
sourceBadge.textContent = usingOverride ? 'override' : 'global';
sourceBadge.textContent = usingOverride ? '직접 조정' : '전체 설정';
const slider = document.createElement('input');
slider.type = 'range';
@@ -2684,26 +2753,23 @@
slider.disabled = !usingOverride;
slider.className = 'company-agent-mix-slider';
// Compact hint: "Brain 55%" only — the model% is just 100 -
// brain%, so showing both was redundant noise. Stays narrow
// enough not to push the checkbox off the row.
const hint = document.createElement('span');
hint.className = 'company-agent-mix-hint';
const renderHint = () => {
const w = parseInt(slider.value, 10) || 50;
hint.textContent = `Brain ${w}%`;
hint.textContent = `${w}%`;
};
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.checked = !usingOverride;
cb.className = 'company-agent-mix-cb';
cb.title = '글로벌 슬라이더 값 사용';
cb.title = '전체 참고 지식 설정 사용';
cb.onchange = () => {
if (cb.checked) {
// Reset to global.
slider.disabled = true;
slider.value = String(globalWeight ?? 50);
sourceBadge.textContent = 'global';
sourceBadge.textContent = '전체 설정';
sourceBadge.classList.remove('override');
renderHint();
vscode.postMessage({
@@ -2713,7 +2779,7 @@
} else {
// Take ownership at the current displayed value.
slider.disabled = false;
sourceBadge.textContent = 'override';
sourceBadge.textContent = '직접 조정';
sourceBadge.classList.add('override');
const w = parseInt(slider.value, 10) || 50;
vscode.postMessage({
@@ -2735,7 +2801,7 @@
const cbWrap = document.createElement('label');
cbWrap.className = 'company-agent-mix-cbwrap';
cbWrap.appendChild(cb);
cbWrap.appendChild(document.createTextNode(' global'));
cbWrap.appendChild(document.createTextNode(' 전체 설정 사용'));
row.appendChild(label);
row.appendChild(sourceBadge);
row.appendChild(slider);
@@ -2759,8 +2825,8 @@
lbl.className = 'field-label';
lbl.innerHTML = `<span>${labelText}</span>` +
(overridden
? '<span class="field-flag">overridden</span>'
: '<span class="field-flag" style="color:var(--text-dim)">default</span>');
? '<span class="field-flag">조정됨</span>'
: '<span class="field-flag" style="color:var(--text-dim)">기본값</span>');
editor.appendChild(lbl);
const el = isTextarea
? document.createElement('textarea')
@@ -2776,22 +2842,22 @@
// 빌트인이든 커스텀이든 사용자가 자유롭게 리네이밍 가능. 변경 후
// CEO 보고서·planner enumeration·세션 로그 등 모든 표시 지점에
// 즉시 반영된다 — resolveAgent가 override를 머지하므로.
const nameInput = _field('name', '이름 (Display Name)', false, a.name, a.defaultName, a.nameOverridden);
const roleInput = _field('role', '역할 (Role Title)', false, a.role, a.defaultRole, a.roleOverridden);
const nameInput = _field('name', '이름', false, a.name, a.defaultName, a.nameOverridden);
const roleInput = _field('role', '역할 소개', false, a.role, a.defaultRole, a.roleOverridden);
// 이모지·색상은 한 줄에 나란히 — CSS는 grid 없이 inline flex로 처리.
const visualWrap = document.createElement('div');
visualWrap.style.cssText = 'display:flex; gap:8px;';
const emojiInput = _field('emoji', '이모지', false, a.emoji, a.defaultEmoji, a.emojiOverridden);
const colorInput = _field('color', '색상 (#hex)', false, a.color, a.defaultColor, a.colorOverridden);
const colorInput = _field('color', '테마 색상', false, a.color, a.defaultColor, a.colorOverridden);
// 위에서 만든 두 필드는 editor에 이미 append됨. 한 줄로 묶고 싶으면
// 부모에서 분리해 visualWrap에 다시 넣는다 — label은 직전 sibling.
// 더 단순하게: emoji/color 입력 후 reflow는 그냥 두고 max-width만 줄임.
emojiInput.style.maxWidth = '80px';
colorInput.style.maxWidth = '120px';
const tagInput = _field('tagline', 'Tagline (한 줄)', false, a.tagline, a.defaultTagline, a.taglineOverridden);
const specInput = _field('specialty', 'Specialty (CEO가 dispatch 판단에 사용)', true, a.specialty, a.defaultSpecialty, a.specialtyOverridden);
const persInput = _field('persona', 'Persona (말투·관점·강조)', true, a.persona, a.defaultPersona, a.personaOverridden);
const tagInput = _field('tagline', '한 줄 소개', false, a.tagline, a.defaultTagline, a.taglineOverridden);
const specInput = _field('specialty', '잘하는 일', true, a.specialty, a.defaultSpecialty, a.specialtyOverridden);
const persInput = _field('persona', '응답 스타일', true, a.persona, a.defaultPersona, a.personaOverridden);
specInput.rows = 3;
persInput.rows = 5;
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "astra",
"displayName": "Astra",
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
"version": "2.1.9",
"version": "2.2.0",
"publisher": "g1nation",
"license": "MIT",
"icon": "assets/icon.png",