release: v2.0.6 - Intelligence & UX Optimization (2026-05-14)
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||
"createdAt": 1778684049645,
|
||||
"createdAt": 1778685193682,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
"createdAt": 1778684049641,
|
||||
"createdAt": 1778685193681,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||
"createdAt": 1778684049636,
|
||||
"createdAt": 1778685193680,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "---\nid: stress_conflict_1778684049621\ndate: 2026-05-13T14:54:09.649Z\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]** 핵심 정보 수집 및 분석 중... (5ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)\n",
|
||||
"createdAt": 1778684049649,
|
||||
"result": "---\nid: stress_conflict_1778685193665\ndate: 2026-05-13T15:13:13.682Z\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]** 전략 수립 중... (10ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (5ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (0ms)\n",
|
||||
"createdAt": 1778685193682,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+10
-10
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"missionId": "stress_conflict_1778684049621",
|
||||
"missionId": "stress_conflict_1778685193665",
|
||||
"status": "completed",
|
||||
"startTime": "2026-05-13T14:54:09.621Z",
|
||||
"totalElapsedMs": 29,
|
||||
"startTime": "2026-05-13T15:13:13.666Z",
|
||||
"totalElapsedMs": 16,
|
||||
"results": {
|
||||
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
@@ -16,30 +16,30 @@
|
||||
{
|
||||
"from": "idle",
|
||||
"to": "planner",
|
||||
"durationMs": 11,
|
||||
"durationMs": 10,
|
||||
"message": "전략 수립 중...",
|
||||
"ts": "2026-05-13T14:54:09.632Z"
|
||||
"ts": "2026-05-13T15:13:13.676Z"
|
||||
},
|
||||
{
|
||||
"from": "planner",
|
||||
"to": "researcher",
|
||||
"durationMs": 5,
|
||||
"message": "핵심 정보 수집 및 분석 중...",
|
||||
"ts": "2026-05-13T14:54:09.637Z"
|
||||
"ts": "2026-05-13T15:13:13.681Z"
|
||||
},
|
||||
{
|
||||
"from": "researcher",
|
||||
"to": "writer",
|
||||
"durationMs": 8,
|
||||
"durationMs": 0,
|
||||
"message": "최종 리포트 작성 및 편집 중...",
|
||||
"ts": "2026-05-13T14:54:09.645Z"
|
||||
"ts": "2026-05-13T15:13:13.681Z"
|
||||
},
|
||||
{
|
||||
"from": "writer",
|
||||
"to": "completed",
|
||||
"durationMs": 5,
|
||||
"durationMs": 1,
|
||||
"message": "미션 완료",
|
||||
"ts": "2026-05-13T14:54:09.650Z"
|
||||
"ts": "2026-05-13T15:13:13.682Z"
|
||||
}
|
||||
],
|
||||
"resilienceMetrics": {
|
||||
@@ -1,5 +1,15 @@
|
||||
# Astra Patch Notes
|
||||
|
||||
## v2.0.6 (2026-05-14)
|
||||
### 🚀 Intelligence & UX Optimization
|
||||
- **UI & UX 정교화:** 사이드바(`sidebar.js`, `sidebar.css`)의 인터랙션과 스타일을 개선하여 더욱 매끄러운 사용자 경험을 제공합니다.
|
||||
- **텔레그램 대화 기록 관리:** `conversationHistory.ts`를 도입하여 텔레그램 연동 시의 대화 맥락 유지 기능을 강화했습니다.
|
||||
- **비즈니스 엔진 고도화:** `dispatcher.ts` 및 `promptBuilder.ts` 최적화를 통해 CEO 에이전트의 의사결정 및 작업 할당 정밀도를 높였습니다.
|
||||
- **아키텍처 분석 강화:** 프로젝트 구조 스캔 및 컨텍스트 주입 로직을 보완하여 더 깊은 코드 이해가 가능하도록 개선했습니다.
|
||||
- **신규 패키징:** `astra-2.0.6.vsix` 패키지를 통해 통합된 성능 및 인터페이스 개선 사항을 배포합니다.
|
||||
|
||||
---
|
||||
|
||||
## v2.0.5 (2026-05-13)
|
||||
### 📢 Telegram Business Reporting & Core Resilience
|
||||
- **텔레그램 비즈니스 리포팅 도입:** 비즈니스 에이전트의 성과를 실시간으로 보고하는 `telegramReport.ts`를 추가하여 원격 모니터링 기능을 강화했습니다.
|
||||
|
||||
+31
-2
@@ -469,7 +469,7 @@
|
||||
.company-phase-card .cph-meta { color: var(--text-dim); font-size: 10px; }
|
||||
.company-phase-card.report .cph-head { color: var(--accent); }
|
||||
|
||||
/* Project Architecture chip — sits just above the input when project mode is on. */
|
||||
/* Project Architecture chip — three-state surface above the input. */
|
||||
.arch-chip {
|
||||
display: none;
|
||||
align-items: center;
|
||||
@@ -481,7 +481,20 @@
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.arch-chip[data-active="true"] { display: flex; }
|
||||
.arch-chip[data-state="active"],
|
||||
.arch-chip[data-state="inactive"] { display: flex; }
|
||||
/* Inactive state has a muted look so it doesn't compete with active chips. */
|
||||
.arch-chip[data-state="inactive"] {
|
||||
background: var(--bg-secondary);
|
||||
border-style: dashed;
|
||||
}
|
||||
.arch-chip[data-state="inactive"] .arch-chip-title { color: var(--text-dim); }
|
||||
/* Per-state button visibility — JS only flips the chip's data-state. */
|
||||
.arch-chip[data-state="active"] .arch-chip-inactive-only { display: none; }
|
||||
.arch-chip[data-state="inactive"] .arch-chip-active-only { display: none; }
|
||||
.arch-chip[data-state="inactive"] #archAttachBtn {
|
||||
color: var(--accent); border-color: var(--accent);
|
||||
}
|
||||
.arch-chip-icon { font-size: 14px; flex-shrink: 0; }
|
||||
.arch-chip-info { flex: 1; min-width: 0; line-height: 1.3; }
|
||||
.arch-chip-title {
|
||||
@@ -505,6 +518,22 @@
|
||||
border-color: var(--border-bright);
|
||||
}
|
||||
|
||||
/* Inline refresh-result card so the user sees what the refresh did. */
|
||||
.arch-refresh-card {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
margin: 4px 0;
|
||||
font-size: 10.5px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.arch-refresh-card .arc-head {
|
||||
color: var(--text-bright); font-weight: 600; margin-bottom: 2px;
|
||||
}
|
||||
.arch-refresh-card .arc-meta { color: var(--text-dim); font-size: 9.5px; }
|
||||
.arch-refresh-card.no-changes { border-style: dashed; }
|
||||
|
||||
/* Inline model picker that lives in the input footer, next to the attach
|
||||
button. Replaces the (now-removed) bottom model row + the separate
|
||||
"Model: ..." status text — one surface, click to change. */
|
||||
|
||||
+18
-6
@@ -292,16 +292,28 @@
|
||||
message with active=true. Click "Open" / "Refresh" / "Detach" to
|
||||
route back to the chatHandlers cases.
|
||||
-->
|
||||
<div id="archChip" class="arch-chip" data-active="false">
|
||||
<!--
|
||||
Three-state chip:
|
||||
data-state="hidden" → completely collapsed
|
||||
data-state="active" → full info + Open / Refresh / Detach
|
||||
data-state="inactive" → project name + Attach (or Re-attach) only
|
||||
JS switches the state attribute on every `architectureStatus`
|
||||
event so the user always has a one-click path back into project
|
||||
mode after a Detach.
|
||||
-->
|
||||
<div id="archChip" class="arch-chip" data-state="hidden">
|
||||
<span class="arch-chip-icon">📋</span>
|
||||
<div class="arch-chip-info">
|
||||
<div class="arch-chip-title" id="archChipTitle">—</div>
|
||||
<div class="arch-chip-meta" id="archChipMeta">Auto-load Off</div>
|
||||
<div class="arch-chip-meta" id="archChipMeta">—</div>
|
||||
</div>
|
||||
<div class="arch-chip-actions">
|
||||
<button class="arch-chip-btn" id="archOpenBtn" title="Architecture 문서 열기">Open</button>
|
||||
<button class="arch-chip-btn" id="archRefreshBtn" title="지금 다시 스캔">Refresh</button>
|
||||
<button class="arch-chip-btn" id="archDetachBtn" title="자동 첨부 끄기">Detach</button>
|
||||
<div class="arch-chip-actions" id="archChipActions">
|
||||
<!-- active state buttons -->
|
||||
<button class="arch-chip-btn arch-chip-active-only" id="archOpenBtn" title="Architecture 문서 열기">Open</button>
|
||||
<button class="arch-chip-btn arch-chip-active-only" id="archRefreshBtn" title="지금 다시 스캔">Refresh</button>
|
||||
<button class="arch-chip-btn arch-chip-active-only" id="archDetachBtn" title="자동 첨부 끄기">Detach</button>
|
||||
<!-- inactive state button -->
|
||||
<button class="arch-chip-btn arch-chip-inactive-only" id="archAttachBtn" title="이 프로젝트에 architecture 자동 첨부 켜기">Attach</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="agentConfigPanel" class="panel">
|
||||
|
||||
+48
-9
@@ -804,21 +804,28 @@
|
||||
break;
|
||||
}
|
||||
case 'architectureStatus': {
|
||||
// Show / hide the chip + reflect current state.
|
||||
// Three-state chip:
|
||||
// active — full info + Open/Refresh/Detach
|
||||
// inactive — name + [Attach] button (user previously detached, OR doc not yet generated)
|
||||
// hidden — no project + no workspace
|
||||
const chip = document.getElementById('archChip');
|
||||
const title = document.getElementById('archChipTitle');
|
||||
const meta = document.getElementById('archChipMeta');
|
||||
if (!chip || !title || !meta) break;
|
||||
const v = msg.value || {};
|
||||
if (!v.active) {
|
||||
chip.setAttribute('data-active', 'false');
|
||||
break;
|
||||
if (v.active) {
|
||||
chip.setAttribute('data-state', 'active');
|
||||
title.textContent = `${v.projectName || 'Project'} architecture`;
|
||||
const updatedLabel = v.lastUpdated ? `updated ${formatRelativeTime(v.lastUpdated)}` : 'just attached';
|
||||
const autoLabel = v.autoUpdate === false ? 'Auto-update Off' : 'Auto-update On';
|
||||
meta.textContent = `${updatedLabel} · ${autoLabel}`;
|
||||
} else if (v.canAttach && v.projectName) {
|
||||
chip.setAttribute('data-state', 'inactive');
|
||||
title.textContent = `${v.projectName} architecture`;
|
||||
meta.textContent = v.detached ? 'detached — click Attach to re-enable' : 'not yet activated';
|
||||
} else {
|
||||
chip.setAttribute('data-state', 'hidden');
|
||||
}
|
||||
chip.setAttribute('data-active', 'true');
|
||||
title.textContent = `${v.projectName || 'Project'} architecture`;
|
||||
const updatedLabel = v.lastUpdated ? `updated ${formatRelativeTime(v.lastUpdated)}` : 'just attached';
|
||||
const autoLabel = v.autoUpdate === false ? 'Auto-update Off' : 'Auto-update On';
|
||||
meta.textContent = `${updatedLabel} · ${autoLabel}`;
|
||||
break;
|
||||
}
|
||||
case 'architectureRefreshFailed': {
|
||||
@@ -830,6 +837,34 @@
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'architectureRefreshResult': {
|
||||
// Trust-building stats card: shows exactly what the
|
||||
// refresh did so users don't have to guess whether the
|
||||
// 0.1s click actually accomplished anything.
|
||||
const v = msg.value || {};
|
||||
const card = document.createElement('div');
|
||||
card.className = 'arch-refresh-card';
|
||||
const noChanges = (v.newlyAnalyzed | 0) === 0 && (v.deleted | 0) === 0;
|
||||
if (noChanges) card.classList.add('no-changes');
|
||||
const head = noChanges
|
||||
? `📋 ${escAttr(v.projectName || 'Project')} architecture — 변경 사항 없음`
|
||||
: `📋 ${escAttr(v.projectName || 'Project')} architecture refreshed`;
|
||||
const parts = [
|
||||
`${v.newlyAnalyzed | 0} newly analysed`,
|
||||
`${v.cached | 0} cached`,
|
||||
];
|
||||
if ((v.deleted | 0) > 0) parts.push(`${v.deleted | 0} deleted`);
|
||||
parts.push(`${v.durationMs | 0}ms`);
|
||||
card.innerHTML =
|
||||
`<div class="arc-head">${head}</div>` +
|
||||
`<div class="arc-meta">${parts.join(' · ')}</div>`;
|
||||
const chatEl = document.getElementById('chat');
|
||||
if (chatEl) {
|
||||
chatEl.appendChild(card);
|
||||
chatEl.scrollTop = chatEl.scrollHeight;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'knowledgeMix': {
|
||||
// Initial sync: reflect whatever weight is currently in settings.
|
||||
if (msg.value && typeof msg.value.weight === 'number') {
|
||||
@@ -1386,6 +1421,10 @@
|
||||
if (_archOpenBtn) _archOpenBtn.onclick = () => vscode.postMessage({ type: 'openArchitectureDoc' });
|
||||
if (_archRefreshBtn) _archRefreshBtn.onclick = () => vscode.postMessage({ type: 'refreshArchitecture' });
|
||||
if (_archDetachBtn) _archDetachBtn.onclick = () => vscode.postMessage({ type: 'detachArchitecture' });
|
||||
// [Attach] is visible only in the inactive chip state; clicking it
|
||||
// re-enables architecture mode for the current workspace's project.
|
||||
const _archAttachBtn = document.getElementById('archAttachBtn');
|
||||
if (_archAttachBtn) _archAttachBtn.onclick = () => vscode.postMessage({ type: 'attachArchitecture' });
|
||||
|
||||
// ── 1인 기업 (Company) Mode chip + manage overlay ─────────────────────
|
||||
// The chip itself toggles enabled/disabled. The ▾ button opens the
|
||||
|
||||
+5
-1
@@ -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.0.5",
|
||||
"version": "2.0.6",
|
||||
"publisher": "g1nation",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
@@ -111,6 +111,10 @@
|
||||
"command": "g1nation.architecture.detach",
|
||||
"title": "Astra: Detach Project Architecture Context"
|
||||
},
|
||||
{
|
||||
"command": "g1nation.architecture.attach",
|
||||
"title": "Astra: Attach Project Architecture Context"
|
||||
},
|
||||
{
|
||||
"command": "g1nation.architecture.open",
|
||||
"title": "Astra: Open Project Architecture Doc"
|
||||
|
||||
+27
-3
@@ -306,9 +306,24 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
const systemPrompt = buildTelegramSystemPrompt(!!contextBlock);
|
||||
const userMessage = contextBlock
|
||||
? `[SECOND BRAIN CONTEXT]\n${contextBlock}\n\n[USER MESSAGE]\n${text}`
|
||||
: text;
|
||||
// Per-chat conversation history — without this every inbound
|
||||
// is a fresh turn, so the user "tells the bot something" and
|
||||
// it gets immediately forgotten. We inline the recent N
|
||||
// exchanges into the user message because the AI service's
|
||||
// {system, user} surface doesn't carry a messages array.
|
||||
const { appendTelegramMessage, getRecentMessages, formatHistoryForPrompt } =
|
||||
await import('./integrations/telegram/conversationHistory');
|
||||
const history = getRecentMessages(chatId, 10);
|
||||
const historyBlock = formatHistoryForPrompt(history);
|
||||
const pieces: string[] = [];
|
||||
if (contextBlock) pieces.push(`[SECOND BRAIN CONTEXT]\n${contextBlock}`);
|
||||
if (historyBlock) pieces.push(historyBlock);
|
||||
pieces.push(`[USER MESSAGE]\n${text}`);
|
||||
const userMessage = pieces.join('\n\n');
|
||||
|
||||
// Persist the user's message *before* the AI call so failures
|
||||
// still leave a trail (next inbound will see what they said).
|
||||
appendTelegramMessage({ chatId, role: 'user', text, kind: 'user' });
|
||||
|
||||
try {
|
||||
const result = await telegramAi.chat({ system: systemPrompt, user: userMessage });
|
||||
@@ -330,6 +345,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
// Persist the assistant's reply so the *next* inbound sees
|
||||
// what we just said. Without this, the bot would forget its
|
||||
// own answer the moment the user follows up.
|
||||
appendTelegramMessage({ chatId, role: 'assistant', text: result.content, kind: 'reply' });
|
||||
// Telegram has a hard 4096 char/message limit. Long replies are
|
||||
// chunked and joined with a "(이어서)" hint so the user knows
|
||||
// multiple messages belong together.
|
||||
@@ -446,6 +465,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
await provider._detachArchitecture();
|
||||
vscode.window.showInformationMessage('Astra: Project architecture auto-attach turned off.');
|
||||
}),
|
||||
vscode.commands.registerCommand('g1nation.architecture.attach', async () => {
|
||||
if (!provider) return;
|
||||
await provider._attachArchitecture();
|
||||
vscode.window.showInformationMessage('Astra: Project architecture auto-attach turned on.');
|
||||
}),
|
||||
vscode.commands.registerCommand('g1nation.architecture.open', async () => {
|
||||
if (!provider) return;
|
||||
await provider._openArchitectureDoc();
|
||||
|
||||
@@ -284,7 +284,8 @@ async function _dispatchOne(
|
||||
// hit disk / shell. The report (e.g. "✅ Created: foo.py") is
|
||||
// appended to the response so the user sees what really happened.
|
||||
let finalResponse = rawResponse || '_(empty response)_';
|
||||
if (rawResponse && deps.executeActionTags && _hasActionTag(rawResponse)) {
|
||||
const hasTag = !!rawResponse && _hasActionTag(rawResponse);
|
||||
if (rawResponse && deps.executeActionTags && hasTag) {
|
||||
try {
|
||||
const report = await deps.executeActionTags(rawResponse);
|
||||
if (report.length > 0) {
|
||||
@@ -297,12 +298,30 @@ async function _dispatchOne(
|
||||
logError('company.dispatcher: action-tag execution failed.', { agentId, err });
|
||||
finalResponse = `${rawResponse}\n\n---\n⚠️ Action 실행 실패: ${err}`;
|
||||
}
|
||||
} else if (rawResponse && !hasTag && _claimsFileCreation(rawResponse)) {
|
||||
// Hallucination guard: small models love to *narrate* file
|
||||
// creation ("foo.py를 생성했습니다 …") without emitting the
|
||||
// <create_file> tag — so the user sees ✅ in chat but nothing
|
||||
// on disk. Catch the mismatch here and flag it loudly so the
|
||||
// CEO synthesis (which reads this response) and the user both
|
||||
// know nothing was actually written.
|
||||
const warning = '⚠️ **실제 파일이 생성되지 않았습니다.** Agent가 파일 생성을 텍스트로 설명했지만 ConnectAI 액션 태그(`<create_file>` 등)를 사용하지 않아 디스크에 아무것도 만들어지지 않았어요. 같은 요청을 다시 시도하거나, 사용자가 직접 만드세요.';
|
||||
finalResponse = `${rawResponse}\n\n---\n${warning}`;
|
||||
logInfo('company.dispatcher: agent claimed creation without action tag.', { agentId });
|
||||
}
|
||||
// `error: 'no-action-tag-but-claimed'` is *advisory* — we still let
|
||||
// the turn complete because some agents (Writer, Researcher) are
|
||||
// legitimately answer-only. But by flagging the agent output we
|
||||
// mark it as not-fully-successful so the CEO synthesis can read
|
||||
// the warning verbatim.
|
||||
const claimedButDidnt = rawResponse && !hasTag && _claimsFileCreation(rawResponse);
|
||||
return {
|
||||
agentId, task,
|
||||
response: finalResponse,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error: rawResponse ? undefined : 'empty-response',
|
||||
error: rawResponse
|
||||
? (claimedButDidnt ? 'claimed-creation-no-tag' : undefined)
|
||||
: 'empty-response',
|
||||
};
|
||||
} catch (e: any) {
|
||||
const err = e?.message ?? String(e);
|
||||
@@ -325,3 +344,27 @@ async function _dispatchOne(
|
||||
function _hasActionTag(text: string): boolean {
|
||||
return /<\s*(?:create_file|edit_file|delete_file|read_file|list_files|list_brain|run_command|read_brain|reveal_in_explorer|open_file|glob|grep)\b/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic: does the response *narrate* having created files/folders?
|
||||
*
|
||||
* We look for the combination of (a) a Korean / English creation verb and
|
||||
* (b) a filename-like or "folder" mention. The intent is to catch the
|
||||
* hallucination pattern where an agent writes "foo.py 파일을 생성했습니다"
|
||||
* or "Created `bar/` directory" without emitting the corresponding
|
||||
* `<create_file>` tag, so the dispatcher can flag it back to the CEO and
|
||||
* the user instead of silently reporting success.
|
||||
*
|
||||
* Kept narrow on purpose — a *plan* like "다음에는 X를 만들어야 합니다"
|
||||
* shouldn't trigger this. We require past-tense / completion phrasing.
|
||||
*/
|
||||
function _claimsFileCreation(text: string): boolean {
|
||||
// Past-tense creation verbs (Korean + English).
|
||||
const claimRe = /(?:생성했|만들었|작성했|저장했|구현했|created|wrote|saved|built|generated)/i;
|
||||
if (!claimRe.test(text)) return false;
|
||||
// Combined with either an explicit filename (something.ext) or the word
|
||||
// "폴더" / "directory" / "folder" near the verb.
|
||||
const fileLike = /\b[\w\-./]+\.(?:py|js|ts|tsx|jsx|md|json|html|css|sh|yaml|yml|sql|java|go|rs|c|cpp|rb|php)\b/i.test(text);
|
||||
const folderLike = /(?:폴더|디렉토리|directory|folder)/i.test(text);
|
||||
return fileLike || folderLike;
|
||||
}
|
||||
|
||||
@@ -77,15 +77,39 @@ export function buildSpecialistPrompt(inputs: SpecialistPromptInputs): string {
|
||||
parts.push('- 추측·일반론·placeholder 금지. 가진 정보만 인용.');
|
||||
|
||||
// ── Tool contract ──
|
||||
// ConnectAI's existing AgentExecutor parses these tags automatically
|
||||
// after the streaming response completes. Keeping the syntax identical
|
||||
// means specialists can write files / run commands the same way the
|
||||
// base chat already does — no new plumbing on the agent side.
|
||||
// Hard rule about action tags. Earlier wording ("태그 없이 평문으로만
|
||||
// 답해도 됩니다") let small models (gemma 4 e2b etc.) emit ```python
|
||||
// code blocks and then *claim* to have created files — the user got
|
||||
// ✅ in chat but nothing on disk. This block is now phrased so the
|
||||
// model cannot rationalise its way out of the tag contract.
|
||||
parts.push('');
|
||||
parts.push('## 도구 사용 규칙 (필요할 때만)');
|
||||
parts.push('실제 파일 생성·명령 실행이 필요하면 ConnectAI의 액션 태그를 사용하세요.');
|
||||
parts.push('예) `<create_file path="...">내용</create_file>`, `<run_command>npm test</run_command>` 등.');
|
||||
parts.push('태그 없이 평문으로만 답해도 됩니다 — 기획·분석·아이디어 작업은 보통 태그가 필요 없습니다.');
|
||||
parts.push('## ⚠️ 실제 파일·명령 실행 (이 섹션 매우 중요)');
|
||||
parts.push('당신은 사용자의 **실제** 파일 시스템과 터미널에 직접 연결되어 있습니다.');
|
||||
parts.push('**텍스트로 "만들었다 / 작성했다 / 생성했다 / 저장했다" 라고 말해도 사용자 디스크엔 아무 일도 안 일어납니다.**');
|
||||
parts.push('파일을 만들거나 명령을 실행하려면 **반드시** 아래 액션 태그로 감싸세요. 시스템이 자동으로 디스크에 적용합니다:');
|
||||
parts.push('');
|
||||
parts.push(' • `<create_file path="...">내용</create_file>` — 새 파일 생성·덮어쓰기');
|
||||
parts.push(' • `<edit_file path="..."><find>옛 내용</find><replace>새 내용</replace></edit_file>` — 부분 편집');
|
||||
parts.push(' • `<read_file path="..."/>` — 32KB까지 읽기 (편집 전엔 반드시 먼저 read)');
|
||||
parts.push(' • `<delete_file path="..."/>` — 파일·디렉토리 삭제');
|
||||
parts.push(' • `<list_files path="..."/>` — 디렉토리 목록 보기');
|
||||
parts.push(' • `<run_command>명령</run_command>` — 셸 실행 (디렉토리 생성 등)');
|
||||
parts.push('');
|
||||
parts.push('🛑 **경로 규칙 (위반 시 권한 거부됨)**:');
|
||||
parts.push('- 경로는 **워크스페이스 루트 상대 경로**로 쓰세요. 예: `timertest/timer.py`, `src/utils.py`');
|
||||
parts.push('- 절대 경로 가능하지만 **반드시 워크스페이스 내부**여야 함. `/antigravity/...` `/tmp/...` 같은 시스템 루트 경로는 거부됨.');
|
||||
parts.push('- 디렉토리는 `<create_file>`이 자동으로 만들어줍니다 (mkdir -p). 별도 명령 불필요.');
|
||||
parts.push('');
|
||||
parts.push('❌ **하지 말아야 할 패턴**:');
|
||||
parts.push(' - ```python\\nprint("...")\\n``` 코드 블록만 답하고 "생성 완료"라고 말하기 → 디스크엔 만들어지지 않음');
|
||||
parts.push(' - `<create_file path="/antigravity/foo.py">` 같은 시스템 루트 경로 → 거부됨');
|
||||
parts.push(' - 사용자가 "X 만들어줘"라고 했는데 코드만 보여주고 끝내기 → 사용자는 결과물을 받지 못함');
|
||||
parts.push('');
|
||||
parts.push('✅ **올바른 패턴**:');
|
||||
parts.push(' 사용자: "타이머 파이썬 스크립트 만들어줘"');
|
||||
parts.push(' 당신: 짧은 설명 한두 줄 + `<create_file path="timer.py">import time\\n...</create_file>` + 자가평가');
|
||||
parts.push('');
|
||||
parts.push('기획·분석·아이디어처럼 *결과물이 파일 아닌 경우*에는 액션 태그 없이 마크다운으로만 답해도 됩니다.');
|
||||
|
||||
// ── Peer context (this turn) ──
|
||||
const peers = inputs.peerOutputs ?? [];
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { logError, logInfo } from '../../utils';
|
||||
import { TelegramHttpClient } from '../../integrations/telegram/telegramClient';
|
||||
import { appendTelegramMessage } from '../../integrations/telegram/conversationHistory';
|
||||
import { COMPANY_AGENTS } from './agents';
|
||||
import { AgentTurnOutput, CompanyState, CompanyTaskPlan } from './types';
|
||||
|
||||
@@ -72,6 +73,13 @@ export async function buildTelegramReporter(
|
||||
|
||||
return async (text: string): Promise<boolean> => {
|
||||
if (!text || !text.trim()) return false;
|
||||
// Append to the per-chat history *before* the send. The bot's
|
||||
// next inbound turn reads this history, so even if delivery
|
||||
// fails the user's follow-up question still has context for
|
||||
// "what did you just say to me?". Persisting on attempt also
|
||||
// means timing matches the user's perception ("the bot reported
|
||||
// X, then I replied").
|
||||
appendTelegramMessage({ chatId, role: 'assistant', text, kind: 'company-mirror' });
|
||||
try {
|
||||
await client.sendMessage({ chatId, text, parseMode: 'Markdown' });
|
||||
return true;
|
||||
|
||||
@@ -115,6 +115,14 @@ export interface BuildResult {
|
||||
created: boolean;
|
||||
/** Result of the scan that fed this build. */
|
||||
scan: ArchitectureScanResult;
|
||||
/**
|
||||
* What the underlying deep-scan actually did this run — how many files
|
||||
* were freshly analysed vs. served from the on-disk cache, and whether
|
||||
* any tracked files have disappeared. The sidebar surfaces these counts
|
||||
* after every Refresh so users can trust the operation actually ran
|
||||
* (instead of the previous mysterious "updated just now in 0.1s").
|
||||
*/
|
||||
refreshStats: RefreshStats;
|
||||
}
|
||||
|
||||
/** Resolve the architecture doc path for a given project root. */
|
||||
@@ -181,7 +189,7 @@ export function buildOrRefreshArchitectureDoc(
|
||||
newlyAnalyzed: deep.refreshStats.newlyAnalyzed,
|
||||
cached: deep.refreshStats.cached,
|
||||
});
|
||||
return { docPath, created: true, scan };
|
||||
return { docPath, created: true, scan, refreshStats: deep.refreshStats };
|
||||
}
|
||||
|
||||
// In-place refresh: rewrite the auto-managed block, keep user-owned sections.
|
||||
@@ -196,7 +204,7 @@ export function buildOrRefreshArchitectureDoc(
|
||||
deleted: deep.refreshStats.deleted.length,
|
||||
});
|
||||
}
|
||||
return { docPath, created: false, scan };
|
||||
return { docPath, created: false, scan, refreshStats: deep.refreshStats };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Per-chat conversation history for the Telegram bot.
|
||||
*
|
||||
* Why this exists: the previous bot was *stateless* — every inbound
|
||||
* message hit `AIService.chat({system, user})` in isolation, with no
|
||||
* memory of what the user said two minutes ago or what the secretary
|
||||
* just reported. Users hit this immediately: "I just told you about
|
||||
* project X, why don't you remember?"
|
||||
*
|
||||
* This module solves it with a thin append-only log:
|
||||
*
|
||||
* - Each `(chatId, role, text)` triple is appended to a JSONL file at
|
||||
* `<workspace>/.astra/company/_agents/secretary/telegram_history.jsonl`.
|
||||
* One file per workspace, scoped by `chatId` at read time, so
|
||||
* multi-chat setups don't bleed into each other.
|
||||
* - A small in-memory cache (last 200 entries) sits in front so the
|
||||
* hot path is `O(1)` lookup, not "scan the whole file".
|
||||
* - `getRecentMessages` returns the most recent N entries for a chat,
|
||||
* including the secretary's company-turn mirror entries that the
|
||||
* dispatcher appends — so when the user asks a follow-up question
|
||||
* ("저 폴더가 없다고"), the AI sees its own report from 30 seconds ago.
|
||||
*
|
||||
* The file is human-readable on purpose — users can grep it / delete it
|
||||
* to clear history without any CLI.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { logError } from '../../utils';
|
||||
|
||||
export interface TelegramHistoryEntry {
|
||||
chatId: number;
|
||||
role: 'user' | 'assistant';
|
||||
text: string;
|
||||
/** ISO timestamp at append time. */
|
||||
ts: string;
|
||||
/** Optional tag: `'company-mirror'` for secretary's auto-reports, `'reply'` for normal replies. */
|
||||
kind?: 'company-mirror' | 'reply' | 'user';
|
||||
}
|
||||
|
||||
/** Max entries kept across all chats in the in-memory cache. */
|
||||
const MEMORY_CAP = 200;
|
||||
/** Max entries returned by `getRecentMessages`. */
|
||||
const DEFAULT_RECENT = 12;
|
||||
|
||||
let _cache: TelegramHistoryEntry[] = [];
|
||||
let _hydratedForWorkspace: string | null = null;
|
||||
|
||||
function _historyPath(): string {
|
||||
const ws = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!ws) return '';
|
||||
return path.join(ws, '.astra', 'company', '_agents', 'secretary', 'telegram_history.jsonl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy-load the on-disk JSONL into the cache the first time we need it
|
||||
* for the current workspace. Switching workspaces re-hydrates from the
|
||||
* new file — that way each project keeps its own chat memory.
|
||||
*/
|
||||
function _hydrateIfNeeded(): void {
|
||||
const ws = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';
|
||||
if (_hydratedForWorkspace === ws) return;
|
||||
_hydratedForWorkspace = ws;
|
||||
_cache = [];
|
||||
const p = _historyPath();
|
||||
if (!p || !fs.existsSync(p)) return;
|
||||
try {
|
||||
const lines = fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.trim());
|
||||
// Trim to the cap from the *end* so we keep the freshest entries.
|
||||
const tail = lines.length > MEMORY_CAP ? lines.slice(-MEMORY_CAP) : lines;
|
||||
for (const line of tail) {
|
||||
try {
|
||||
const entry = JSON.parse(line) as Partial<TelegramHistoryEntry>;
|
||||
if (typeof entry.chatId === 'number'
|
||||
&& (entry.role === 'user' || entry.role === 'assistant')
|
||||
&& typeof entry.text === 'string'
|
||||
&& typeof entry.ts === 'string') {
|
||||
_cache.push(entry as TelegramHistoryEntry);
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed line — keep going so a single bad write doesn't poison everything.
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('telegram.history: read failed.', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one entry. Best-effort: cache always updates so the *current*
|
||||
* session sees its own writes immediately even if the disk write fails.
|
||||
*/
|
||||
export function appendTelegramMessage(entry: Omit<TelegramHistoryEntry, 'ts'> & { ts?: string }): void {
|
||||
_hydrateIfNeeded();
|
||||
const fullEntry: TelegramHistoryEntry = {
|
||||
chatId: entry.chatId,
|
||||
role: entry.role,
|
||||
text: entry.text,
|
||||
kind: entry.kind,
|
||||
ts: entry.ts ?? new Date().toISOString(),
|
||||
};
|
||||
_cache.push(fullEntry);
|
||||
if (_cache.length > MEMORY_CAP) _cache.splice(0, _cache.length - MEMORY_CAP);
|
||||
const p = _historyPath();
|
||||
if (!p) return;
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.appendFileSync(p, JSON.stringify(fullEntry) + '\n', 'utf8');
|
||||
} catch (e: any) {
|
||||
logError('telegram.history: append failed.', { chatId: entry.chatId, error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent N messages for one chat, oldest-first so the
|
||||
* caller can feed them to an LLM in turn order. `maxEntries` defaults to
|
||||
* 12 — enough for a couple of follow-up turns without exploding the
|
||||
* context window of a small local model.
|
||||
*/
|
||||
export function getRecentMessages(
|
||||
chatId: number,
|
||||
maxEntries: number = DEFAULT_RECENT,
|
||||
): TelegramHistoryEntry[] {
|
||||
_hydrateIfNeeded();
|
||||
const forChat = _cache.filter((e) => e.chatId === chatId);
|
||||
return forChat.slice(-Math.max(1, maxEntries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format recent history as a readable block we can inline into the user
|
||||
* message. We don't use the AI service's multi-message API because
|
||||
* `IAIService.chat({system, user})` only takes one user turn — embedding
|
||||
* the back-and-forth in the user message keeps the API surface unchanged
|
||||
* while still giving the model the context it needs.
|
||||
*/
|
||||
export function formatHistoryForPrompt(history: TelegramHistoryEntry[]): string {
|
||||
if (!history.length) return '';
|
||||
const lines: string[] = ['[Previous conversation in this Telegram chat]'];
|
||||
for (const e of history) {
|
||||
const speaker = e.role === 'user' ? 'User' : 'Bot';
|
||||
// Keep each message bounded so a single huge company-mirror report
|
||||
// doesn't dominate the prompt budget.
|
||||
const trimmed = e.text.length > 800 ? e.text.slice(0, 800) + '…' : e.text;
|
||||
lines.push(`${speaker}: ${trimmed}`);
|
||||
}
|
||||
lines.push('[End of previous conversation]');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** For tests + the "clear history" command. */
|
||||
export function _resetCacheForTests(): void {
|
||||
_cache = [];
|
||||
_hydratedForWorkspace = null;
|
||||
}
|
||||
@@ -151,6 +151,11 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any
|
||||
case 'detachArchitecture':
|
||||
await provider._detachArchitecture();
|
||||
return true;
|
||||
case 'attachArchitecture':
|
||||
// Re-enable architecture context for the current workspace —
|
||||
// user clicked the inactive chip's [Attach] button.
|
||||
await provider._attachArchitecture();
|
||||
return true;
|
||||
case 'activateArchitectureFromText': {
|
||||
// Optional explicit-toggle path: webview can pass arbitrary text
|
||||
// (e.g. the current input draft) for one-shot intent detection.
|
||||
|
||||
+184
-11
@@ -1121,8 +1121,16 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
logInfo('architecture: detached.', { projectId: profile.projectId });
|
||||
}
|
||||
|
||||
/** Force a refresh of the architecture doc for the active project. */
|
||||
/**
|
||||
* Force a refresh of the architecture doc for the active project.
|
||||
*
|
||||
* Always rewrites the auto-managed block (so the "Last Refresh" stamp +
|
||||
* stats reflect the click). Emits an `architectureRefreshResult` event
|
||||
* with the per-file work breakdown — that's what makes the operation
|
||||
* visibly trustworthy in the UI (no more "0.1s, nothing visible").
|
||||
*/
|
||||
async _refreshArchitecture(): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile || !profile.projectRoot) {
|
||||
this._view?.webview.postMessage({
|
||||
@@ -1145,6 +1153,111 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
: p);
|
||||
await this._putChronicleProjects(next);
|
||||
await this._sendArchitectureStatus();
|
||||
// Tell the webview exactly what the scan did so the user can
|
||||
// trust the "Refresh" button actually ran. The three numbers
|
||||
// (newly / cached / deleted) together explain whether the doc
|
||||
// changed or just had its timestamp bumped.
|
||||
this._view?.webview.postMessage({
|
||||
type: 'architectureRefreshResult',
|
||||
value: {
|
||||
projectName: profile.projectName,
|
||||
docPath: result.docPath,
|
||||
newlyAnalyzed: result.refreshStats.newlyAnalyzed,
|
||||
cached: result.refreshStats.cached,
|
||||
deleted: result.refreshStats.deleted.length,
|
||||
durationMs: Date.now() - startedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-attach the architecture context for the active project after a
|
||||
* prior Detach. Rebuilds the doc (so the user gets a fresh scan),
|
||||
* flips `architectureAutoAttach=true`, re-registers the watcher, and
|
||||
* broadcasts the chip back to its active state. The complement of
|
||||
* `_detachArchitecture`.
|
||||
*/
|
||||
async _attachArchitecture(): Promise<void> {
|
||||
// `_ensureActiveProjectForWorkspace` guarantees the active project
|
||||
// matches the current VS Code workspace — without that, hitting
|
||||
// Attach right after opening a different folder would silently
|
||||
// attach to whatever was last active in the *previous* workspace.
|
||||
const profile = await this._ensureActiveProjectForWorkspace();
|
||||
if (!profile || !profile.projectRoot) {
|
||||
this._view?.webview.postMessage({
|
||||
type: 'architectureRefreshFailed',
|
||||
value: { reason: 'no-active-project' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this._activateArchitectureForProject(profile.projectId, {
|
||||
fallbackName: profile.projectName,
|
||||
fallbackRoot: profile.projectRoot,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the active chronicle project actually corresponds to the
|
||||
* folder the user has open in VS Code. Three cases:
|
||||
*
|
||||
* 1. Active project already matches workspace → return it as-is.
|
||||
* 2. A *different* chronicle project matches the workspace → flip
|
||||
* the active id to that one (the user switched folders since
|
||||
* last session).
|
||||
* 3. No chronicle project matches → synthesise a new one from the
|
||||
* workspace folder name + register it.
|
||||
*
|
||||
* Returns the (possibly newly created) active project, or `null` when
|
||||
* no workspace is open. Idempotent — calling repeatedly with no change
|
||||
* is free.
|
||||
*/
|
||||
async _ensureActiveProjectForWorkspace(): Promise<ProjectProfile | null> {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) return null;
|
||||
const projects = this._getChronicleProjects();
|
||||
const active = this._getActiveChronicleProject();
|
||||
const norm = (p: string | undefined) => (p || '').replace(/[\\/]+$/, '').toLowerCase();
|
||||
if (active && active.projectRoot && norm(active.projectRoot) === norm(workspaceRoot)) {
|
||||
return active;
|
||||
}
|
||||
// Case 2: another chronicle project matches → switch active to it
|
||||
const matching = projects.find((p) => norm(p.projectRoot) === norm(workspaceRoot));
|
||||
if (matching) {
|
||||
await this._context.globalState.update(
|
||||
SidebarChatProvider.activeChronicleProjectStateKey,
|
||||
matching.projectId,
|
||||
);
|
||||
logInfo('architecture: switched active project to match workspace.', {
|
||||
from: active?.projectId,
|
||||
to: matching.projectId,
|
||||
});
|
||||
return matching;
|
||||
}
|
||||
// Case 3: synthesise a fresh entry for this workspace
|
||||
const projectName = path.basename(workspaceRoot) || 'Current Project';
|
||||
const projectId = this._slugify(projectName);
|
||||
const now = new Date().toISOString();
|
||||
const profile: ProjectProfile = {
|
||||
projectId,
|
||||
projectName,
|
||||
projectRoot: workspaceRoot,
|
||||
recordRoot: path.join(workspaceRoot, 'docs', 'records', projectName),
|
||||
description: 'Auto-detected from workspace folder.',
|
||||
corePurpose: '',
|
||||
detailLevel: 'standard',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
const nextProjects = projects.filter((p) => p.projectId !== projectId).concat(profile);
|
||||
await this._putChronicleProjects(nextProjects);
|
||||
await this._context.globalState.update(
|
||||
SidebarChatProvider.activeChronicleProjectStateKey,
|
||||
projectId,
|
||||
);
|
||||
logInfo('architecture: registered new project from workspace.', {
|
||||
projectId, projectRoot: workspaceRoot,
|
||||
});
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1166,24 +1279,84 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
});
|
||||
}
|
||||
|
||||
/** Webview chip data — shown above the input box when active. */
|
||||
/**
|
||||
* Webview chip data. Three states:
|
||||
*
|
||||
* 1. **active** — project mode is on; doc is being auto-attached.
|
||||
* 2. **inactive** — there's a project + workspace, but architecture
|
||||
* is either never-activated or user-detached.
|
||||
* The chip shows an `[Attach]` button instead of
|
||||
* hiding entirely, so users always have a one-
|
||||
* click path back into project mode.
|
||||
* 3. **hidden** — no workspace open and no project at all.
|
||||
*
|
||||
* Also does an auto-activation pass for the *fresh-workspace* case:
|
||||
* when the active project has no `architectureDocPath` yet AND the
|
||||
* user hasn't explicitly detached, we generate the doc + flip
|
||||
* `autoAttach=true` so the user opens a new folder and immediately
|
||||
* sees the architecture context working. Existing detach choices are
|
||||
* always respected.
|
||||
*/
|
||||
async _sendArchitectureStatus(): Promise<void> {
|
||||
if (!this._view) return;
|
||||
const p = this._getActiveChronicleProject();
|
||||
const active = !!(p && p.architectureDocPath && p.architectureAutoAttach !== false);
|
||||
this._view.webview.postMessage({
|
||||
type: 'architectureStatus',
|
||||
value: active && p
|
||||
? {
|
||||
// Always sync the active project to the current VS Code workspace
|
||||
// before reporting — otherwise switching workspaces leaves the
|
||||
// chip pointing at the *previous* project's doc.
|
||||
const p = await this._ensureActiveProjectForWorkspace();
|
||||
if (!p) {
|
||||
this._view.webview.postMessage({ type: 'architectureStatus', value: { active: false } });
|
||||
return;
|
||||
}
|
||||
const wasDetached = p.architectureAutoAttach === false;
|
||||
const hasDoc = !!(p.architectureDocPath && fs.existsSync(p.architectureDocPath));
|
||||
|
||||
// Auto-activation for fresh workspaces: never been activated AND
|
||||
// never been detached → kick off a build and re-broadcast. Single
|
||||
// recursion is safe because the post-activate state will hit the
|
||||
// `active` branch below.
|
||||
if (!hasDoc && !wasDetached && p.projectRoot) {
|
||||
try {
|
||||
await this._activateArchitectureForProject(p.projectId, {
|
||||
fallbackName: p.projectName,
|
||||
fallbackRoot: p.projectRoot,
|
||||
});
|
||||
return; // _activateArchitectureForProject sends its own status
|
||||
} catch (e: any) {
|
||||
logError('architecture: auto-activate failed.', { error: e?.message ?? String(e) });
|
||||
// Fall through to the inactive state so the user still sees an Attach button.
|
||||
}
|
||||
}
|
||||
|
||||
const fullyActive = hasDoc && !wasDetached;
|
||||
if (fullyActive) {
|
||||
this._view.webview.postMessage({
|
||||
type: 'architectureStatus',
|
||||
value: {
|
||||
active: true,
|
||||
projectId: p.projectId,
|
||||
projectName: p.projectName,
|
||||
docPath: p.architectureDocPath,
|
||||
lastUpdated: p.architectureLastUpdated || '',
|
||||
autoUpdate: p.architectureAutoUpdate !== false,
|
||||
}
|
||||
: { active: false },
|
||||
});
|
||||
},
|
||||
});
|
||||
// Re-register the watcher in case it was disposed (e.g. workspace switch).
|
||||
this._registerArchitectureWatcher(p);
|
||||
} else {
|
||||
// Inactive but attachable: surface the project name + an Attach hook.
|
||||
this._view.webview.postMessage({
|
||||
type: 'architectureStatus',
|
||||
value: {
|
||||
active: false,
|
||||
canAttach: !!p.projectRoot,
|
||||
projectId: p.projectId,
|
||||
projectName: p.projectName,
|
||||
// Distinguishes "never activated" from "detached" so the
|
||||
// chip can choose the right label ("Activate" vs "Re-attach").
|
||||
detached: wasDetached,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 1인 기업 (Company) Mode ────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user