v2.2.17: Google Service Control & Astra Office Flow Layer
This commit is contained in:
+64
-11
@@ -155,7 +155,9 @@ button,input,select{font:inherit}
|
|||||||
background:rgba(255,255,255,.035);
|
background:rgba(255,255,255,.035);
|
||||||
transition:border-color .16s ease,background .16s ease,transform .16s ease;
|
transition:border-color .16s ease,background .16s ease,transform .16s ease;
|
||||||
}
|
}
|
||||||
|
.roster-item[data-agent]{cursor:pointer}
|
||||||
.roster-item.active{border-color:rgba(138,124,255,.48);background:linear-gradient(180deg,rgba(138,124,255,.16),rgba(138,124,255,.08));transform:translateX(2px)}
|
.roster-item.active{border-color:rgba(138,124,255,.48);background:linear-gradient(180deg,rgba(138,124,255,.16),rgba(138,124,255,.08));transform:translateX(2px)}
|
||||||
|
.roster-item.preview{border-color:rgba(255,255,255,.2);background:rgba(255,255,255,.07)}
|
||||||
.roster-avatar{
|
.roster-avatar{
|
||||||
width:33px;height:33px;border-radius:12px;
|
width:33px;height:33px;border-radius:12px;
|
||||||
display:grid;place-items:center;
|
display:grid;place-items:center;
|
||||||
@@ -170,6 +172,12 @@ button,input,select{font:inherit}
|
|||||||
.roster-status{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.28)}
|
.roster-status{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.28)}
|
||||||
.roster-item.active .roster-status{background:var(--role-color,var(--accent));box-shadow:0 0 12px var(--role-color,var(--accent))}
|
.roster-item.active .roster-status{background:var(--role-color,var(--accent));box-shadow:0 0 12px var(--role-color,var(--accent))}
|
||||||
.office-shell{min-width:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);overflow:hidden}
|
.office-shell{min-width:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);overflow:hidden}
|
||||||
|
.office-shell[data-roster-empty="true"] .office{filter:saturate(.72) brightness(.9)}
|
||||||
|
.office-shell[data-roster-empty="true"] .stage{opacity:.86}
|
||||||
|
.office-shell[data-status="executing"] .office{box-shadow:inset 0 0 0 1px rgba(138,124,255,.12),0 0 0 1px rgba(138,124,255,.08)}
|
||||||
|
.office-shell[data-status="reviewing"] .office{box-shadow:inset 0 0 0 1px rgba(70,216,255,.14),0 0 0 1px rgba(70,216,255,.08)}
|
||||||
|
.office-shell[data-status="waiting_approval"] .office{box-shadow:inset 0 0 0 1px rgba(245,196,90,.16),0 0 0 1px rgba(245,196,90,.08)}
|
||||||
|
.office-shell[data-status="error"] .office{box-shadow:inset 0 0 0 1px rgba(255,107,122,.18),0 0 0 1px rgba(255,107,122,.08)}
|
||||||
.mission-strip{
|
.mission-strip{
|
||||||
min-height:72px;
|
min-height:72px;
|
||||||
display:flex;
|
display:flex;
|
||||||
@@ -209,11 +217,18 @@ button,input,select{font:inherit}
|
|||||||
radial-gradient(circle at 18% 100%,rgba(70,216,255,.12),transparent 28%),
|
radial-gradient(circle at 18% 100%,rgba(70,216,255,.12),transparent 28%),
|
||||||
linear-gradient(135deg,#31283A,#201A29 72%);
|
linear-gradient(135deg,#31283A,#201A29 72%);
|
||||||
}
|
}
|
||||||
.office:before{content:'';position:absolute;left:0;right:0;top:16%;bottom:0;background-image:linear-gradient(rgba(255,255,255,.032) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.032) 1px,transparent 1px);background-size:48px 48px}
|
.office.has-art{
|
||||||
.office:after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,transparent 0 40%,rgba(0,0,0,.18) 100%);pointer-events:none}
|
background:
|
||||||
|
linear-gradient(180deg,rgba(6,10,18,.06),rgba(6,10,18,.12)),
|
||||||
|
var(--office-backdrop) center center / cover no-repeat;
|
||||||
|
}
|
||||||
|
.office:before{content:'';position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);background-size:48px 48px;opacity:.28}
|
||||||
|
.office.has-art:before{background:linear-gradient(180deg,rgba(4,7,14,.06),transparent 28%,rgba(4,7,14,.1));opacity:1}
|
||||||
|
.office:after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,transparent 0 42%,rgba(0,0,0,.2) 100%);pointer-events:none}
|
||||||
.stage{position:relative;width:720px;height:585px;margin:0;z-index:2}
|
.stage{position:relative;width:720px;height:585px;margin:0;z-index:2}
|
||||||
.wall-window{position:absolute;top:16px;width:86px;height:42px;border:2px solid rgba(215,228,255,.35);border-radius:8px;background:linear-gradient(180deg,rgba(160,208,255,.34),rgba(110,150,210,.08));box-shadow:inset 0 0 0 1px rgba(15,20,31,.55)}
|
.wall-window{position:absolute;top:16px;width:86px;height:42px;border:2px solid rgba(215,228,255,.35);border-radius:8px;background:linear-gradient(180deg,rgba(160,208,255,.34),rgba(110,150,210,.08));box-shadow:inset 0 0 0 1px rgba(15,20,31,.55)}
|
||||||
.wall-window.w1{left:84px}.wall-window.w2{left:550px}
|
.wall-window.w1{left:84px}.wall-window.w2{left:550px}
|
||||||
|
.office.has-art .wall-window{display:none}
|
||||||
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
|
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
|
||||||
.obj{filter:drop-shadow(3px 5px 0 rgba(0,0,0,.28));z-index:4}
|
.obj{filter:drop-shadow(3px 5px 0 rgba(0,0,0,.28));z-index:4}
|
||||||
.desk{width:112px;z-index:5;filter:drop-shadow(4px 7px 0 rgba(0,0,0,.28))}
|
.desk{width:112px;z-index:5;filter:drop-shadow(4px 7px 0 rgba(0,0,0,.28))}
|
||||||
@@ -237,6 +252,8 @@ button,input,select{font:inherit}
|
|||||||
.char[data-agent="support"],.desk[data-agent="support"],.roster-item[data-agent="support"]{--role-color:#94A3B8}
|
.char[data-agent="support"],.desk[data-agent="support"],.roster-item[data-agent="support"]{--role-color:#94A3B8}
|
||||||
.char[data-agent="writer"],.desk[data-agent="writer"],.roster-item[data-agent="writer"]{--role-color:#FBBF24}
|
.char[data-agent="writer"],.desk[data-agent="writer"],.roster-item[data-agent="writer"]{--role-color:#FBBF24}
|
||||||
.desk::after{content:'';position:absolute;inset:-4px;border-radius:10px;border:1px solid transparent;pointer-events:none;transition:border-color .2s ease,box-shadow .2s ease}
|
.desk::after{content:'';position:absolute;inset:-4px;border-radius:10px;border:1px solid transparent;pointer-events:none;transition:border-color .2s ease,box-shadow .2s ease}
|
||||||
|
.desk.preview::after{border-color:rgba(255,255,255,.28);box-shadow:0 0 0 1px rgba(255,255,255,.04),0 0 18px rgba(255,255,255,.16)}
|
||||||
|
.char.preview::before{content:'';position:absolute;left:18px;top:-14px;width:20px;height:20px;border-radius:50%;border:1px solid rgba(255,255,255,.42);box-shadow:0 0 18px rgba(255,255,255,.18)}
|
||||||
.stage:has(.char.active[data-agent="ceo"]) .desk[data-agent="ceo"]::after,
|
.stage:has(.char.active[data-agent="ceo"]) .desk[data-agent="ceo"]::after,
|
||||||
.stage:has(.char.active[data-agent="planner"]) .desk[data-agent="planner"]::after,
|
.stage:has(.char.active[data-agent="planner"]) .desk[data-agent="planner"]::after,
|
||||||
.stage:has(.char.active[data-agent="researcher"]) .desk[data-agent="researcher"]::after,
|
.stage:has(.char.active[data-agent="researcher"]) .desk[data-agent="researcher"]::after,
|
||||||
@@ -247,7 +264,7 @@ button,input,select{font:inherit}
|
|||||||
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after,
|
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after,
|
||||||
.stage:has(.char.active[data-agent="writer"]) .desk[data-agent="writer"]::after{border-color:var(--role-color);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 18px color-mix(in srgb,var(--role-color) 35%,transparent)}
|
.stage:has(.char.active[data-agent="writer"]) .desk[data-agent="writer"]::after{border-color:var(--role-color);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 18px color-mix(in srgb,var(--role-color) 35%,transparent)}
|
||||||
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
|
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
|
||||||
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.2;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:nowrap}
|
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.35;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:normal}
|
||||||
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
||||||
.brief-card{
|
.brief-card{
|
||||||
padding:14px;
|
padding:14px;
|
||||||
@@ -453,6 +470,23 @@ body[data-edit-mode="true"] .char .shadow{display:none}
|
|||||||
|
|
||||||
<script>(function(){
|
<script>(function(){
|
||||||
const base='http://127.0.0.1:8765/assets/pixelOffice/derived'; const stage=document.getElementById('stage');
|
const base='http://127.0.0.1:8765/assets/pixelOffice/derived'; const stage=document.getElementById('stage');
|
||||||
|
const officeEl = stage && stage.closest ? stage.closest('.office') : null;
|
||||||
|
if(officeEl){
|
||||||
|
officeEl.style.setProperty('--office-backdrop', 'url("'+base+'/office-backdrop-astra-v2.png")');
|
||||||
|
officeEl.classList.add('has-art');
|
||||||
|
}
|
||||||
|
let _stageScale = 1;
|
||||||
|
function _fitStage(){
|
||||||
|
const shell = stage && stage.closest ? stage.closest('.office') : null;
|
||||||
|
if(!stage || !shell) return;
|
||||||
|
const sx = Math.max(.62, (shell.clientWidth - 28) / 720);
|
||||||
|
const sy = Math.max(.62, (shell.clientHeight - 28) / 585);
|
||||||
|
_stageScale = Math.min(1, sx, sy);
|
||||||
|
stage.style.transform = 'scale(' + _stageScale + ')';
|
||||||
|
stage.style.transformOrigin = 'center center';
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', _fitStage);
|
||||||
|
setTimeout(_fitStage, 0);
|
||||||
// ── 데이터 모델 ──
|
// ── 데이터 모델 ──
|
||||||
// stations: 책상 + 캐릭터 정의 배열 (let — 추가/제거 가능).
|
// stations: 책상 + 캐릭터 정의 배열 (let — 추가/제거 가능).
|
||||||
// key = 안정적 식별자 (DOM dataset.role 로도 사용). 사용자가 새로 만든 책상은 자동 생성.
|
// key = 안정적 식별자 (DOM dataset.role 로도 사용). 사용자가 새로 만든 책상은 자동 생성.
|
||||||
@@ -479,10 +513,10 @@ const DESK_SPRITE_CHOICES=['desk-main','desk-boss','desk-dark-mirror','desk-main
|
|||||||
// 추가 가능한 프랍 sprite 후보.
|
// 추가 가능한 프랍 sprite 후보.
|
||||||
const PROP_SPRITE_CHOICES=['board','plant-tall','bookshelf','plant-bushy','partition','cooler','filing','couch','rug','shelf','printer','monitor-blue','monitor-black','chair-blue','crt'];
|
const PROP_SPRITE_CHOICES=['board','plant-tall','bookshelf','plant-bushy','partition','cooler','filing','couch','rug','shelf','printer','monitor-blue','monitor-black','chair-blue','crt'];
|
||||||
const DEFAULT_PROPS=[
|
const DEFAULT_PROPS=[
|
||||||
{name:'board',x:316,y:12,w:88},{name:'plant-tall',x:44,y:92,w:42},{name:'bookshelf',x:86,y:70,w:54},
|
{name:'plant-tall',x:40,y:118,w:42},{name:'bookshelf',x:86,y:88,w:54},
|
||||||
{name:'plant-bushy',x:642,y:96,w:42},{name:'partition',x:520,y:208,w:72},{name:'cooler',x:640,y:248,w:38},
|
{name:'plant-bushy',x:640,y:118,w:42},{name:'cooler',x:646,y:286,w:38},
|
||||||
{name:'filing',x:620,y:330,w:42},{name:'couch',x:578,y:432,w:96},{name:'rug',x:560,y:510,w:126},
|
{name:'filing',x:618,y:374,w:42},{name:'couch',x:584,y:452,w:96},
|
||||||
{name:'shelf',x:40,y:504,w:118},{name:'printer',x:520,y:520,w:58},{name:'monitor-blue',x:356,y:56,w:44},
|
{name:'rug',x:560,y:514,w:126},{name:'printer',x:520,y:526,w:58},
|
||||||
];
|
];
|
||||||
|
|
||||||
let stations=[]; // mutable, 시작 시 default 또는 saved layout 로 채움.
|
let stations=[]; // mutable, 시작 시 default 또는 saved layout 로 채움.
|
||||||
@@ -823,6 +857,8 @@ function apply(s){
|
|||||||
_lastState = s; // D. 컨텍스트 메뉴 / 세부보기에서 사용.
|
_lastState = s; // D. 컨텍스트 메뉴 / 세부보기에서 사용.
|
||||||
const st = s?.status || 'idle';
|
const st = s?.status || 'idle';
|
||||||
const meta = _statusMeta(st);
|
const meta = _statusMeta(st);
|
||||||
|
const officeShell = document.querySelector('.office-shell');
|
||||||
|
if(officeShell) officeShell.dataset.status = st;
|
||||||
// 정적 갱신은 *항상* — 헤더/태스크/단계/로그/프로그레스.
|
// 정적 갱신은 *항상* — 헤더/태스크/단계/로그/프로그레스.
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const phasePill = document.getElementById('phasePill');
|
const phasePill = document.getElementById('phasePill');
|
||||||
@@ -900,6 +936,7 @@ function apply(s){
|
|||||||
} else {
|
} else {
|
||||||
mm.style.display = 'none';
|
mm.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
setTimeout(_fitStage, 0);
|
||||||
// 활성 캐릭터 결정. roleMap 은 agentKey → 실제 존재하는 station.key 로 lookup
|
// 활성 캐릭터 결정. roleMap 은 agentKey → 실제 존재하는 station.key 로 lookup
|
||||||
// 하므로, 매핑된 책상이 없으면 null. 사용자가 default ceo 책상을 지워도 안전.
|
// 하므로, 매핑된 책상이 없으면 null. 사용자가 default ceo 책상을 지워도 안전.
|
||||||
let role = null;
|
let role = null;
|
||||||
@@ -1100,6 +1137,8 @@ function _renderRoster(roster, activeAgentId){
|
|||||||
const count = document.getElementById('rosterCount');
|
const count = document.getElementById('rosterCount');
|
||||||
if(!wrap || !count) return;
|
if(!wrap || !count) return;
|
||||||
const list = Array.isArray(roster) ? roster : [];
|
const list = Array.isArray(roster) ? roster : [];
|
||||||
|
const officeShell = document.querySelector('.office-shell');
|
||||||
|
if(officeShell) officeShell.dataset.rosterEmpty = list.length ? 'false' : 'true';
|
||||||
count.textContent = String(list.length);
|
count.textContent = String(list.length);
|
||||||
if(!list.length){
|
if(!list.length){
|
||||||
wrap.innerHTML = '<div class="roster-item"><div class="roster-copy"><strong>등록된 팀이 없습니다</strong><span class="roster-meta">회사 모드를 켜면 라인업이 표시됩니다.</span></div></div>';
|
wrap.innerHTML = '<div class="roster-item"><div class="roster-copy"><strong>등록된 팀이 없습니다</strong><span class="roster-meta">회사 모드를 켜면 라인업이 표시됩니다.</span></div></div>';
|
||||||
@@ -1117,6 +1156,20 @@ function _renderRoster(roster, activeAgentId){
|
|||||||
'</div>'+
|
'</div>'+
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
wrap.querySelectorAll('.roster-item[data-agent]').forEach((item)=>{
|
||||||
|
const id = item.dataset.agent;
|
||||||
|
item.addEventListener('mouseenter', ()=>_previewAgent(id, true, item));
|
||||||
|
item.addEventListener('mouseleave', ()=>_previewAgent(id, false, item));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function _previewAgent(agentId, on, item){
|
||||||
|
if(item) item.classList.toggle('preview', !!on);
|
||||||
|
const st = findStationByAgent(agentId);
|
||||||
|
if(!st) return;
|
||||||
|
const desk = __deskWrap[st.key];
|
||||||
|
const ch = chars[st.key];
|
||||||
|
if(desk) desk.classList.toggle('preview', !!on);
|
||||||
|
if(ch) ch.classList.toggle('preview', !!on);
|
||||||
}
|
}
|
||||||
// refactor #G-full — roster 에 있는 agent 중 desk 가 없는 경우 자동 생성.
|
// refactor #G-full — roster 에 있는 agent 중 desk 가 없는 경우 자동 생성.
|
||||||
// 한 번 처리된 agentId 는 _autoDeskedFor 에 기록 → 사용자가 그 desk 를 지워도 재생성 안 함.
|
// 한 번 처리된 agentId 는 _autoDeskedFor 에 기록 → 사용자가 그 desk 를 지워도 재생성 안 함.
|
||||||
@@ -1664,8 +1717,8 @@ stage.addEventListener('mousedown', e=>{
|
|||||||
const rect = stage.getBoundingClientRect();
|
const rect = stage.getBoundingClientRect();
|
||||||
const tx = parseFloat(target.style.left)||0;
|
const tx = parseFloat(target.style.left)||0;
|
||||||
const ty = parseFloat(target.style.top)||0;
|
const ty = parseFloat(target.style.top)||0;
|
||||||
_dragDX = e.clientX - rect.left - tx;
|
_dragDX = ((e.clientX - rect.left) / _stageScale) - tx;
|
||||||
_dragDY = e.clientY - rect.top - ty;
|
_dragDY = ((e.clientY - rect.top) / _stageScale) - ty;
|
||||||
target.classList.add('dragging');
|
target.classList.add('dragging');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1700,8 +1753,8 @@ document.addEventListener('keydown', e=>{
|
|||||||
document.addEventListener('mousemove', e=>{
|
document.addEventListener('mousemove', e=>{
|
||||||
if(!_editMode || !_drag) return;
|
if(!_editMode || !_drag) return;
|
||||||
const rect = stage.getBoundingClientRect();
|
const rect = stage.getBoundingClientRect();
|
||||||
let x = e.clientX - rect.left - _dragDX;
|
let x = ((e.clientX - rect.left) / _stageScale) - _dragDX;
|
||||||
let y = e.clientY - rect.top - _dragDY;
|
let y = ((e.clientY - rect.top) / _stageScale) - _dragDY;
|
||||||
// 4px 격자 snap
|
// 4px 격자 snap
|
||||||
x = Math.round(x/4)*4;
|
x = Math.round(x/4)*4;
|
||||||
y = Math.round(y/4)*4;
|
y = Math.round(y/4)*4;
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
<!-- ASTRA:AUTO-START -->
|
<!-- ASTRA:AUTO-START -->
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
- **Workspace**: `ConnectAI` `v2.2.15` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
- **Workspace**: `ConnectAI` `v2.2.16` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
||||||
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
|
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
|
||||||
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
|
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
|
||||||
- **Stats**: 250 source files, ~50,558 lines across 5 top-level modules.
|
- **Stats**: 250 source files, ~50,659 lines across 5 top-level modules.
|
||||||
|
|
||||||
## Last Refresh
|
## Last Refresh
|
||||||
- **Time**: 2026-05-16T13:16:41.338Z
|
- **Time**: 2026-05-16T13:21:15.124Z
|
||||||
- **Files newly analysed**: 3
|
- **Files newly analysed**: 5
|
||||||
- **Files reused from cache**: 247
|
- **Files reused from cache**: 245
|
||||||
|
|
||||||
## Directory Map
|
## Directory Map
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -64,7 +64,7 @@ flowchart LR
|
|||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
### `src/` — 127 files, ~34,341 lines
|
### `src/` — 127 files, ~34,442 lines
|
||||||
|
|
||||||
**Sub-directories**
|
**Sub-directories**
|
||||||
- `src/features/` (54) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
- `src/features/` (54) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
||||||
@@ -87,7 +87,7 @@ flowchart LR
|
|||||||
- `src/core/services.ts` (164 lines)
|
- `src/core/services.ts` (164 lines)
|
||||||
- `src/lib/paths.ts` (151 lines)
|
- `src/lib/paths.ts` (151 lines)
|
||||||
- `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so
|
- `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so
|
||||||
- `src/sidebarProvider.ts` (4141 lines)
|
- `src/sidebarProvider.ts` (4149 lines)
|
||||||
- `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
- `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
||||||
- `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
- `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
||||||
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
||||||
@@ -103,7 +103,7 @@ flowchart LR
|
|||||||
- `src/features/company/sessionStore.ts` (231 lines) — Disk persistence for company-mode session artefacts. Each company turn produces a timestamped directory: <workspaceRoot>/.astra/company/sessions/2026-05-13T21-29/ ├─ brief.md ← CEO's task decompositio
|
- `src/features/company/sessionStore.ts` (231 lines) — Disk persistence for company-mode session artefacts. Each company turn produces a timestamped directory: <workspaceRoot>/.astra/company/sessions/2026-05-13T21-29/ ├─ brief.md ← CEO's task decompositio
|
||||||
- `src/features/projectArchitecture/scanner.ts` (644 lines) — Deep static analyser for the Project Architecture Context generator. Walks the project tree (skipping the usual nodemodules / out / dist noise), pulls the role of each interesting file from its leadin
|
- `src/features/projectArchitecture/scanner.ts` (644 lines) — Deep static analyser for the Project Architecture Context generator. Walks the project tree (skipping the usual nodemodules / out / dist noise), pulls the role of each interesting file from its leadin
|
||||||
- `src/lib/contextManager.ts` (275 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
|
- `src/lib/contextManager.ts` (275 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
|
||||||
- `src/extension.ts` (1179 lines)
|
- `src/extension.ts` (1201 lines)
|
||||||
- `src/features/company/resumeStore.ts` (134 lines) — Disk persistence for company-turn resume state. 각 turn의 sessionDir 안에 resume.json을 두고, dispatcher가 매 의미 있는 시점(plan 확정 / 각 stage 직후 / abort 시점)에 현재 상태를 덮어쓴다. 재개 시점에는 이 파일을 읽어 nextIndex 부터 dispatch 재개.
|
- `src/features/company/resumeStore.ts` (134 lines) — Disk persistence for company-turn resume state. 각 turn의 sessionDir 안에 resume.json을 두고, dispatcher가 매 의미 있는 시점(plan 확정 / 각 stage 직후 / abort 시점)에 현재 상태를 덮어쓴다. 재개 시점에는 이 파일을 읽어 nextIndex 부터 dispatch 재개.
|
||||||
- `src/core/astraPath.ts` (50 lines) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토리를 해결하여, 사용자 프로젝트 루트가 아닌 ConnectAI 패키지 내부에 데이터를 저장합니다. 이 모듈은 AAL(Astra Autonomou
|
- `src/core/astraPath.ts` (50 lines) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토리를 해결하여, 사용자 프로젝트 루트가 아닌 ConnectAI 패키지 내부에 데이터를 저장합니다. 이 모듈은 AAL(Astra Autonomou
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한
|
|||||||
**Designed for High-Performance Decision Making.**
|
**Designed for High-Performance Decision Making.**
|
||||||
Copyright (C) **g1nation**. All rights reserved.
|
Copyright (C) **g1nation**. All rights reserved.
|
||||||
|
|
||||||
_Last auto-scan: 2026-05-16T13:16:41.338Z · signature `325106ed`_
|
_Last auto-scan: 2026-05-16T13:21:15.124Z · signature `f2379ce`_
|
||||||
<!-- ASTRA:AUTO-END -->
|
<!-- ASTRA:AUTO-END -->
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"generatedAt": "2026-05-16T13:16:41.347Z",
|
"generatedAt": "2026-05-16T13:21:15.133Z",
|
||||||
"files": {
|
"files": {
|
||||||
"src/agent.ts": {
|
"src/agent.ts": {
|
||||||
"mtimeMs": 1778936503000,
|
"mtimeMs": 1778936503000,
|
||||||
@@ -259,9 +259,9 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"src/extension.ts": {
|
"src/extension.ts": {
|
||||||
"mtimeMs": 1778935438000,
|
"mtimeMs": 1778937452000,
|
||||||
"size": 61216,
|
"size": 62380,
|
||||||
"lines": 1179,
|
"lines": 1201,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": [
|
"imports": [
|
||||||
"src/utils",
|
"src/utils",
|
||||||
@@ -362,15 +362,15 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"src/features/astraOffice/view/officeBody.ts": {
|
"src/features/astraOffice/view/officeBody.ts": {
|
||||||
"mtimeMs": 1778937173000,
|
"mtimeMs": 1778937436000,
|
||||||
"size": 3988,
|
"size": 3986,
|
||||||
"lines": 102,
|
"lines": 102,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"src/features/astraOffice/view/officeStyles.ts": {
|
"src/features/astraOffice/view/officeStyles.ts": {
|
||||||
"mtimeMs": 1778937307000,
|
"mtimeMs": 1778937430000,
|
||||||
"size": 20643,
|
"size": 20956,
|
||||||
"lines": 342,
|
"lines": 342,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": []
|
"imports": []
|
||||||
@@ -404,9 +404,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/features/calendar/calendarCache.ts": {
|
"src/features/calendar/calendarCache.ts": {
|
||||||
"mtimeMs": 1778935391000,
|
"mtimeMs": 1778937433000,
|
||||||
"size": 8115,
|
"size": 11824,
|
||||||
"lines": 170,
|
"lines": 241,
|
||||||
"role": "Google Calendar (iCal) 캐시 — fetch + parse + 회사 shared/calendarcache.md 에 저장. Connectorigin 의 googlecalendar.py 를 TypeScript / native fetch 로 옮김. OAuth 없음. 사용자가 Google Calendar 설정 → \"비공개 주소(iCal 형식)\" 복",
|
"role": "Google Calendar (iCal) 캐시 — fetch + parse + 회사 shared/calendarcache.md 에 저장. Connectorigin 의 googlecalendar.py 를 TypeScript / native fetch 로 옮김. OAuth 없음. 사용자가 Google Calendar 설정 → \"비공개 주소(iCal 형식)\" 복",
|
||||||
"imports": [
|
"imports": [
|
||||||
"src/features/calendar/icsParser"
|
"src/features/calendar/icsParser"
|
||||||
@@ -1183,9 +1183,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/sidebarProvider.ts": {
|
"src/sidebarProvider.ts": {
|
||||||
"mtimeMs": 1778933999000,
|
"mtimeMs": 1778937445000,
|
||||||
"size": 188454,
|
"size": 189218,
|
||||||
"lines": 4141,
|
"lines": 4149,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": [
|
"imports": [
|
||||||
"src/utils",
|
"src/utils",
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||||
"createdAt": 1778937508211,
|
"createdAt": 1778938100767,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
"createdAt": 1778937508205,
|
"createdAt": 1778938100758,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"createdAt": 1778937508200,
|
"createdAt": 1778938100756,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "---\nid: stress_conflict_1778937508187\ndate: 2026-05-16T13:18:28.211Z\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]** 전략 수립 중... (12ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (10ms)\n",
|
"result": "---\nid: stress_conflict_1778938100739\ndate: 2026-05-16T13:28:20.772Z\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]** 핵심 정보 수집 및 분석 중... (6ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (6ms)\n",
|
||||||
"createdAt": 1778937508211,
|
"createdAt": 1778938100772,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+11
-11
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"missionId": "stress_conflict_1778937508187",
|
"missionId": "stress_conflict_1778938100739",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"startTime": "2026-05-16T13:18:28.187Z",
|
"startTime": "2026-05-16T13:28:20.739Z",
|
||||||
"totalElapsedMs": 25,
|
"totalElapsedMs": 33,
|
||||||
"results": {
|
"results": {
|
||||||
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
@@ -16,30 +16,30 @@
|
|||||||
{
|
{
|
||||||
"from": "idle",
|
"from": "idle",
|
||||||
"to": "planner",
|
"to": "planner",
|
||||||
"durationMs": 12,
|
"durationMs": 11,
|
||||||
"message": "전략 수립 중...",
|
"message": "전략 수립 중...",
|
||||||
"ts": "2026-05-16T13:18:28.199Z"
|
"ts": "2026-05-16T13:28:20.750Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "planner",
|
"from": "planner",
|
||||||
"to": "researcher",
|
"to": "researcher",
|
||||||
"durationMs": 1,
|
"durationMs": 6,
|
||||||
"message": "핵심 정보 수집 및 분석 중...",
|
"message": "핵심 정보 수집 및 분석 중...",
|
||||||
"ts": "2026-05-16T13:18:28.200Z"
|
"ts": "2026-05-16T13:28:20.756Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "researcher",
|
"from": "researcher",
|
||||||
"to": "writer",
|
"to": "writer",
|
||||||
"durationMs": 10,
|
"durationMs": 6,
|
||||||
"message": "최종 리포트 작성 및 편집 중...",
|
"message": "최종 리포트 작성 및 편집 중...",
|
||||||
"ts": "2026-05-16T13:18:28.210Z"
|
"ts": "2026-05-16T13:28:20.762Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "writer",
|
"from": "writer",
|
||||||
"to": "completed",
|
"to": "completed",
|
||||||
"durationMs": 2,
|
"durationMs": 10,
|
||||||
"message": "미션 완료",
|
"message": "미션 완료",
|
||||||
"ts": "2026-05-16T13:18:28.212Z"
|
"ts": "2026-05-16T13:28:20.772Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"resilienceMetrics": {
|
"resilienceMetrics": {
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
# Astra Patch Notes
|
# Astra Patch Notes
|
||||||
|
|
||||||
|
## v2.2.17 (2026-05-16)
|
||||||
|
### ⚙️ Google Service Control & Astra Office Flow Layer
|
||||||
|
- **구글 서비스 설정 패널(Google Control) 도입:** 설정 패널 내에 구글 캘린더 OAuth 연결, iCal 동기화, 클라이언트 자격 증명 관리를 위한 전용 섹션을 추가했습니다.
|
||||||
|
- **아스트라 오피스 플로우 레이어(Flow Layer) 탑재:** 오피스 스테이지 상단에 SVG 기반의 플로우 레이어를 추가하여 에이전트 간의 관계나 작업 흐름을 시각화할 수 있는 기술적 기반을 마련했습니다.
|
||||||
|
- **배경 최적화:** 아스트라 오피스 V2 전용 백드롭 자산을 추가하고 디자인 디테일을 개선했습니다.
|
||||||
|
- **실시간 동기화 강화:** 설정 변경 시 즉시 오피스 상태와 캘린더 캐시에 반영되도록 백엔드 핸들러를 정교화했습니다.
|
||||||
|
- **신규 패키징:** `astra-2.2.17.vsix` 패키지를 통해 구글 연동 제어권이 강화된 최신 버전을 배포합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## v2.2.16 (2026-05-16)
|
## v2.2.16 (2026-05-16)
|
||||||
### 🏢 Astra Office UI Overhaul: Operations Floor Experience
|
### 🏢 Astra Office UI Overhaul: Operations Floor Experience
|
||||||
- **차세대 오피스 UI 도입:** 단순한 뷰어를 넘어 실제 운영 본부(Operations Floor)의 느낌을 주는 대대적인 인터페이스 개편을 단행했습니다.
|
- **차세대 오피스 UI 도입:** 단순한 뷰어를 넘어 실제 운영 본부(Operations Floor)의 느낌을 주는 대대적인 인터페이스 개편을 단행했습니다.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 517 KiB |
@@ -7,5 +7,5 @@
|
|||||||
"corePurpose": "",
|
"corePurpose": "",
|
||||||
"detailLevel": "standard",
|
"detailLevel": "standard",
|
||||||
"createdAt": "2026-05-13T13:09:33.788Z",
|
"createdAt": "2026-05-13T13:09:33.788Z",
|
||||||
"updatedAt": "2026-05-16T13:14:50.049Z"
|
"updatedAt": "2026-05-16T13:22:37.704Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,86 @@
|
|||||||
<div id="tgError" class="error" hidden></div>
|
<div id="tgError" class="error" hidden></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Google (Calendar + Sheets) -->
|
||||||
|
<section class="section" data-section="google">
|
||||||
|
<h2>Google (Calendar · Sheets)</h2>
|
||||||
|
<p class="hint">회의록·할일을 Google Calendar 에 자동 등록하고 Sheets 를 읽고 쓰려면 OAuth 가 필요합니다. <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a> 에서 Desktop OAuth Client 만들고 Client ID/Secret 을 아래에 붙여넣으세요.</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>현재 연결 상태</label>
|
||||||
|
<div class="readout" id="googleConnStatus">—</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label for="gClientId">OAuth Client ID</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="gClientId" type="text" placeholder="xxxxxxxx.apps.googleusercontent.com" autocomplete="off" spellcheck="false" />
|
||||||
|
<button data-save="google.clientId">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label for="gClientSecret">OAuth Client Secret</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="gClientSecret" type="password" placeholder="GOCSPX-..." autocomplete="off" spellcheck="false" />
|
||||||
|
<button data-save="google.clientSecret">저장</button>
|
||||||
|
</div>
|
||||||
|
<small class="hint">Desktop OAuth client 의 secret 은 Google 가이드상 비공개 아님. 그래도 Settings Sync 에는 포함되지 않습니다 (machine scope).</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button id="googleConnect">OAuth 연결 / 재연결</button>
|
||||||
|
<button id="googleDisconnect" class="ghost">연결 해제</button>
|
||||||
|
<span id="googleConnStatusInline" class="status-inline"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label for="gCalendarId">Calendar ID</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="gCalendarId" type="text" placeholder="primary" autocomplete="off" spellcheck="false" />
|
||||||
|
<button data-save="google.calendarId">저장</button>
|
||||||
|
</div>
|
||||||
|
<small class="hint">기본 'primary' (본인 메인 캘린더). 다른 캘린더 ID 입력 가능.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label for="gDefaultDur">기본 일정 길이 (분)</label>
|
||||||
|
<div class="input-group narrow">
|
||||||
|
<input id="gDefaultDur" type="number" min="5" max="720" step="5" />
|
||||||
|
<button data-save="google.defaultEventDurationMinutes">저장</button>
|
||||||
|
</div>
|
||||||
|
<small class="hint">duration / end 안 지정된 일정 생성 시 사용. Default 60.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top:18px;font-size:13px;color:var(--muted)">iCal 읽기 (선택 사항)</h3>
|
||||||
|
<p class="hint">OAuth 없이 비공개 iCal URL 로 일정만 읽고 싶을 때. OAuth 연결돼 있으면 비워둬도 됩니다.</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label for="gIcalUrl">iCal URL</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="gIcalUrl" type="password" placeholder="https://calendar.google.com/calendar/ical/.../basic.ics" autocomplete="off" spellcheck="false" />
|
||||||
|
<button data-save="google.icalUrl">저장</button>
|
||||||
|
</div>
|
||||||
|
<small class="hint">URL 자체가 capability 토큰이라 password 처리. Settings Sync 에 안 포함됨.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label for="gIcalDays">iCal 미리 가져올 일수</label>
|
||||||
|
<div class="input-group narrow">
|
||||||
|
<input id="gIcalDays" type="number" min="1" max="90" step="1" />
|
||||||
|
<button data-save="google.icalDaysAhead">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button id="googleIcalRefresh">지금 iCal 새로고침</button>
|
||||||
|
<span id="googleIcalStatus" class="status-inline"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="googleFeedback" class="feedback" hidden></div>
|
||||||
|
<div id="googleError" class="error" hidden></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Advanced -->
|
<!-- Advanced -->
|
||||||
<section class="section" data-section="advanced">
|
<section class="section" data-section="advanced">
|
||||||
<h2>고급</h2>
|
<h2>고급</h2>
|
||||||
|
|||||||
@@ -41,6 +41,22 @@
|
|||||||
const advAutoSteps = $('advAutoSteps');
|
const advAutoSteps = $('advAutoSteps');
|
||||||
const advCtxSize = $('advCtxSize');
|
const advCtxSize = $('advCtxSize');
|
||||||
|
|
||||||
|
// ---- Google (Calendar + Sheets) ----
|
||||||
|
const gClientId = $('gClientId');
|
||||||
|
const gClientSecret = $('gClientSecret');
|
||||||
|
const gCalendarId = $('gCalendarId');
|
||||||
|
const gDefaultDur = $('gDefaultDur');
|
||||||
|
const gIcalUrl = $('gIcalUrl');
|
||||||
|
const gIcalDays = $('gIcalDays');
|
||||||
|
const googleConnStatus = $('googleConnStatus');
|
||||||
|
const googleConnStatusInline = $('googleConnStatusInline');
|
||||||
|
const googleConnectBtn = $('googleConnect');
|
||||||
|
const googleDisconnectBtn = $('googleDisconnect');
|
||||||
|
const googleIcalRefreshBtn = $('googleIcalRefresh');
|
||||||
|
const googleIcalStatus = $('googleIcalStatus');
|
||||||
|
const googleFeedback = $('googleFeedback');
|
||||||
|
const googleError = $('googleError');
|
||||||
|
|
||||||
// ---- Banner ----
|
// ---- Banner ----
|
||||||
const bannerError = $('bannerError');
|
const bannerError = $('bannerError');
|
||||||
|
|
||||||
@@ -115,6 +131,32 @@
|
|||||||
document.querySelector('[data-save="advanced.autoSteps"]').addEventListener('click', () =>
|
document.querySelector('[data-save="advanced.autoSteps"]').addEventListener('click', () =>
|
||||||
vscode.postMessage({ type: 'advanced.update', maxAutoSteps: Number(advAutoSteps.value) })
|
vscode.postMessage({ type: 'advanced.update', maxAutoSteps: Number(advAutoSteps.value) })
|
||||||
);
|
);
|
||||||
|
// ---- Google listeners ----
|
||||||
|
document.querySelector('[data-save="google.clientId"]').addEventListener('click', () =>
|
||||||
|
vscode.postMessage({ type: 'google.update', clientId: gClientId.value })
|
||||||
|
);
|
||||||
|
document.querySelector('[data-save="google.clientSecret"]').addEventListener('click', () => {
|
||||||
|
// 저장 후 입력 필드는 비움 — 다음부터는 placeholder 가 "저장됨" 으로 표시됨.
|
||||||
|
vscode.postMessage({ type: 'google.update', clientSecret: gClientSecret.value });
|
||||||
|
gClientSecret.value = '';
|
||||||
|
});
|
||||||
|
document.querySelector('[data-save="google.calendarId"]').addEventListener('click', () =>
|
||||||
|
vscode.postMessage({ type: 'google.update', calendarId: gCalendarId.value })
|
||||||
|
);
|
||||||
|
document.querySelector('[data-save="google.defaultEventDurationMinutes"]').addEventListener('click', () =>
|
||||||
|
vscode.postMessage({ type: 'google.update', defaultEventDurationMinutes: Number(gDefaultDur.value) })
|
||||||
|
);
|
||||||
|
document.querySelector('[data-save="google.icalUrl"]').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'google.update', icalUrl: gIcalUrl.value });
|
||||||
|
gIcalUrl.value = '';
|
||||||
|
});
|
||||||
|
document.querySelector('[data-save="google.icalDaysAhead"]').addEventListener('click', () =>
|
||||||
|
vscode.postMessage({ type: 'google.update', icalDaysAhead: Number(gIcalDays.value) })
|
||||||
|
);
|
||||||
|
googleConnectBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.connect' }));
|
||||||
|
googleDisconnectBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.disconnect' }));
|
||||||
|
googleIcalRefreshBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.icalRefresh' }));
|
||||||
|
|
||||||
document.querySelector('[data-save="advanced.ctxSize"]').addEventListener('click', () =>
|
document.querySelector('[data-save="advanced.ctxSize"]').addEventListener('click', () =>
|
||||||
vscode.postMessage({ type: 'advanced.update', maxContextSize: Number(advCtxSize.value) })
|
vscode.postMessage({ type: 'advanced.update', maxContextSize: Number(advCtxSize.value) })
|
||||||
);
|
);
|
||||||
@@ -264,6 +306,32 @@
|
|||||||
advMulti.checked = !!adv.multiAgentEnabled;
|
advMulti.checked = !!adv.multiAgentEnabled;
|
||||||
setIfNotFocused(advAutoSteps, adv.maxAutoSteps);
|
setIfNotFocused(advAutoSteps, adv.maxAutoSteps);
|
||||||
setIfNotFocused(advCtxSize, adv.maxContextSize);
|
setIfNotFocused(advCtxSize, adv.maxContextSize);
|
||||||
|
|
||||||
|
// ---- Google (Calendar + Sheets) ----
|
||||||
|
const g = state.google;
|
||||||
|
if (g) {
|
||||||
|
setIfNotFocused(gClientId, g.clientId);
|
||||||
|
// Secret 은 값 자체를 화면에 안 그림 — 설정 여부만 placeholder 로 표현.
|
||||||
|
gClientSecret.placeholder = g.hasClientSecret ? '••• 저장됨 (덮어쓰려면 새 값 입력)' : 'GOCSPX-...';
|
||||||
|
setIfNotFocused(gCalendarId, g.calendarId);
|
||||||
|
setIfNotFocused(gDefaultDur, g.defaultEventDurationMinutes);
|
||||||
|
gIcalUrl.placeholder = g.hasIcalUrl ? '••• 저장됨 (덮어쓰려면 새 URL 입력)' : 'https://calendar.google.com/calendar/ical/.../basic.ics';
|
||||||
|
setIfNotFocused(gIcalDays, g.icalDaysAhead);
|
||||||
|
// 연결 상태 readout.
|
||||||
|
if (g.connected) {
|
||||||
|
const at = g.connectedAt ? g.connectedAt.slice(0, 16).replace('T', ' ') : '';
|
||||||
|
googleConnStatus.textContent = `✅ 연결됨${g.connectedAs ? ' · ' + g.connectedAs : ''}${at ? ' (' + at + ')' : ''}`;
|
||||||
|
googleConnStatus.style.color = '#10b981';
|
||||||
|
googleDisconnectBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
googleConnStatus.textContent = '⛔ OAuth 연결 안됨 — Client ID/Secret 저장 후 [OAuth 연결] 클릭';
|
||||||
|
googleConnStatus.style.color = '';
|
||||||
|
googleDisconnectBtn.disabled = true;
|
||||||
|
}
|
||||||
|
googleIcalStatus.textContent = g.lastIcalFetchAt
|
||||||
|
? `마지막 새로고침: ${g.lastIcalFetchAt.slice(0, 16).replace('T', ' ')}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vscode.postMessage({ type: 'ready' });
|
vscode.postMessage({ type: 'ready' });
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "astra",
|
"name": "astra",
|
||||||
"displayName": "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.",
|
"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.2.16",
|
"version": "2.2.17",
|
||||||
"publisher": "g1nation",
|
"publisher": "g1nation",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
@@ -804,6 +804,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
// Astra Settings webview — single entry point for user-facing config (Phase 5-A: Telegram only).
|
// Astra Settings webview — single entry point for user-facing config (Phase 5-A: Telegram only).
|
||||||
const settingsPanel = new SettingsPanelProvider({
|
const settingsPanel = new SettingsPanelProvider({
|
||||||
extensionUri: context.extensionUri,
|
extensionUri: context.extensionUri,
|
||||||
|
context,
|
||||||
secrets: context.secrets,
|
secrets: context.secrets,
|
||||||
telegramClient,
|
telegramClient,
|
||||||
telegramBot,
|
telegramBot,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const OFFICE_BODY = `
|
|||||||
<div class="office-stage-wrap">
|
<div class="office-stage-wrap">
|
||||||
<div class="office">
|
<div class="office">
|
||||||
<div class="stage" id="stage">
|
<div class="stage" id="stage">
|
||||||
|
<svg id="flowLayer" class="flow-layer" viewBox="0 0 720 585" preserveAspectRatio="none" aria-hidden="true"></svg>
|
||||||
<div class="wall-window w1"></div>
|
<div class="wall-window w1"></div>
|
||||||
<div class="wall-window w2"></div>
|
<div class="wall-window w2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -149,7 +149,9 @@ button,input,select{font:inherit}
|
|||||||
background:rgba(255,255,255,.035);
|
background:rgba(255,255,255,.035);
|
||||||
transition:border-color .16s ease,background .16s ease,transform .16s ease;
|
transition:border-color .16s ease,background .16s ease,transform .16s ease;
|
||||||
}
|
}
|
||||||
|
.roster-item[data-agent]{cursor:pointer}
|
||||||
.roster-item.active{border-color:rgba(138,124,255,.48);background:linear-gradient(180deg,rgba(138,124,255,.16),rgba(138,124,255,.08));transform:translateX(2px)}
|
.roster-item.active{border-color:rgba(138,124,255,.48);background:linear-gradient(180deg,rgba(138,124,255,.16),rgba(138,124,255,.08));transform:translateX(2px)}
|
||||||
|
.roster-item.preview{border-color:rgba(255,255,255,.2);background:rgba(255,255,255,.07)}
|
||||||
.roster-avatar{
|
.roster-avatar{
|
||||||
width:33px;height:33px;border-radius:12px;
|
width:33px;height:33px;border-radius:12px;
|
||||||
display:grid;place-items:center;
|
display:grid;place-items:center;
|
||||||
@@ -164,6 +166,12 @@ button,input,select{font:inherit}
|
|||||||
.roster-status{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.28)}
|
.roster-status{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.28)}
|
||||||
.roster-item.active .roster-status{background:var(--role-color,var(--accent));box-shadow:0 0 12px var(--role-color,var(--accent))}
|
.roster-item.active .roster-status{background:var(--role-color,var(--accent));box-shadow:0 0 12px var(--role-color,var(--accent))}
|
||||||
.office-shell{min-width:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);overflow:hidden}
|
.office-shell{min-width:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);overflow:hidden}
|
||||||
|
.office-shell[data-roster-empty="true"] .office{filter:saturate(.72) brightness(.9)}
|
||||||
|
.office-shell[data-roster-empty="true"] .stage{opacity:.86}
|
||||||
|
.office-shell[data-status="executing"] .office{box-shadow:inset 0 0 0 1px rgba(138,124,255,.12),0 0 0 1px rgba(138,124,255,.08)}
|
||||||
|
.office-shell[data-status="reviewing"] .office{box-shadow:inset 0 0 0 1px rgba(70,216,255,.14),0 0 0 1px rgba(70,216,255,.08)}
|
||||||
|
.office-shell[data-status="waiting_approval"] .office{box-shadow:inset 0 0 0 1px rgba(245,196,90,.16),0 0 0 1px rgba(245,196,90,.08)}
|
||||||
|
.office-shell[data-status="error"] .office{box-shadow:inset 0 0 0 1px rgba(255,107,122,.18),0 0 0 1px rgba(255,107,122,.08)}
|
||||||
.mission-strip{
|
.mission-strip{
|
||||||
min-height:72px;
|
min-height:72px;
|
||||||
display:flex;
|
display:flex;
|
||||||
@@ -203,11 +211,36 @@ button,input,select{font:inherit}
|
|||||||
radial-gradient(circle at 18% 100%,rgba(70,216,255,.12),transparent 28%),
|
radial-gradient(circle at 18% 100%,rgba(70,216,255,.12),transparent 28%),
|
||||||
linear-gradient(135deg,#31283A,#201A29 72%);
|
linear-gradient(135deg,#31283A,#201A29 72%);
|
||||||
}
|
}
|
||||||
.office:before{content:'';position:absolute;left:0;right:0;top:16%;bottom:0;background-image:linear-gradient(rgba(255,255,255,.032) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.032) 1px,transparent 1px);background-size:48px 48px}
|
.office.has-art{
|
||||||
.office:after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,transparent 0 40%,rgba(0,0,0,.18) 100%);pointer-events:none}
|
background:
|
||||||
|
linear-gradient(180deg,rgba(6,10,18,.06),rgba(6,10,18,.12)),
|
||||||
|
var(--office-backdrop) center center / cover no-repeat;
|
||||||
|
}
|
||||||
|
.office:before{content:'';position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);background-size:48px 48px;opacity:.28}
|
||||||
|
.office.has-art:before{background:linear-gradient(180deg,rgba(4,7,14,.06),transparent 28%,rgba(4,7,14,.1));opacity:1}
|
||||||
|
.office:after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,transparent 0 42%,rgba(0,0,0,.2) 100%);pointer-events:none}
|
||||||
.stage{position:relative;width:720px;height:585px;margin:0;z-index:2}
|
.stage{position:relative;width:720px;height:585px;margin:0;z-index:2}
|
||||||
|
.flow-layer{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:3}
|
||||||
|
.flow-route{
|
||||||
|
fill:none;
|
||||||
|
stroke:rgba(138,124,255,.34);
|
||||||
|
stroke-width:2;
|
||||||
|
stroke-linecap:round;
|
||||||
|
stroke-linejoin:round;
|
||||||
|
stroke-dasharray:5 8;
|
||||||
|
filter:drop-shadow(0 0 6px rgba(138,124,255,.25));
|
||||||
|
}
|
||||||
|
.flow-node{
|
||||||
|
fill:rgba(138,124,255,.18);
|
||||||
|
stroke:rgba(255,255,255,.34);
|
||||||
|
stroke-width:1.2;
|
||||||
|
}
|
||||||
|
.flow-node[data-status="done"]{fill:rgba(53,215,164,.34);stroke:rgba(53,215,164,.8)}
|
||||||
|
.flow-node[data-status="active"]{fill:rgba(138,124,255,.6);stroke:#D8D2FF;filter:drop-shadow(0 0 8px rgba(138,124,255,.72))}
|
||||||
|
.flow-node[data-status="pending"]{fill:rgba(255,255,255,.08);stroke:rgba(255,255,255,.22)}
|
||||||
.wall-window{position:absolute;top:16px;width:86px;height:42px;border:2px solid rgba(215,228,255,.35);border-radius:8px;background:linear-gradient(180deg,rgba(160,208,255,.34),rgba(110,150,210,.08));box-shadow:inset 0 0 0 1px rgba(15,20,31,.55)}
|
.wall-window{position:absolute;top:16px;width:86px;height:42px;border:2px solid rgba(215,228,255,.35);border-radius:8px;background:linear-gradient(180deg,rgba(160,208,255,.34),rgba(110,150,210,.08));box-shadow:inset 0 0 0 1px rgba(15,20,31,.55)}
|
||||||
.wall-window.w1{left:84px}.wall-window.w2{left:550px}
|
.wall-window.w1{left:84px}.wall-window.w2{left:550px}
|
||||||
|
.office.has-art .wall-window{display:none}
|
||||||
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
|
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
|
||||||
.obj{filter:drop-shadow(3px 5px 0 rgba(0,0,0,.28));z-index:4}
|
.obj{filter:drop-shadow(3px 5px 0 rgba(0,0,0,.28));z-index:4}
|
||||||
.desk{width:112px;z-index:5;filter:drop-shadow(4px 7px 0 rgba(0,0,0,.28))}
|
.desk{width:112px;z-index:5;filter:drop-shadow(4px 7px 0 rgba(0,0,0,.28))}
|
||||||
@@ -231,6 +264,32 @@ button,input,select{font:inherit}
|
|||||||
.char[data-agent="support"],.desk[data-agent="support"],.roster-item[data-agent="support"]{--role-color:#94A3B8}
|
.char[data-agent="support"],.desk[data-agent="support"],.roster-item[data-agent="support"]{--role-color:#94A3B8}
|
||||||
.char[data-agent="writer"],.desk[data-agent="writer"],.roster-item[data-agent="writer"]{--role-color:#FBBF24}
|
.char[data-agent="writer"],.desk[data-agent="writer"],.roster-item[data-agent="writer"]{--role-color:#FBBF24}
|
||||||
.desk::after{content:'';position:absolute;inset:-4px;border-radius:10px;border:1px solid transparent;pointer-events:none;transition:border-color .2s ease,box-shadow .2s ease}
|
.desk::after{content:'';position:absolute;inset:-4px;border-radius:10px;border:1px solid transparent;pointer-events:none;transition:border-color .2s ease,box-shadow .2s ease}
|
||||||
|
.desk.pipeline-desk::after{border-color:rgba(255,255,255,.12)}
|
||||||
|
.desk[data-flow="done"]::after{border-color:rgba(53,215,164,.26)}
|
||||||
|
.desk[data-flow="active"]::after{border-color:rgba(138,124,255,.72);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 20px rgba(138,124,255,.34)}
|
||||||
|
.desk[data-flow="pending"]::after{border-color:rgba(255,255,255,.16)}
|
||||||
|
.flow-badge{
|
||||||
|
position:absolute;
|
||||||
|
right:-7px;
|
||||||
|
top:-12px;
|
||||||
|
min-width:28px;
|
||||||
|
height:22px;
|
||||||
|
display:grid;
|
||||||
|
place-items:center;
|
||||||
|
padding:0 7px;
|
||||||
|
border-radius:999px;
|
||||||
|
border:1px solid rgba(255,255,255,.18);
|
||||||
|
background:rgba(8,12,22,.88);
|
||||||
|
color:#E7E2FF;
|
||||||
|
font-size:10px;
|
||||||
|
font-weight:750;
|
||||||
|
letter-spacing:.01em;
|
||||||
|
box-shadow:0 10px 22px rgba(0,0,0,.24);
|
||||||
|
}
|
||||||
|
.desk[data-flow="done"] .flow-badge{color:#B7FFE7;border-color:rgba(53,215,164,.28)}
|
||||||
|
.desk[data-flow="active"] .flow-badge{color:#F2EFFF;border-color:rgba(138,124,255,.5);background:rgba(31,24,66,.92)}
|
||||||
|
.desk.preview::after{border-color:rgba(255,255,255,.28);box-shadow:0 0 0 1px rgba(255,255,255,.04),0 0 18px rgba(255,255,255,.16)}
|
||||||
|
.char.preview::before{content:'';position:absolute;left:18px;top:-14px;width:20px;height:20px;border-radius:50%;border:1px solid rgba(255,255,255,.42);box-shadow:0 0 18px rgba(255,255,255,.18)}
|
||||||
.stage:has(.char.active[data-agent="ceo"]) .desk[data-agent="ceo"]::after,
|
.stage:has(.char.active[data-agent="ceo"]) .desk[data-agent="ceo"]::after,
|
||||||
.stage:has(.char.active[data-agent="planner"]) .desk[data-agent="planner"]::after,
|
.stage:has(.char.active[data-agent="planner"]) .desk[data-agent="planner"]::after,
|
||||||
.stage:has(.char.active[data-agent="researcher"]) .desk[data-agent="researcher"]::after,
|
.stage:has(.char.active[data-agent="researcher"]) .desk[data-agent="researcher"]::after,
|
||||||
@@ -241,7 +300,7 @@ button,input,select{font:inherit}
|
|||||||
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after,
|
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after,
|
||||||
.stage:has(.char.active[data-agent="writer"]) .desk[data-agent="writer"]::after{border-color:var(--role-color);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 18px color-mix(in srgb,var(--role-color) 35%,transparent)}
|
.stage:has(.char.active[data-agent="writer"]) .desk[data-agent="writer"]::after{border-color:var(--role-color);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 18px color-mix(in srgb,var(--role-color) 35%,transparent)}
|
||||||
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
|
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
|
||||||
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.2;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:nowrap}
|
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.35;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:normal}
|
||||||
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
||||||
.brief-card{
|
.brief-card{
|
||||||
padding:14px;
|
padding:14px;
|
||||||
|
|||||||
@@ -5,6 +5,23 @@
|
|||||||
const OFFICE_RUNTIME_JS_TEMPLATE = `
|
const OFFICE_RUNTIME_JS_TEMPLATE = `
|
||||||
<script>(function(){
|
<script>(function(){
|
||||||
const base='\${assets.derivedBase}'; const stage=document.getElementById('stage');
|
const base='\${assets.derivedBase}'; const stage=document.getElementById('stage');
|
||||||
|
const officeEl = stage && stage.closest ? stage.closest('.office') : null;
|
||||||
|
if(officeEl){
|
||||||
|
officeEl.style.setProperty('--office-backdrop', 'url(\"'+base+'/office-backdrop-astra-v2.png\")');
|
||||||
|
officeEl.classList.add('has-art');
|
||||||
|
}
|
||||||
|
let _stageScale = 1;
|
||||||
|
function _fitStage(){
|
||||||
|
const shell = stage && stage.closest ? stage.closest('.office') : null;
|
||||||
|
if(!stage || !shell) return;
|
||||||
|
const sx = Math.max(.62, (shell.clientWidth - 28) / 720);
|
||||||
|
const sy = Math.max(.62, (shell.clientHeight - 28) / 585);
|
||||||
|
_stageScale = Math.min(1, sx, sy);
|
||||||
|
stage.style.transform = 'scale(' + _stageScale + ')';
|
||||||
|
stage.style.transformOrigin = 'center center';
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', _fitStage);
|
||||||
|
setTimeout(_fitStage, 0);
|
||||||
// \u2500\u2500 \uB370\uC774\uD130 \uBAA8\uB378 \u2500\u2500
|
// \u2500\u2500 \uB370\uC774\uD130 \uBAA8\uB378 \u2500\u2500
|
||||||
// stations: \uCC45\uC0C1 + \uCE90\uB9AD\uD130 \uC815\uC758 \uBC30\uC5F4 (let \u2014 \uCD94\uAC00/\uC81C\uAC70 \uAC00\uB2A5).
|
// stations: \uCC45\uC0C1 + \uCE90\uB9AD\uD130 \uC815\uC758 \uBC30\uC5F4 (let \u2014 \uCD94\uAC00/\uC81C\uAC70 \uAC00\uB2A5).
|
||||||
// key = \uC548\uC815\uC801 \uC2DD\uBCC4\uC790 (DOM dataset.role \uB85C\uB3C4 \uC0AC\uC6A9). \uC0AC\uC6A9\uC790\uAC00 \uC0C8\uB85C \uB9CC\uB4E0 \uCC45\uC0C1\uC740 \uC790\uB3D9 \uC0DD\uC131.
|
// key = \uC548\uC815\uC801 \uC2DD\uBCC4\uC790 (DOM dataset.role \uB85C\uB3C4 \uC0AC\uC6A9). \uC0AC\uC6A9\uC790\uAC00 \uC0C8\uB85C \uB9CC\uB4E0 \uCC45\uC0C1\uC740 \uC790\uB3D9 \uC0DD\uC131.
|
||||||
@@ -31,13 +48,17 @@ const DESK_SPRITE_CHOICES=['desk-main','desk-boss','desk-dark-mirror','desk-main
|
|||||||
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uD504\uB78D sprite \uD6C4\uBCF4.
|
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uD504\uB78D sprite \uD6C4\uBCF4.
|
||||||
const PROP_SPRITE_CHOICES=['board','plant-tall','bookshelf','plant-bushy','partition','cooler','filing','couch','rug','shelf','printer','monitor-blue','monitor-black','chair-blue','crt'];
|
const PROP_SPRITE_CHOICES=['board','plant-tall','bookshelf','plant-bushy','partition','cooler','filing','couch','rug','shelf','printer','monitor-blue','monitor-black','chair-blue','crt'];
|
||||||
const DEFAULT_PROPS=[
|
const DEFAULT_PROPS=[
|
||||||
{name:'board',x:316,y:12,w:88},{name:'plant-tall',x:44,y:92,w:42},{name:'bookshelf',x:86,y:70,w:54},
|
{name:'plant-tall',x:40,y:118,w:42},{name:'bookshelf',x:86,y:88,w:54},
|
||||||
{name:'plant-bushy',x:642,y:96,w:42},{name:'partition',x:520,y:208,w:72},{name:'cooler',x:640,y:248,w:38},
|
{name:'plant-bushy',x:640,y:118,w:42},{name:'cooler',x:646,y:286,w:38},
|
||||||
{name:'filing',x:620,y:330,w:42},{name:'couch',x:578,y:432,w:96},{name:'rug',x:560,y:510,w:126},
|
{name:'filing',x:618,y:374,w:42},{name:'couch',x:584,y:452,w:96},
|
||||||
{name:'shelf',x:40,y:504,w:118},{name:'printer',x:520,y:520,w:58},{name:'monitor-blue',x:356,y:56,w:44},
|
{name:'rug',x:560,y:514,w:126},{name:'printer',x:520,y:526,w:58},
|
||||||
];
|
];
|
||||||
|
|
||||||
let stations=[]; // mutable, \uC2DC\uC791 \uC2DC default \uB610\uB294 saved layout \uB85C \uCC44\uC6C0.
|
let stations=[]; // mutable, \uC2DC\uC791 \uC2DC default \uB610\uB294 saved layout \uB85C \uCC44\uC6C0.
|
||||||
|
let _hasSavedLayout=false;
|
||||||
|
let _layoutSource='default';
|
||||||
|
let _lastPipelineLayoutSignature='';
|
||||||
|
let _latestSnapshot=null;
|
||||||
let __nextDeskN=100; // user-add \uCC45\uC0C1 id \uCE74\uC6B4\uD130 (default \uC640 \uCDA9\uB3CC \uD68C\uD53C \uC704\uD574 \uD070 \uC218\uC5D0\uC11C \uC2DC\uC791).
|
let __nextDeskN=100; // user-add \uCC45\uC0C1 id \uCE74\uC6B4\uD130 (default \uC640 \uCDA9\uB3CC \uD68C\uD53C \uC704\uD574 \uD070 \uC218\uC5D0\uC11C \uC2DC\uC791).
|
||||||
let __nextObjN=0;
|
let __nextObjN=0;
|
||||||
const stationByKey={}; // \uBE60\uB978 lookup. stations \uBCC0\uACBD \uC2DC rebuild.
|
const stationByKey={}; // \uBE60\uB978 lookup. stations \uBCC0\uACBD \uC2DC rebuild.
|
||||||
@@ -51,6 +72,193 @@ function _rebuildStationIndex(){
|
|||||||
Object.keys(stationByKey).forEach(k=>delete stationByKey[k]);
|
Object.keys(stationByKey).forEach(k=>delete stationByKey[k]);
|
||||||
stations.forEach(st=>{ stationByKey[st.key]=st; });
|
stations.forEach(st=>{ stationByKey[st.key]=st; });
|
||||||
}
|
}
|
||||||
|
function _safeDeskKey(raw){
|
||||||
|
return 'pipe_' + String(raw || 'agent').toLowerCase().replace(/[^a-z0-9_-]+/g,'_');
|
||||||
|
}
|
||||||
|
function _uniqueAgentsById(list){
|
||||||
|
const seen=new Set(), out=[];
|
||||||
|
for(const item of list||[]){
|
||||||
|
if(!item || !item.agentId || seen.has(item.agentId)) continue;
|
||||||
|
seen.add(item.agentId); out.push(item);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function _inferRoleFromStageLabel(label){
|
||||||
|
const s=String(label||'').toLowerCase();
|
||||||
|
if(/qa|테스트|검증/.test(s)) return 'qa';
|
||||||
|
if(/검토|감리|review/.test(s)) return 'inspector';
|
||||||
|
if(/디자인|ux|ui/.test(s)) return 'designer';
|
||||||
|
if(/시장|트렌드|조사|리서치|research/.test(s)) return 'researcher';
|
||||||
|
if(/설계|개발|구현|배포|deploy|dev/.test(s)) return 'developer';
|
||||||
|
if(/기획|방향|문서|plan/.test(s)) return 'planner';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function _resolvedStageAgentId(stage, roster){
|
||||||
|
const explicit = stage && (stage.agentId || stage.agent);
|
||||||
|
if(explicit) return explicit;
|
||||||
|
const role = _inferRoleFromStageLabel(stage && stage.label);
|
||||||
|
if(!role) return null;
|
||||||
|
const match = (roster||[]).find(r=>r && r.roleCategory===role);
|
||||||
|
return match ? match.agentId : null;
|
||||||
|
}
|
||||||
|
function _pipelineRosterOrder(roster, pipeline){
|
||||||
|
const list=Array.isArray(roster)?roster.slice():[];
|
||||||
|
if(!pipeline || !Array.isArray(pipeline.stages) || !pipeline.stages.length) return list;
|
||||||
|
const byId=new Map(list.map(r=>[r.agentId,r]));
|
||||||
|
const ordered=[];
|
||||||
|
const ceo=byId.get('ceo');
|
||||||
|
if(ceo) ordered.push(ceo);
|
||||||
|
for(const stage of pipeline.stages){
|
||||||
|
const aid=_resolvedStageAgentId(stage,list);
|
||||||
|
const agent=aid && byId.get(aid);
|
||||||
|
if(agent) ordered.push(agent);
|
||||||
|
}
|
||||||
|
for(const agent of list) ordered.push(agent);
|
||||||
|
return _uniqueAgentsById(ordered);
|
||||||
|
}
|
||||||
|
function _stationSpriteFor(slot, roleCategory){
|
||||||
|
if(roleCategory==='ceo') return 'desk-boss';
|
||||||
|
return slot >= 4 ? 'desk-dark-mirror' : (slot % 2 ? 'desk-dark' : 'desk-main');
|
||||||
|
}
|
||||||
|
function _stationSlot(slot){
|
||||||
|
const path=[
|
||||||
|
[72,220,'R'],[214,210,'R'],[356,220,'R'],[498,210,'R'],
|
||||||
|
[498,394,'L'],[356,404,'L'],[214,394,'L'],[72,404,'L'],
|
||||||
|
];
|
||||||
|
if(path[slot]) return path[slot];
|
||||||
|
const extra=slot-path.length;
|
||||||
|
const cols=4;
|
||||||
|
return [54+(extra%cols)*144, 42+Math.floor(extra/cols)*86, extra%2?'L':'R'];
|
||||||
|
}
|
||||||
|
function _makeStationForRosterAgent(agent, slot){
|
||||||
|
if(agent.roleCategory==='ceo' || agent.agentId==='ceo'){
|
||||||
|
return {
|
||||||
|
key:'ceo',agentKey:agent.agentId,label:agent.agentName||'CEO',charRow:_roleCategoryToCharRow(agent.roleCategory),
|
||||||
|
deskSprite:'desk-boss',deskX:292,deskY:82,deskW:136,seatX:319,seatY:113,face:'R',
|
||||||
|
dock:[350,164],roam:[[306,196],[396,196]],boss:true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const [deskX,deskY,face]=_stationSlot(slot);
|
||||||
|
const deskW=112;
|
||||||
|
const seatX=face==='L' ? deskX+48 : deskX+4;
|
||||||
|
const seatY=deskY+36;
|
||||||
|
return {
|
||||||
|
key:_safeDeskKey(agent.agentId),agentKey:agent.agentId,label:agent.agentName||agent.agentId,
|
||||||
|
charRow:_roleCategoryToCharRow(agent.roleCategory),deskSprite:_stationSpriteFor(slot,agent.roleCategory),
|
||||||
|
deskX,deskY,deskW,seatX,seatY,face,boss:false,
|
||||||
|
dock:[deskX+(face==='L'?72:32),deskY+82],
|
||||||
|
roam:[[deskX+18,deskY+116],[deskX+78,deskY+102]],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function _rebuildScene(nextStations, props){
|
||||||
|
_clearStage();
|
||||||
|
stations=(nextStations||[]).map(st=>Object.assign({},st));
|
||||||
|
_rebuildStationIndex();
|
||||||
|
stations.forEach(buildStation);
|
||||||
|
(props||DEFAULT_PROPS).forEach(p=>addImg(p.name,p.x,p.y,p.w));
|
||||||
|
}
|
||||||
|
function _restoreDefaultScene(){
|
||||||
|
_rebuildScene(DEFAULT_STATIONS,DEFAULT_PROPS);
|
||||||
|
_layoutSource='default';
|
||||||
|
_lastPipelineLayoutSignature='';
|
||||||
|
const shell=document.querySelector('.office-shell');
|
||||||
|
if(shell) shell.dataset.layout='default';
|
||||||
|
}
|
||||||
|
function _syncPipelineLayout(pipeline, roster){
|
||||||
|
if(_hasSavedLayout || _editMode) return;
|
||||||
|
const stages=pipeline && Array.isArray(pipeline.stages) ? pipeline.stages : [];
|
||||||
|
if(!stages.length){
|
||||||
|
if(_layoutSource==='pipeline') _restoreDefaultScene();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ordered=_pipelineRosterOrder(roster,pipeline);
|
||||||
|
const signature=ordered.map(r=>r.agentId).join('|')+'::'+stages.map(s=>(s.label||'')+':'+(_resolvedStageAgentId(s,ordered)||'')).join('|');
|
||||||
|
if(_layoutSource==='pipeline' && signature===_lastPipelineLayoutSignature) return;
|
||||||
|
const ceo=ordered.find(r=>r.agentId==='ceo' || r.roleCategory==='ceo');
|
||||||
|
const workers=ordered.filter(r=>r!==ceo);
|
||||||
|
const next=[];
|
||||||
|
if(ceo) next.push(_makeStationForRosterAgent(ceo,0));
|
||||||
|
workers.forEach((agent,idx)=>next.push(_makeStationForRosterAgent(agent,idx)));
|
||||||
|
if(next.length){
|
||||||
|
_rebuildScene(next,DEFAULT_PROPS);
|
||||||
|
_layoutSource='pipeline';
|
||||||
|
_lastPipelineLayoutSignature=signature;
|
||||||
|
const shell=document.querySelector('.office-shell');
|
||||||
|
if(shell) shell.dataset.layout='pipeline';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function _clearPipelineDecor(){
|
||||||
|
Object.values(__deskWrap).forEach(wrap=>{
|
||||||
|
wrap.classList.remove('pipeline-desk');
|
||||||
|
delete wrap.dataset.flow;
|
||||||
|
wrap.querySelectorAll('.flow-badge').forEach(el=>el.remove());
|
||||||
|
});
|
||||||
|
const flow=document.getElementById('flowLayer');
|
||||||
|
if(flow) flow.innerHTML='';
|
||||||
|
}
|
||||||
|
function _deskCenterForAgent(agentId){
|
||||||
|
const st=findStationByAgent(agentId);
|
||||||
|
if(!st) return null;
|
||||||
|
const wrap=__deskWrap[st.key];
|
||||||
|
if(!wrap) return null;
|
||||||
|
const x=parseFloat(wrap.style.left)+(parseFloat(wrap.style.width)||st.deskW||112)/2;
|
||||||
|
const y=parseFloat(wrap.style.top)+34;
|
||||||
|
return {x,y,st,wrap};
|
||||||
|
}
|
||||||
|
function _renderPipelineDecor(pipeline, roster){
|
||||||
|
_clearPipelineDecor();
|
||||||
|
const stages=pipeline && Array.isArray(pipeline.stages) ? pipeline.stages : [];
|
||||||
|
if(!stages.length) return;
|
||||||
|
const stageAgents=stages.map(stg=>({ stage:stg, agentId:_resolvedStageAgentId(stg,roster) })).filter(x=>!!x.agentId);
|
||||||
|
if(!stageAgents.length) return;
|
||||||
|
const meta=new Map();
|
||||||
|
stageAgents.forEach((item,idx)=>{
|
||||||
|
const prev=meta.get(item.agentId)||{first:idx+1,count:0,statuses:[]};
|
||||||
|
prev.count++;
|
||||||
|
prev.statuses.push(item.stage.status||'pending');
|
||||||
|
meta.set(item.agentId,prev);
|
||||||
|
});
|
||||||
|
meta.forEach((m,agentId)=>{
|
||||||
|
const center=_deskCenterForAgent(agentId);
|
||||||
|
if(!center) return;
|
||||||
|
const status=m.statuses.includes('active') ? 'active' : (m.statuses.every(s=>s==='done') ? 'done' : 'pending');
|
||||||
|
center.wrap.classList.add('pipeline-desk');
|
||||||
|
center.wrap.dataset.flow=status;
|
||||||
|
const badge=document.createElement('div');
|
||||||
|
badge.className='flow-badge';
|
||||||
|
badge.textContent=m.count>1 ? (m.first+'×'+m.count) : String(m.first);
|
||||||
|
center.wrap.appendChild(badge);
|
||||||
|
});
|
||||||
|
const compact=[], seenRouteAgents=new Set();
|
||||||
|
for(const item of stageAgents){
|
||||||
|
if(seenRouteAgents.has(item.agentId)) continue;
|
||||||
|
seenRouteAgents.add(item.agentId);
|
||||||
|
compact.push(item);
|
||||||
|
}
|
||||||
|
const pts=compact.map(item=>{
|
||||||
|
const center=_deskCenterForAgent(item.agentId);
|
||||||
|
const m=meta.get(item.agentId);
|
||||||
|
const status=m && m.statuses.includes('active') ? 'active' : (m && m.statuses.every(s=>s==='done') ? 'done' : 'pending');
|
||||||
|
return { ...item, status, center };
|
||||||
|
}).filter(x=>!!x.center);
|
||||||
|
const flow=document.getElementById('flowLayer');
|
||||||
|
if(!flow || pts.length<1) return;
|
||||||
|
if(pts.length>1){
|
||||||
|
const path=document.createElementNS('http://www.w3.org/2000/svg','path');
|
||||||
|
path.setAttribute('class','flow-route');
|
||||||
|
path.setAttribute('d',pts.map((p,i)=>(i?'L':'M')+p.center.x+' '+p.center.y).join(' '));
|
||||||
|
flow.appendChild(path);
|
||||||
|
}
|
||||||
|
pts.forEach((p)=>{
|
||||||
|
const c=document.createElementNS('http://www.w3.org/2000/svg','circle');
|
||||||
|
c.setAttribute('class','flow-node');
|
||||||
|
c.setAttribute('cx',String(p.center.x));
|
||||||
|
c.setAttribute('cy',String(p.center.y));
|
||||||
|
c.setAttribute('r',p.status==='active'?'7':'5');
|
||||||
|
c.dataset.status=p.status||'pending';
|
||||||
|
flow.appendChild(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
// agentKey \u2192 station.key \uB85C \uB77C\uC6B0\uD305. roleMap \uC758 \uB3D9\uC801 \uBC84\uC804.
|
// agentKey \u2192 station.key \uB85C \uB77C\uC6B0\uD305. roleMap \uC758 \uB3D9\uC801 \uBC84\uC804.
|
||||||
function findStationByAgent(agentId){
|
function findStationByAgent(agentId){
|
||||||
if(!agentId) return null;
|
if(!agentId) return null;
|
||||||
@@ -375,6 +583,8 @@ function apply(s){
|
|||||||
_lastState = s; // D. 컨텍스트 메뉴 / 세부보기에서 사용.
|
_lastState = s; // D. 컨텍스트 메뉴 / 세부보기에서 사용.
|
||||||
const st = s?.status || 'idle';
|
const st = s?.status || 'idle';
|
||||||
const meta = _statusMeta(st);
|
const meta = _statusMeta(st);
|
||||||
|
const officeShell = document.querySelector('.office-shell');
|
||||||
|
if(officeShell) officeShell.dataset.status = st;
|
||||||
// 정적 갱신은 *항상* — 헤더/태스크/단계/로그/프로그레스.
|
// 정적 갱신은 *항상* — 헤더/태스크/단계/로그/프로그레스.
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const phasePill = document.getElementById('phasePill');
|
const phasePill = document.getElementById('phasePill');
|
||||||
@@ -436,7 +646,7 @@ function apply(s){
|
|||||||
dot.dataset.status = stg.status || 'pending';
|
dot.dataset.status = stg.status || 'pending';
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.className = 'mm-label';
|
label.className = 'mm-label';
|
||||||
label.textContent = (i+1) + '. ' + (stg.label || '단계') + (stg.agent ? ' · ' + stg.agent : '');
|
label.textContent = (i+1) + '. ' + (stg.label || '단계') + ((stg.agentId || stg.agent) ? ' · ' + (stg.agentId || stg.agent) : '');
|
||||||
dot.appendChild(label);
|
dot.appendChild(label);
|
||||||
mm.appendChild(dot);
|
mm.appendChild(dot);
|
||||||
if(i < stages.length - 1){
|
if(i < stages.length - 1){
|
||||||
@@ -452,6 +662,7 @@ function apply(s){
|
|||||||
} else {
|
} else {
|
||||||
mm.style.display = 'none';
|
mm.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
setTimeout(_fitStage, 0);
|
||||||
// 활성 캐릭터 결정. roleMap 은 agentKey → 실제 존재하는 station.key 로 lookup
|
// 활성 캐릭터 결정. roleMap 은 agentKey → 실제 존재하는 station.key 로 lookup
|
||||||
// 하므로, 매핑된 책상이 없으면 null. 사용자가 default ceo 책상을 지워도 안전.
|
// 하므로, 매핑된 책상이 없으면 null. 사용자가 default ceo 책상을 지워도 안전.
|
||||||
let role = null;
|
let role = null;
|
||||||
@@ -652,6 +863,8 @@ function _renderRoster(roster, activeAgentId){
|
|||||||
const count = document.getElementById('rosterCount');
|
const count = document.getElementById('rosterCount');
|
||||||
if(!wrap || !count) return;
|
if(!wrap || !count) return;
|
||||||
const list = Array.isArray(roster) ? roster : [];
|
const list = Array.isArray(roster) ? roster : [];
|
||||||
|
const officeShell = document.querySelector('.office-shell');
|
||||||
|
if(officeShell) officeShell.dataset.rosterEmpty = list.length ? 'false' : 'true';
|
||||||
count.textContent = String(list.length);
|
count.textContent = String(list.length);
|
||||||
if(!list.length){
|
if(!list.length){
|
||||||
wrap.innerHTML = '<div class="roster-item"><div class="roster-copy"><strong>등록된 팀이 없습니다</strong><span class="roster-meta">회사 모드를 켜면 라인업이 표시됩니다.</span></div></div>';
|
wrap.innerHTML = '<div class="roster-item"><div class="roster-copy"><strong>등록된 팀이 없습니다</strong><span class="roster-meta">회사 모드를 켜면 라인업이 표시됩니다.</span></div></div>';
|
||||||
@@ -669,6 +882,20 @@ function _renderRoster(roster, activeAgentId){
|
|||||||
'</div>'+
|
'</div>'+
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
wrap.querySelectorAll('.roster-item[data-agent]').forEach((item)=>{
|
||||||
|
const id = item.dataset.agent;
|
||||||
|
item.addEventListener('mouseenter', ()=>_previewAgent(id, true, item));
|
||||||
|
item.addEventListener('mouseleave', ()=>_previewAgent(id, false, item));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function _previewAgent(agentId, on, item){
|
||||||
|
if(item) item.classList.toggle('preview', !!on);
|
||||||
|
const st = findStationByAgent(agentId);
|
||||||
|
if(!st) return;
|
||||||
|
const desk = __deskWrap[st.key];
|
||||||
|
const ch = chars[st.key];
|
||||||
|
if(desk) desk.classList.toggle('preview', !!on);
|
||||||
|
if(ch) ch.classList.toggle('preview', !!on);
|
||||||
}
|
}
|
||||||
// refactor #G-full — roster 에 있는 agent 중 desk 가 없는 경우 자동 생성.
|
// refactor #G-full — roster 에 있는 agent 중 desk 가 없는 경우 자동 생성.
|
||||||
// 한 번 처리된 agentId 는 _autoDeskedFor 에 기록 → 사용자가 그 desk 를 지워도 재생성 안 함.
|
// 한 번 처리된 agentId 는 _autoDeskedFor 에 기록 → 사용자가 그 desk 를 지워도 재생성 안 함.
|
||||||
@@ -735,20 +962,31 @@ function _ensureRosterDesks(roster){
|
|||||||
|
|
||||||
function applyFromSnapshot(snap){
|
function applyFromSnapshot(snap){
|
||||||
if(!snap) return;
|
if(!snap) return;
|
||||||
|
_latestSnapshot = snap;
|
||||||
const roster = Array.isArray(snap.roster) ? snap.roster : [];
|
const roster = Array.isArray(snap.roster) ? snap.roster : [];
|
||||||
_renderRoster(roster, snap.activeAgentId);
|
const orderedRoster = _pipelineRosterOrder(roster, snap.pipeline);
|
||||||
_ensureRosterDesks(roster);
|
_renderRoster(orderedRoster, snap.activeAgentId);
|
||||||
|
_syncPipelineLayout(snap.pipeline, orderedRoster);
|
||||||
|
if(_layoutSource!=='pipeline' || _hasSavedLayout) _ensureRosterDesks(orderedRoster);
|
||||||
|
_renderPipelineDecor(snap.pipeline, orderedRoster);
|
||||||
const active = (snap.activeAgentId && roster.find(a => a.agentId === snap.activeAgentId)) || roster[0];
|
const active = (snap.activeAgentId && roster.find(a => a.agentId === snap.activeAgentId)) || roster[0];
|
||||||
|
const activeStage = snap.pipeline && Array.isArray(snap.pipeline.stages)
|
||||||
|
? snap.pipeline.stages.find(stg => stg.status === 'active')
|
||||||
|
: null;
|
||||||
|
const stageCount = snap.pipeline && Array.isArray(snap.pipeline.stages) ? snap.pipeline.stages.length : 0;
|
||||||
|
const doneCount = snap.pipeline && Array.isArray(snap.pipeline.stages)
|
||||||
|
? snap.pipeline.stages.filter(stg => stg.status === 'done').length
|
||||||
|
: 0;
|
||||||
const synthetic = {
|
const synthetic = {
|
||||||
agentId: snap.activeAgentId || (active && active.agentId) || 'main',
|
agentId: snap.activeAgentId || (active && active.agentId) || 'main',
|
||||||
agentName: (active && active.agentName) || 'Agent',
|
agentName: (active && active.agentName) || 'Agent',
|
||||||
status: (active && active.status) || _phaseToStatus(snap.phase),
|
status: (active && active.status) || _phaseToStatus(snap.phase),
|
||||||
currentTask: snap.task && snap.task.goal,
|
currentTask: snap.task && snap.task.goal,
|
||||||
currentStep: active && active.currentStep,
|
currentStep: (active && active.currentStep) || (activeStage && activeStage.label),
|
||||||
// apply() 의 message regex 가 첫 토큰을 agentId 로 추출 — activeAgentId 그대로 넘김.
|
// apply() 의 message regex 가 첫 토큰을 agentId 로 추출 — activeAgentId 그대로 넘김.
|
||||||
message: snap.activeAgentId || '',
|
message: snap.activeAgentId || '',
|
||||||
recentLogs: (active && active.lastLog) ? [active.lastLog] : [],
|
recentLogs: (active && active.lastLog) ? [active.lastLog] : [],
|
||||||
progress: snap.pipeline ? (snap.pipeline.index / Math.max(1, snap.pipeline.stages.length)) : 0,
|
progress: stageCount ? (doneCount / stageCount) : 0,
|
||||||
pipelineStages: snap.pipeline && snap.pipeline.stages,
|
pipelineStages: snap.pipeline && snap.pipeline.stages,
|
||||||
needUserInput: (snap.awaiting && snap.awaiting.kind === 'clarification') ? snap.awaiting.questions : undefined,
|
needUserInput: (snap.awaiting && snap.awaiting.kind === 'clarification') ? snap.awaiting.questions : undefined,
|
||||||
awaitingApproval: (snap.awaiting && snap.awaiting.kind === 'approval') ? snap.awaiting.questions[0] : undefined,
|
awaitingApproval: (snap.awaiting && snap.awaiting.kind === 'approval') ? snap.awaiting.questions[0] : undefined,
|
||||||
@@ -865,6 +1103,7 @@ function _clearStage(){
|
|||||||
// refactor #G-full: layout reset 시 auto-desk 결정 기록도 초기화 →
|
// refactor #G-full: layout reset 시 auto-desk 결정 기록도 초기화 →
|
||||||
// 다음 snapshot 도착 시 roster 기반으로 다시 평가.
|
// 다음 snapshot 도착 시 roster 기반으로 다시 평가.
|
||||||
_autoDeskedFor.clear();
|
_autoDeskedFor.clear();
|
||||||
|
_clearPipelineDecor();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _applyRot(el, rot){
|
function _applyRot(el, rot){
|
||||||
@@ -1216,8 +1455,8 @@ stage.addEventListener('mousedown', e=>{
|
|||||||
const rect = stage.getBoundingClientRect();
|
const rect = stage.getBoundingClientRect();
|
||||||
const tx = parseFloat(target.style.left)||0;
|
const tx = parseFloat(target.style.left)||0;
|
||||||
const ty = parseFloat(target.style.top)||0;
|
const ty = parseFloat(target.style.top)||0;
|
||||||
_dragDX = e.clientX - rect.left - tx;
|
_dragDX = ((e.clientX - rect.left) / _stageScale) - tx;
|
||||||
_dragDY = e.clientY - rect.top - ty;
|
_dragDY = ((e.clientY - rect.top) / _stageScale) - ty;
|
||||||
target.classList.add('dragging');
|
target.classList.add('dragging');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1252,8 +1491,8 @@ document.addEventListener('keydown', e=>{
|
|||||||
document.addEventListener('mousemove', e=>{
|
document.addEventListener('mousemove', e=>{
|
||||||
if(!_editMode || !_drag) return;
|
if(!_editMode || !_drag) return;
|
||||||
const rect = stage.getBoundingClientRect();
|
const rect = stage.getBoundingClientRect();
|
||||||
let x = e.clientX - rect.left - _dragDX;
|
let x = ((e.clientX - rect.left) / _stageScale) - _dragDX;
|
||||||
let y = e.clientY - rect.top - _dragDY;
|
let y = ((e.clientY - rect.top) / _stageScale) - _dragDY;
|
||||||
// 4px 격자 snap
|
// 4px 격자 snap
|
||||||
x = Math.round(x/4)*4;
|
x = Math.round(x/4)*4;
|
||||||
y = Math.round(y/4)*4;
|
y = Math.round(y/4)*4;
|
||||||
@@ -1331,9 +1570,20 @@ window.addEventListener('message', e=>{
|
|||||||
const d = e.data;
|
const d = e.data;
|
||||||
if(!d || typeof d !== 'object') return;
|
if(!d || typeof d !== 'object') return;
|
||||||
if(d.type === 'pixelOfficeLayoutLoaded'){
|
if(d.type === 'pixelOfficeLayoutLoaded'){
|
||||||
if(d.value) _restoreLayout(d.value);
|
_hasSavedLayout = !!d.value;
|
||||||
|
if(d.value) {
|
||||||
|
_layoutSource = 'saved';
|
||||||
|
_restoreLayout(d.value);
|
||||||
|
} else if(_latestSnapshot) {
|
||||||
|
_syncPipelineLayout(_latestSnapshot.pipeline, _pipelineRosterOrder(_latestSnapshot.roster || [], _latestSnapshot.pipeline));
|
||||||
|
_renderPipelineDecor(_latestSnapshot.pipeline, _latestSnapshot.roster || []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(d.type === 'pixelOfficeLayoutSaved'){
|
if(d.type === 'pixelOfficeLayoutSaved'){
|
||||||
|
if(d.value && !d.value.reset) {
|
||||||
|
_hasSavedLayout = true;
|
||||||
|
_layoutSource = 'saved';
|
||||||
|
}
|
||||||
if(d.value && d.value.reset){
|
if(d.value && d.value.reset){
|
||||||
// 디폴트로 리셋된 경우 — 페이지를 재로딩해서 코드 기본값으로 복귀.
|
// 디폴트로 리셋된 경우 — 페이지를 재로딩해서 코드 기본값으로 복귀.
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const TELEGRAM_TOKEN_SECRET_KEY = 'g1nation.telegram.botToken';
|
|||||||
|
|
||||||
export interface SettingsPanelDeps {
|
export interface SettingsPanelDeps {
|
||||||
extensionUri: vscode.Uri;
|
extensionUri: vscode.Uri;
|
||||||
|
/** Used by features that store data in workspaceState / globalState (Google OAuth tokens etc). */
|
||||||
|
context: vscode.ExtensionContext;
|
||||||
secrets: vscode.SecretStorage;
|
secrets: vscode.SecretStorage;
|
||||||
/** Returns the live Telegram client so we can call getMe for "test connection". */
|
/** Returns the live Telegram client so we can call getMe for "test connection". */
|
||||||
telegramClient: ITelegramClient;
|
telegramClient: ITelegramClient;
|
||||||
@@ -81,6 +83,21 @@ interface SettingsState {
|
|||||||
maxAutoSteps: number;
|
maxAutoSteps: number;
|
||||||
maxContextSize: number;
|
maxContextSize: number;
|
||||||
};
|
};
|
||||||
|
google: {
|
||||||
|
clientId: string;
|
||||||
|
/** secret 자체는 client 에 echo 안 함 — *설정 여부* 만. true 면 input placeholder 가 "저장됨" 으로 바뀜. */
|
||||||
|
hasClientSecret: boolean;
|
||||||
|
calendarId: string;
|
||||||
|
defaultEventDurationMinutes: number;
|
||||||
|
/** iCal URL 도 capability 토큰성이라 *설정 여부* 만 전송. 사용자가 새 값 입력 시 덮어씀. */
|
||||||
|
hasIcalUrl: boolean;
|
||||||
|
icalDaysAhead: number;
|
||||||
|
/** OAuth 연결 상태 — globalState 의 refresh token 존재 여부 + 누가 연결됐는지. */
|
||||||
|
connected: boolean;
|
||||||
|
connectedAs?: string;
|
||||||
|
connectedAt?: string;
|
||||||
|
lastIcalFetchAt?: string;
|
||||||
|
};
|
||||||
/** Sectional banner shown when config.update fails (e.g. reload required). */
|
/** Sectional banner shown when config.update fails (e.g. reload required). */
|
||||||
bannerError?: string;
|
bannerError?: string;
|
||||||
}
|
}
|
||||||
@@ -208,6 +225,19 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
case 'advanced.update':
|
case 'advanced.update':
|
||||||
await this._handleAdvancedUpdate(msg);
|
await this._handleAdvancedUpdate(msg);
|
||||||
return;
|
return;
|
||||||
|
case 'google.update':
|
||||||
|
await this._handleGoogleUpdate(msg);
|
||||||
|
return;
|
||||||
|
case 'google.connect':
|
||||||
|
await vscode.commands.executeCommand('g1nation.calendar.connectOAuth');
|
||||||
|
await this._refreshState();
|
||||||
|
return;
|
||||||
|
case 'google.disconnect':
|
||||||
|
await this._handleGoogleDisconnect();
|
||||||
|
return;
|
||||||
|
case 'google.icalRefresh':
|
||||||
|
await this._handleGoogleIcalRefresh();
|
||||||
|
return;
|
||||||
case 'openVscodeSettings':
|
case 'openVscodeSettings':
|
||||||
await vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation');
|
await vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation');
|
||||||
return;
|
return;
|
||||||
@@ -403,6 +433,82 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────── Google (Calendar + Sheets) ──────────────
|
||||||
|
// Settings 패널이 보여주는 모든 Google 필드는 `g1nation.google.*` configuration 으로
|
||||||
|
// 저장. secret(Client Secret / iCal URL / refresh token)은 *값 자체를 client 에 echo
|
||||||
|
// 안 함* — 설정 여부만 true/false 로. 사용자가 새 값을 입력 시 덮어쓰는 단방향.
|
||||||
|
|
||||||
|
private _buildGoogleState(): SettingsState['google'] {
|
||||||
|
const ctx = this._deps.context;
|
||||||
|
const { readCalendarConfig } = require('../calendar') as typeof import('../calendar');
|
||||||
|
const cur = readCalendarConfig(ctx);
|
||||||
|
return {
|
||||||
|
clientId: cur.clientId ?? '',
|
||||||
|
hasClientSecret: !!cur.clientSecret,
|
||||||
|
calendarId: cur.calendarId ?? 'primary',
|
||||||
|
defaultEventDurationMinutes: cur.defaultDurationMinutes ?? 60,
|
||||||
|
hasIcalUrl: !!cur.icalUrl,
|
||||||
|
icalDaysAhead: cur.daysAhead ?? 14,
|
||||||
|
connected: !!cur.refreshToken,
|
||||||
|
connectedAs: cur.connectedAs,
|
||||||
|
connectedAt: cur.connectedAt,
|
||||||
|
lastIcalFetchAt: cur.lastFetchAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleGoogleUpdate(msg: any): Promise<void> {
|
||||||
|
const { writeCalendarConfig } = require('../calendar') as typeof import('../calendar');
|
||||||
|
// 필드별로 *명시적으로 전달된 것만* 패치. undefined / 누락은 무시.
|
||||||
|
const patch: any = {};
|
||||||
|
if (typeof msg.clientId === 'string') patch.clientId = msg.clientId.trim() || undefined;
|
||||||
|
if (typeof msg.clientSecret === 'string') patch.clientSecret = msg.clientSecret.trim() || undefined;
|
||||||
|
if (typeof msg.calendarId === 'string') patch.calendarId = msg.calendarId.trim() || 'primary';
|
||||||
|
if (typeof msg.defaultEventDurationMinutes === 'number' && Number.isFinite(msg.defaultEventDurationMinutes)) {
|
||||||
|
patch.defaultDurationMinutes = Math.max(5, Math.min(720, Math.floor(msg.defaultEventDurationMinutes)));
|
||||||
|
}
|
||||||
|
if (typeof msg.icalUrl === 'string') patch.icalUrl = msg.icalUrl.trim();
|
||||||
|
if (typeof msg.icalDaysAhead === 'number' && Number.isFinite(msg.icalDaysAhead)) {
|
||||||
|
patch.daysAhead = Math.max(1, Math.min(90, Math.floor(msg.icalDaysAhead)));
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length === 0) return;
|
||||||
|
await writeCalendarConfig(this._deps.context, patch);
|
||||||
|
this._lastSuccess = '저장되었습니다.';
|
||||||
|
this._lastError = undefined;
|
||||||
|
await this._refreshState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleGoogleDisconnect(): Promise<void> {
|
||||||
|
const { writeCalendarConfig } = require('../calendar') as typeof import('../calendar');
|
||||||
|
await writeCalendarConfig(this._deps.context, {
|
||||||
|
refreshToken: undefined,
|
||||||
|
accessToken: undefined,
|
||||||
|
accessTokenExpiresAt: undefined,
|
||||||
|
connectedAs: undefined,
|
||||||
|
connectedAt: undefined,
|
||||||
|
});
|
||||||
|
this._lastSuccess = 'OAuth 연결을 해제했습니다. https://myaccount.google.com/permissions 에서 권한 회수 권장.';
|
||||||
|
this._lastError = undefined;
|
||||||
|
await this._refreshState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleGoogleIcalRefresh(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { refreshCalendarCache } = require('../calendar') as typeof import('../calendar');
|
||||||
|
const r = await refreshCalendarCache(this._deps.context);
|
||||||
|
if (r.ok) {
|
||||||
|
this._lastSuccess = `iCal 새로고침 완료 — ${r.count}개 일정 동기화`;
|
||||||
|
this._lastError = undefined;
|
||||||
|
} else {
|
||||||
|
this._lastError = r.error || 'iCal 새로고침 실패';
|
||||||
|
this._lastSuccess = undefined;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this._lastError = e?.message ?? String(e);
|
||||||
|
this._lastSuccess = undefined;
|
||||||
|
}
|
||||||
|
await this._refreshState();
|
||||||
|
}
|
||||||
|
|
||||||
private async _handleAdvancedUpdate(msg: any): Promise<void> {
|
private async _handleAdvancedUpdate(msg: any): Promise<void> {
|
||||||
if (typeof msg.dryRun === 'boolean') {
|
if (typeof msg.dryRun === 'boolean') {
|
||||||
await this._safeConfigUpdate('dryRun', msg.dryRun);
|
await this._safeConfigUpdate('dryRun', msg.dryRun);
|
||||||
@@ -466,6 +572,7 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50) ?? 50,
|
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50) ?? 50,
|
||||||
maxContextSize: cfg.get<number>('maxContextSize', 32000) ?? 32000,
|
maxContextSize: cfg.get<number>('maxContextSize', 32000) ?? 32000,
|
||||||
},
|
},
|
||||||
|
google: this._buildGoogleState(),
|
||||||
bannerError: this._bannerError,
|
bannerError: this._bannerError,
|
||||||
};
|
};
|
||||||
const payload = { type: 'state', value: state };
|
const payload = { type: 'state', value: state };
|
||||||
|
|||||||
Reference in New Issue
Block a user