diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index ef4ea22..096dd9a 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1778765910142, + "createdAt": 1778767038020, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 90da284..051ef8f 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1778765910134, + "createdAt": 1778767038017, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index 8fd6032..c7d8b47 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1778765910130, + "createdAt": 1778767038016, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index 9139de2..38628f8 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -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" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778765910115.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778767038004.json similarity index 81% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778765910115.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778767038004.json index 9b605d1..98a4438 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778765910115.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778767038004.json @@ -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": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 6b04df1..2aefe89 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -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단계 체크리스트(두뇌 연결, 모델 선택, 첫 질문)를 추가하여 초기 설정 과정을 직관적으로 개선했습니다. diff --git a/astra-2.1.9.vsix b/astra-2.2.0.vsix similarity index 92% rename from astra-2.1.9.vsix rename to astra-2.2.0.vsix index b68745b..d8b35c7 100644 Binary files a/astra-2.1.9.vsix and b/astra-2.2.0.vsix differ diff --git a/docs/records/ConnectAI/chronicle.config.json b/docs/records/ConnectAI/chronicle.config.json index ad7f1c8..dc58e53 100644 --- a/docs/records/ConnectAI/chronicle.config.json +++ b/docs/records/ConnectAI/chronicle.config.json @@ -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" } diff --git a/media/sidebar.css b/media/sidebar.css index ac95795..32ce44a 100644 --- a/media/sidebar.css +++ b/media/sidebar.css @@ -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; } diff --git a/media/sidebar.html b/media/sidebar.html index 9724f75..162a948 100644 --- a/media/sidebar.html +++ b/media/sidebar.html @@ -162,11 +162,11 @@
-
활성 에이전트 + 모델
-
CEO는 항상 활성. 각 에이전트별로 모델을 따로 지정할 수 있습니다 — 다른 모델을 쓸 때만 LM Studio가 swap합니다.
+
AI 팀 구성
+
이번 작업에 참여할 팀원을 고릅니다. 대표는 항상 참여하고, 모델·지식 반영은 필요할 때만 조정하세요.
- +
@@ -177,9 +177,9 @@ (no separate modal) so the user can see existing agents while editing — easier to spot id collisions. -->
-
새 에이전트 추가
+
새 팀원 추가
-
- +
@@ -220,33 +220,33 @@
-
워크 파이프라인
-
CEO 자유 분배 대신 사용자가 정한 stage 순서대로 dispatch합니다. loop-back 정규식이 매칭되면 이전 stage로 되돌아갑니다 (최대 maxIterations 회).
+
작업 흐름
+
대표에게 맡기거나, 사용자가 정한 순서대로 팀원이 이어서 작업하게 만듭니다.
- - +
- +
    -
    -
    파이프라인 편집
    +
    작업 흐름 편집
    -
    -
    단계 (Stages)
    +
    작업 단계
      - +
      diff --git a/media/sidebar.js b/media/sidebar.js index a0b359b..c90e607 100644 --- a/media/sidebar.js +++ b/media/sidebar.js @@ -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 = ''; + tplSel.innerHTML = ''; 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 = ''; + _activePipelineSel.innerHTML = ''; 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 = `${escAttr(p.name || p.id)} ${escAttr(p.id)} · ${(p.stages || []).length} stages${p.id === activeId ? ' · ● 활성' : ''}`; + title.className = 'pipeline-summary-title'; + title.innerHTML = `${escAttr(p.name || p.id)}${(p.stages || []).length}단계${p.id === activeId ? ' · 현재 사용 중' : ''}`; 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 = `${escAttr(title)}${escAttr(hint)}`; + _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)} ${escAttr(a.role)}`; + name.innerHTML = `${escAttr(a.name)}${escAttr(a.role)}`; 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 = `${labelText}` + (overridden - ? 'overridden' - : 'default'); + ? '조정됨' + : '기본값'); 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; diff --git a/package.json b/package.json index a94d88c..fcb26e7 100644 --- a/package.json +++ b/package.json @@ -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",