v2.2.13: Pixel Office Interactive Editor & Core Refinement

This commit is contained in:
g1nation
2026-05-16 13:18:49 +09:00
parent dea106ce68
commit c4f01fd6af
11 changed files with 1030 additions and 655 deletions
+30 -30
View File
@@ -3,20 +3,20 @@
<!-- ASTRA:AUTO-START -->
## Snapshot
- **Workspace**: `connectai` `v2.2.11` _(absolute path varies by environment; resolved from the active VS Code workspace)_
- **Workspace**: `ConnectAI` `v2.2.12` _(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.
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
- **Stats**: 224 source files, ~45,895 lines across 5 top-level modules.
- **Stats**: 224 source files, ~45,925 lines across 5 top-level modules.
## Last Refresh
- **Time**: 2026-05-15T11:45:54.886Z
- **Files newly analysed**: 2
- **Files reused from cache**: 222
- **Time**: 2026-05-16T03:37:49.405Z
- **Files newly analysed**: 0
- **Files reused from cache**: 224
## Directory Map
```mermaid
mindmap
root((connectai))
root((ConnectAI))
src/
features/
core/
@@ -64,14 +64,14 @@ flowchart LR
## Modules
### `src/` — 110 files, ~30,899 lines
### `src/` — 110 files, ~30,926 lines
**Sub-directories**
- `src/features/` (37) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때
- `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토
- `src/memory/` (8) — Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. "왜 이렇게 결정했는지", "어떤 흐름으로 진행했는지" 기록. 저장
- `src/retrieval/` (8) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-I
- `src/docs/` (6) — Bug: Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts ...
- `src/docs/` (6) — src Chronicle Records
- `src/lib/` (6) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변
- `src/integrations/` (4) — Per-chat conversation history for the Telegram bot. Why this exists: the previous bot was stateless — every inbound mess
- `src/lmstudio/` (4) — 4 files (.ts)
@@ -87,12 +87,12 @@ flowchart LR
- `src/core/services.ts` (164 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/sidebarProvider.ts` (5083 lines)
- `src/sidebarProvider.ts` (5119 lines)
- `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/skills/agentKnowledgeMap.ts` (374 lines)
- `src/retrieval/lessonHelpers.ts` (325 lines) — Lesson / Experience Memory — pure helpers (no vscode dependency) "Lesson" = a markdown file in the active brain that captures a past mistake/risk and how to avoid repeating it. Identified by a lessons
- `src/agent.ts` (3269 lines)
- `src/agent.ts` (3260 lines)
- `src/lib/engine.ts` (906 lines)
- `src/features/company/dispatcher.ts` (1419 lines) — Sequential dispatcher for 1인 기업 모드. Drives one company "turn": user prompt → CEO planner (JSON {brief, tasks}) → for each task in plan: dispatch one specialist (sequentially) - build specialist prompt
- `src/features/approval/approvalQueue.ts` (129 lines)
@@ -107,11 +107,11 @@ flowchart LR
- `src/features/company/intentAlignment.ts` (334 lines) — Intent Alignment — 사용자의 자연어 요청을 실행 가능한 작업 조건으로 변환. 사용자는 자기 의도와 배경지식이 에이전트에게 충분히 전달되었다고 착각하는 경향이 있다 (투명성의 착각·지식의 저주·공통 기반 부족). 그래서 에이전트가 즉시 작업에 돌입하면 사용자가 머릿속에 가진 것과 다른 결과를 만들어 낸다. 이 모듈은 그 격차를 메꾸는 한 단계
- `src/features/projectChronicle/types.ts` (118 lines)
### `media/` — 6 files, ~6,763 lines
### `media/` — 6 files, ~6,766 lines
**Key files**
- `media/sidebar.css` (1990 lines) — Stylesheet
- `media/sidebar.js` (3598 lines)
- `media/sidebar.css` (1986 lines) — Stylesheet
- `media/sidebar.js` (3605 lines)
- `media/sidebar.html` (531 lines) — Astra
- `media/settings-panel.css` (210 lines) — Stylesheet
- `media/settings-panel.html` (164 lines) — Astra Settings
@@ -163,8 +163,8 @@ flowchart LR
### `docs/` — 75 files, ~2,886 lines
**Sub-directories**
- `docs/records/` (63) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
- `docs/docs/` (5) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...
- `docs/records/` (63) — Astra Project Chronicle Records
- `docs/docs/` (5) — docs Chronicle Records
**Key files**
- `docs/TELEGRAM_REMOTE_EXECUTION_PLAN.md` (452 lines) — Telegram Remote Execution 기획서
@@ -174,24 +174,24 @@ flowchart LR
- `docs/records/ConnectAI/development/2026-05-03_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
- `docs/records/ConnectAI/timeline.md` (140 lines) — Project Timeline
- `docs/Advanced_Features_Implementation_Guide.md` (40 lines) — Advanced Features Implementation Guide
- `docs/PROJECT_CHRONICLE_GUARD_ROADMAP.md` (43 lines) — Project Chronicle Guard: Search Engine Roadmap
- `docs/UX_UI_Consistency_Guidelines.md` (44 lines) — UX/UI Consistency Guidelines
- `docs/docs/records/docs/README.md` (18 lines) — docs Chronicle Records
- `docs/docs/records/docs/bugs/BUG-0001-viewed-integration-retrieval-test-ts-1-59-integration-retrie.md` (16 lines) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...
- `docs/docs/records/docs/chronicle.config.json` (11 lines) — JSON configuration
- `docs/docs/records/docs/project-profile.md` (31 lines) — Project Profile
- `docs/docs/records/docs/README.md` (18 lines) — docs Chronicle Records
- `docs/docs/records/docs/timeline.md` (7 lines) — Project Timeline
- `docs/PROJECT_CHRONICLE_GUARD_ROADMAP.md` (43 lines) — Project Chronicle Guard: Search Engine Roadmap
- `docs/records/ConnectAI/bugs/BUG-0001-volumes-data-project-antigravity-connectai-프로젝트-코드-리뷰-해줄-수-있.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
- `docs/records/ConnectAI/bugs/BUG-0002-지금-내가-분석-요청하고-너가-답을-줄때-아래-템플릿에-맞춰-답을-써주고-있는데-개선-포인트가-있는지-확인해.md` (16 lines) — Bug: 지금 내가 분석 요청하고 너가 답을 줄때 아래 템플릿에 맞춰 답을 써주고 있는데, 개선 포인트가 있는지 확인해줘. ## 내가 보는 위험 가장 큰...
- `docs/records/ConnectAI/bugs/BUG-0003-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0004-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0005-다시한번-답줘-volumes-data-project-antigravity-connectai-내-질문에-대한-.md` (16 lines) — Bug: 다시한번 답줘. /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는...
- `docs/records/ConnectAI/bugs/BUG-0006-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0007-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0009-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
- `docs/records/ConnectAI/bugs/BUG-0010-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
- `docs/records/ConnectAI/bugs/BUG-0011-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
- `docs/records/ConnectAI/bugs/BUG-0012-질문이-있어-논문을-쓰려고해-논문-주제는-서비스적이-아닌-사용자가-ai에게-구조로-질문을-해야-사용자의-의도.md` (16 lines) — Bug: 질문이 있어. 논문을 쓰려고해. 논문 주제는 서비스적이 아닌 사용자가 ai에게 구조로 질문을 해야 사용자의 의도에 맞는 답변을 받을 수 있을까야...
- `docs/records/ConnectAI/README.md` (18 lines) — Astra Project Chronicle Records
- `docs/records/ConnectAI/bugs/BUG-0001-volumes-data-project-antigravity-connectai-프로젝트-코드-리뷰-해줄-수-있.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
- `docs/records/ConnectAI/bugs/BUG-0002-지금-내가-분석-요청하고-너가-답을-줄때-아래-템플릿에-맞춰-답을-써주고-있는데-개선-포인트가-있는지-확인해.md` (16 lines) — Bug: 지금 내가 분석 요청하고 너가 답을 줄때 아래 템플릿에 맞춰 답을 써주고 있는데, 개선 포인트가 있는지 확인해줘. ## 내가 보는 위험 가장 큰...
- `docs/records/ConnectAI/bugs/BUG-0003-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0004-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0005-다시한번-답줘-volumes-data-project-antigravity-connectai-내-질문에-대한-.md` (16 lines) — Bug: 다시한번 답줘. /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는...
- `docs/records/ConnectAI/bugs/BUG-0006-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0007-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
- `docs/records/ConnectAI/bugs/BUG-0009-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
- `docs/records/ConnectAI/bugs/BUG-0010-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
## VS Code Extension Surface
- **Extension ID**: `g1nation.astra`
@@ -319,7 +319,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한
**Designed for High-Performance Decision Making.**
Copyright (C) **g1nation**. All rights reserved.
_Last auto-scan: 2026-05-15T11:45:54.886Z · signature `ac525c98`_
_Last auto-scan: 2026-05-16T03:37:49.405Z · signature `2e3db319`_
<!-- ASTRA:AUTO-END -->
## Purpose
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1778845577946,
"createdAt": 1778902731275,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1778845577944,
"createdAt": 1778902731267,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"createdAt": 1778845577941,
"createdAt": 1778902731263,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "---\nid: stress_conflict_1778845577925\ndate: 2026-05-15T11:46:17.947Z\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]** 전략 수립 중... (15ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (2ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (3ms)\n",
"createdAt": 1778845577948,
"result": "---\nid: stress_conflict_1778902731247\ndate: 2026-05-16T03:38:51.279Z\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": 1778902731279,
"modelVersion": "unknown"
}
@@ -1,8 +1,8 @@
{
"missionId": "stress_conflict_1778845577925",
"missionId": "stress_conflict_1778902731247",
"status": "completed",
"startTime": "2026-05-15T11:46:17.925Z",
"totalElapsedMs": 24,
"startTime": "2026-05-16T03:38:51.247Z",
"totalElapsedMs": 33,
"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": 15,
"durationMs": 11,
"message": "전략 수립 중...",
"ts": "2026-05-15T11:46:17.940Z"
"ts": "2026-05-16T03:38:51.258Z"
},
{
"from": "planner",
"to": "researcher",
"durationMs": 2,
"durationMs": 5,
"message": "핵심 정보 수집 및 분석 중...",
"ts": "2026-05-15T11:46:17.942Z"
"ts": "2026-05-16T03:38:51.263Z"
},
{
"from": "researcher",
"to": "writer",
"durationMs": 3,
"durationMs": 8,
"message": "최종 리포트 작성 및 편집 중...",
"ts": "2026-05-15T11:46:17.945Z"
"ts": "2026-05-16T03:38:51.271Z"
},
{
"from": "writer",
"to": "completed",
"durationMs": 4,
"durationMs": 9,
"message": "미션 완료",
"ts": "2026-05-15T11:46:17.949Z"
"ts": "2026-05-16T03:38:51.280Z"
}
],
"resilienceMetrics": {
+11
View File
@@ -1,5 +1,16 @@
# Astra Patch Notes
## v2.2.13 (2026-05-16)
### 🏗️ Pixel Office Interactive Editor & Core Refinement
- **픽셀 오피스 편집 모드 도입:** 책상 추가/삭제, 에이전트 매핑 변경, 가구(프랍) 배치 및 크기 조절이 가능한 인터랙티브 편집 기능을 추가했습니다.
- **속성 패널(Property Panel) 구현:** 선택한 팀원이나 가구의 상세 속성(라벨, 방향, 스프라이트 종류 등)을 실시간으로 수정할 수 있는 UI를 탑재했습니다.
- **프로젝트 컨텍스트 동기화 최적화:** 로컬 환경에 맞게 `chronicle.config.json`의 경로 정보를 최신화하고 프로젝트 인지 능력을 안정화했습니다.
- **신규 패키징:** `astra-2.2.13.vsix` 패키지를 통해 더욱 강력해진 픽셀 오피스 커스터마이징 기능을 제공합니다.
---
## v2.2.12 (2026-05-16)
### 🚀 Sync & Visual Polish Expansion
- **픽셀 오피스(Pixel Office) 자산 및 레이아웃 확장:** 다양한 방향의 캐릭터 애니메이션과 사무실 레이아웃 프리뷰 자산을 대폭 확충하여 시각적 피드백의 완성도를 높였습니다.
+5 -5
View File
@@ -1,11 +1,11 @@
{
"projectId": "connectai",
"projectName": "connectai",
"projectRoot": "E:\\Wiki\\connectai",
"recordRoot": "E:\\Wiki\\connectai\\docs\\records\\connectai",
"projectName": "ConnectAI",
"projectRoot": "/Volumes/Data/project/Antigravity/ConnectAI",
"recordRoot": "/Volumes/Data/project/Antigravity/ConnectAI/docs/records/ConnectAI",
"description": "Auto-created by Project Architecture activation.",
"corePurpose": "",
"detailLevel": "standard",
"createdAt": "2026-05-14T00:57:32.245Z",
"updatedAt": "2026-05-15T11:48:58.185Z"
"createdAt": "2026-05-13T13:09:33.788Z",
"updatedAt": "2026-05-16T03:39:49.319Z"
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "astra",
"displayName": "Astra",
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
"version": "2.2.12",
"version": "2.2.13",
"publisher": "g1nation",
"license": "MIT",
"icon": "assets/icon.png",
+408 -44
View File
@@ -3873,8 +3873,6 @@ header{display:flex;justify-content:space-between;align-items:center;padding:10p
.office:before{content:'';position:absolute;left:0;right:0;top:16%;bottom:0;background-image:linear-gradient(rgba(255,255,255,.028) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.028) 1px,transparent 1px);background-size:48px 48px}
.office:after{content:'';position:absolute;left:0;right:0;top:15.5%;height:8px;background:linear-gradient(180deg,rgba(0,0,0,.36),transparent)}
.stage{position:relative;width:720px;height:585px;margin:0}
.zone{position:absolute;border:1px solid rgba(255,255,255,.05);background:rgba(255,255,255,.018);box-shadow:inset 0 1px 0 rgba(255,255,255,.035);border-radius:10px}
.zone.exec{left:220px;top:18px;width:280px;height:150px;background:rgba(124,131,255,.06)}.zone.core{left:28px;top:175px;width:520px;height:170px}.zone.ops{left:28px;top:355px;width:520px;height:170px}.zone.lounge{left:560px;top:355px;width:132px;height:170px;background:rgba(255,189,89,.045)}
.wall-window{position:absolute;top:16px;width:86px;height:42px;border:3px solid rgba(206,223,255,.35);background:linear-gradient(180deg,rgba(160,208,255,.3),rgba(110,150,210,.1));box-shadow:inset 0 0 0 2px rgba(15,20,31,.55)}
.wall-window.w1{left:84px}.wall-window.w2{left:550px}
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
@@ -3885,26 +3883,26 @@ header{display:flex;justify-content:space-between;align-items:center;padding:10p
@keyframes po-pulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.6}}
/* ── C. 직군별 페르소나 컬러 ── 책상 outline 가벼운 강조, 활성 캐릭터 위 점이 직군색.
data-role attribute로 자동 매핑. 사용자가 PNG sprite로 swap해도 컬러는 유지. */
.char[data-role="ceo"],.desk[data-role="ceo"] {--role-color:#A78BFA}
.char[data-role="planner"],.desk[data-role="planner"] {--role-color:#60A5FA}
.char[data-role="researcher"],.desk[data-role="researcher"] {--role-color:#10B981}
.char[data-role="designer"],.desk[data-role="designer"] {--role-color:#F472B6}
.char[data-role="developer"],.desk[data-role="developer"] {--role-color:#FBBF24}
.char[data-role="qa"],.desk[data-role="qa"] {--role-color:#22D3EE}
.char[data-role="inspector"],.desk[data-role="inspector"] {--role-color:#FB923C}
.char[data-role="support"],.desk[data-role="support"] {--role-color:#94A3B8}
.char[data-agent="ceo"],.desk[data-agent="ceo"] {--role-color:#A78BFA}
.char[data-agent="planner"],.desk[data-agent="planner"] {--role-color:#60A5FA}
.char[data-agent="researcher"],.desk[data-agent="researcher"] {--role-color:#10B981}
.char[data-agent="designer"],.desk[data-agent="designer"] {--role-color:#F472B6}
.char[data-agent="developer"],.desk[data-agent="developer"] {--role-color:#FBBF24}
.char[data-agent="qa"],.desk[data-agent="qa"] {--role-color:#22D3EE}
.char[data-agent="inspector"],.desk[data-agent="inspector"] {--role-color:#FB923C}
.char[data-agent="support"],.desk[data-agent="support"] {--role-color:#94A3B8}
.char.active::after{content:'';position:absolute;left:0;right:0;bottom:-4px;height:3px;background:var(--role-color,var(--accent));box-shadow:0 0 8px var(--role-color,var(--accent));border-radius:2px;animation:po-glow 1.6s ease-in-out infinite}
@keyframes po-glow{0%,100%{opacity:.7}50%{opacity:1}}
.desk{position:relative}
.desk::after{content:'';position:absolute;inset:-2px;border-radius:4px;border:2px solid transparent;pointer-events:none;transition:border-color .3s}
.char.active ~ .desk[data-role],.stage:has(.char.active[data-role="ceo"]) .desk[data-role="ceo"]::after,
.stage:has(.char.active[data-role="planner"]) .desk[data-role="planner"]::after,
.stage:has(.char.active[data-role="researcher"]) .desk[data-role="researcher"]::after,
.stage:has(.char.active[data-role="designer"]) .desk[data-role="designer"]::after,
.stage:has(.char.active[data-role="developer"]) .desk[data-role="developer"]::after,
.stage:has(.char.active[data-role="qa"]) .desk[data-role="qa"]::after,
.stage:has(.char.active[data-role="inspector"]) .desk[data-role="inspector"]::after,
.stage:has(.char.active[data-role="support"]) .desk[data-role="support"]::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="researcher"]) .desk[data-agent="researcher"]::after,
.stage:has(.char.active[data-agent="designer"]) .desk[data-agent="designer"]::after,
.stage:has(.char.active[data-agent="developer"]) .desk[data-agent="developer"]::after,
.stage:has(.char.active[data-agent="qa"]) .desk[data-agent="qa"]::after,
.stage:has(.char.active[data-agent="inspector"]) .desk[data-agent="inspector"]::after,
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after
{border-color:var(--role-color)}
.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%);background:#fff;color:#222;padding:5px 8px;border-radius:8px;font-size:11px;box-shadow:2px 2px 0 rgba(0,0,0,.35);white-space:nowrap}
@@ -3949,7 +3947,29 @@ body:not([data-edit-mode="true"]) .char{cursor:pointer}
.ctx-detail dt{color:#94A3B8;font-weight:600;white-space:nowrap}
.ctx-detail dd{margin:0;color:#F1F4FB;overflow-wrap:anywhere}
.ctx-detail .cd-logs{margin-top:10px;padding:6px 8px;background:rgba(0,0,0,.3);border-radius:4px;font-family:ui-monospace,monospace;font-size:10.5px;max-height:120px;overflow-y:auto}
.edit-toolbar{display:flex;gap:8px;align-items:center;padding:6px 16px;background:rgba(99,102,241,.18);border-bottom:1px solid rgba(99,102,241,.4);font-size:11px}.edit-toolbar .et-hint{flex:1;color:#D7DBEA}.edit-toolbar button{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.25);color:#F1F4FB;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px}.edit-toolbar button:hover{background:rgba(99,102,241,.35)}
.edit-toolbar{display:flex;gap:8px;align-items:center;padding:6px 16px;background:rgba(99,102,241,.18);border-bottom:1px solid rgba(99,102,241,.4);font-size:11px;flex-wrap:wrap}.edit-toolbar .et-hint{flex:1;color:#D7DBEA;min-width:160px}.edit-toolbar button{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.25);color:#F1F4FB;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px}.edit-toolbar button:hover{background:rgba(99,102,241,.35)}
.edit-toolbar button.add{background:rgba(16,185,129,.22);border-color:rgba(16,185,129,.55)}.edit-toolbar button.add:hover{background:rgba(16,185,129,.4)}
.edit-toolbar button.del{background:rgba(239,68,68,.22);border-color:rgba(239,68,68,.55)}.edit-toolbar button.del:hover{background:rgba(239,68,68,.4)}.edit-toolbar button[disabled]{opacity:.4;cursor:not-allowed}
/* 선택된 item 의 속성 편집 패널 — 우측 슬라이드 */
.prop-panel{position:absolute;right:12px;top:90px;width:240px;background:#13162A;border:1px solid #2A2E3F;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.6);padding:12px;font-size:11px;color:#F1F4FB;z-index:25;display:none}
.prop-panel.show{display:block}
.prop-panel h4{margin:0 0 8px;font-size:12px;color:#A78BFA;text-transform:uppercase;letter-spacing:.04em}
.prop-panel .pp-row{margin-bottom:8px}
.prop-panel label{display:block;font-size:10px;color:#94A3B8;margin-bottom:2px}
.prop-panel select,.prop-panel input{width:100%;background:#0c1020;color:#F1F4FB;border:1px solid #2A2E3F;border-radius:4px;padding:3px 6px;font-size:11px}
.prop-panel .pp-thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin-top:4px}
.prop-panel .pp-thumb{width:100%;aspect-ratio:1/1;background:#0c1020;border:1px solid #2A2E3F;border-radius:3px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:2px}
.prop-panel .pp-thumb img{max-width:100%;max-height:100%;image-rendering:pixelated}
.prop-panel .pp-thumb.active{border-color:#A78BFA;box-shadow:0 0 0 2px rgba(167,139,250,.35)}
/* 프랍 추가 picker — 모달 grid */
.prop-picker{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1100;display:flex;align-items:center;justify-content:center}
.prop-picker-box{background:#13162A;border:1px solid #2A2E3F;border-radius:10px;padding:14px;max-width:520px;max-height:80vh;overflow-y:auto;color:#F1F4FB}
.prop-picker-box h3{margin:0 0 10px;font-size:13px;color:#A78BFA}
.prop-picker-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
.prop-pick{background:#0c1020;border:1px solid #2A2E3F;border-radius:4px;padding:6px;cursor:pointer;text-align:center}
.prop-pick:hover{border-color:#A78BFA}
.prop-pick img{max-width:60px;max-height:60px;image-rendering:pixelated}
.prop-pick .pp-name{font-size:10px;color:#94A3B8;margin-top:4px;word-break:break-all}
/* 편집 모드 — 드래그 가능 요소 강조 */
body[data-edit-mode="true"] .stage{background-image:linear-gradient(rgba(99,102,241,.15) 1px,transparent 1px),linear-gradient(90deg,rgba(99,102,241,.15) 1px,transparent 1px);background-size:32px 32px}
body[data-edit-mode="true"] .desk,body[data-edit-mode="true"] .char,body[data-edit-mode="true"] .obj{cursor:grab;outline:1px dashed rgba(99,102,241,.5)}
@@ -3963,7 +3983,10 @@ footer{padding:8px 16px 12px;border-top:1px solid rgba(255,255,255,.08);backgrou
<header><div><div class="h-title">🏢 ASTRA OFFICE</div><div class="h-sub" id="agent">Astra</div></div><div style="display:flex;gap:8px;align-items:center;"><button id="editBtn" class="edit-btn" title="배치 편집 모드 토글">✏️ 편집</button><div class="status" id="status">idle</div></div></header>
<div id="miniMap" class="mini-map" style="display:none;"></div>
<div id="editToolbar" class="edit-toolbar" style="display:none;">
<span class="et-hint">드래그로 이동 · <b>R</b> 회전(Shift+R 반시계) · <b>]</b> 위 / <b>[</b> 아래 · 4px snap</span>
<span class="et-hint">드래그로 이동 · <b>R</b> 회전 · <b>]</b>/<b>[</b> 레이어 · 4px snap</span>
<button id="addDeskBtn" class="add" title="책상 추가">+ 책상</button>
<button id="addPropBtn" class="add" title="프랍(소품) 추가">+ 프랍</button>
<button id="deleteSelBtn" class="del" title="선택 항목 삭제" disabled>🗑 삭제</button>
<button id="layerUpBtn" title="레이어 위로 (])">⬆</button>
<button id="layerDownBtn" title="레이어 아래로 ([)">⬇</button>
<button id="saveBtn">💾 저장</button>
@@ -3971,29 +3994,118 @@ footer{padding:8px 16px 12px;border-top:1px solid rgba(255,255,255,.08);backgrou
<button id="cancelBtn" title="저장 안 하고 종료">✕ 취소</button>
</div>
<div class="strip"><span><b>작업</b> <span id="task">—</span></span><span><b>단계</b> <span id="step">—</span></span></div>
<main class="office"><div class="stage" id="stage"><div class="zone exec"></div><div class="zone core"></div><div class="zone ops"></div><div class="zone lounge"></div><div class="wall-window w1"></div><div class="wall-window w2"></div></div></main>
<main class="office"><div class="stage" id="stage"><div class="wall-window w1"></div><div class="wall-window w2"></div></div><div id="propPanel" class="prop-panel"></div></main>
<div id="ticker" class="ticker" style="display:none;"><div class="tk-track" id="tickerTrack"></div></div>
<footer><div class="progress"><div class="bar" id="bar"></div></div><div id="log">—</div></footer>
<script>(function(){
const base='${assets.derivedBase}'; const stage=document.getElementById('stage');
const stations=[
{key:'ceo',label:'CEO',row:0,desk:'desk-boss',deskX:304,deskY:84,deskW:136,seatX:331,seatY:115,face:'R',dock:[362,164],roam:[[320,196],[396,196]],boss:true},
{key:'planner',label:'\uAE30\uD68D',row:1,desk:'desk-main',deskX:60,deskY:228,deskW:112,seatX:64,seatY:264,face:'R',dock:[96,322],roam:[[154,350],[200,330]]},
{key:'researcher',label:'\uB9AC\uC11C\uCE58',row:2,desk:'desk-dark-mirror',deskX:236,deskY:214,deskW:112,seatX:284,seatY:248,face:'L',dock:[304,310],roam:[[322,340],[344,322]]},
{key:'designer',label:'\uB514\uC790\uC778',row:3,desk:'desk-main',deskX:402,deskY:240,deskW:112,seatX:406,seatY:276,face:'R',dock:[438,334],roam:[[492,350],[520,326]]},
{key:'developer',label:'\uAC1C\uBC1C',row:4,desk:'desk-dark-mirror',deskX:56,deskY:410,deskW:112,seatX:104,seatY:442,face:'L',dock:[124,500],roam:[[150,534],[188,518]]},
{key:'qa',label:'QA',row:5,desk:'desk-main',deskX:232,deskY:394,deskW:112,seatX:236,seatY:430,face:'R',dock:[268,486],roam:[[320,520],[352,500]]},
{key:'inspector',label:'\uAC10\uB9AC',row:6,desk:'desk-dark-mirror',deskX:408,deskY:420,deskW:112,seatX:456,seatY:452,face:'L',dock:[476,506],roam:[[506,532],[540,502]]},
{key:'support',label:'\uC9C0\uC6D0',row:7,desk:'desk-main',deskX:220,deskY:472,deskW:112,seatX:224,seatY:504,face:'R',dock:[256,548],roam:[[360,548],[484,540]]},
// \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).
// 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.
// agentKey = \uC774 \uCC45\uC0C1\uC5D0 \uB9E4\uD551\uB41C \uC5D0\uC774\uC804\uD2B8 ID (ceo/planner/researcher/...). \uC5C6\uC73C\uBA74 idle.
// charRow = \uC0AC\uC6A9\uD560 sprite row (0~7). \uCE90\uB9AD\uD130 \uC678\uBAA8/\uBC29\uD5A5.
// deskSprite= \uCC45\uC0C1 PNG \uC774\uB984 (desk-main / desk-boss / desk-dark-mirror \uB4F1).
// objs: \uD504\uB78D \uC815\uC758 \uBC30\uC5F4 (let). user-add/remove.
const DEFAULT_STATIONS=[
{key:'ceo',agentKey:'ceo',label:'CEO',charRow:0,deskSprite:'desk-boss',deskX:304,deskY:84,deskW:136,seatX:331,seatY:115,face:'R',dock:[362,164],roam:[[320,196],[396,196]],boss:true},
{key:'planner',agentKey:'planner',label:'\uAE30\uD68D',charRow:1,deskSprite:'desk-main',deskX:60,deskY:228,deskW:112,seatX:64,seatY:264,face:'R',dock:[96,322],roam:[[154,350],[200,330]]},
{key:'researcher',agentKey:'researcher',label:'\uB9AC\uC11C\uCE58',charRow:2,deskSprite:'desk-dark-mirror',deskX:236,deskY:214,deskW:112,seatX:284,seatY:248,face:'L',dock:[304,310],roam:[[322,340],[344,322]]},
{key:'designer',agentKey:'designer',label:'\uB514\uC790\uC778',charRow:3,deskSprite:'desk-main',deskX:402,deskY:240,deskW:112,seatX:406,seatY:276,face:'R',dock:[438,334],roam:[[492,350],[520,326]]},
{key:'developer',agentKey:'developer',label:'\uAC1C\uBC1C',charRow:4,deskSprite:'desk-dark-mirror',deskX:56,deskY:410,deskW:112,seatX:104,seatY:442,face:'L',dock:[124,500],roam:[[150,534],[188,518]]},
{key:'qa',agentKey:'qa',label:'QA',charRow:5,deskSprite:'desk-main',deskX:232,deskY:394,deskW:112,seatX:236,seatY:430,face:'R',dock:[268,486],roam:[[320,520],[352,500]]},
{key:'inspector',agentKey:'inspector',label:'\uAC10\uB9AC',charRow:6,deskSprite:'desk-dark-mirror',deskX:408,deskY:420,deskW:112,seatX:456,seatY:452,face:'L',dock:[476,506],roam:[[506,532],[540,502]]},
{key:'support',agentKey:'support',label:'\uC9C0\uC6D0',charRow:7,deskSprite:'desk-main',deskX:220,deskY:472,deskW:112,seatX:224,seatY:504,face:'R',dock:[256,548],roam:[[360,548],[484,540]]},
];
const roleMap={ceo:'ceo',writer:'planner',researcher:'researcher',designer:'designer',editor:'designer',developer:'developer',qa:'qa',inspector:'inspector',secretary:'support',business:'inspector'};
const stationByKey={}; stations.forEach(st=>stationByKey[st.key]=st);
// \uC5D0\uC774\uC804\uD2B8 ID alias \u2014 \uAC19\uC740 \uD398\uB974\uC18C\uB098\uC758 \uB2E4\uC591\uD55C \uD638\uCE6D\uC744 \uAC19\uC740 book agentKey \uB85C. (writer\u2192planner, editor\u2192designer, secretary\u2192support, business\u2192inspector)
const AGENT_ALIASES={writer:'planner',editor:'designer',secretary:'support',business:'inspector'};
// \uB9E4\uD551 dropdown \uC5D0 \uBCF4\uC5EC\uC904 \uC5D0\uC774\uC804\uD2B8 \uD6C4\uBCF4. agentKey \uAC00 unique \uD55C base set.
const AGENT_CHOICES=['','ceo','planner','researcher','designer','developer','qa','inspector','support'];
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uCC45\uC0C1 sprite \uD6C4\uBCF4 (assets/pixelOffice/derived \uC5D0 \uC788\uB294 desk-* PNG).
const DESK_SPRITE_CHOICES=['desk-main','desk-boss','desk-dark-mirror','desk-main-mirror','desk-dark','desk-boss-mirror','desk-main-front','desk-dark-front','desk-boss-front','desk-partition'];
// \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 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-bushy',x:642,y:96,w:42},{name:'partition',x:520,y:208,w:72},{name:'cooler',x:640,y:248,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:'shelf',x:40,y:504,w:118},{name:'printer',x:520,y:520,w:58},{name:'monitor-blue',x:356,y:56,w:44},
];
let stations=[]; // mutable, \uC2DC\uC791 \uC2DC default \uB610\uB294 saved layout \uB85C \uCC44\uC6C0.
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;
const stationByKey={}; // \uBE60\uB978 lookup. stations \uBCC0\uACBD \uC2DC rebuild.
const __deskWrap={}; // role \u2192 desk DOM wrap.
const chars={}; // role \u2192 char DOM.
const anim={}; // role \u2192 animation state.
function png(name){return base+'/'+name+'.png'}
let __objN=0;function addImg(name,x,y,w,cls='obj'){const i=document.createElement('img');i.src=png(name);i.className=cls;i.dataset.objId='obj_'+(__objN++);i.dataset.objName=name;if(w)i.dataset.objW=w;i.style.left=x+'px';i.style.top=y+'px';if(w)i.style.width=w+'px';stage.appendChild(i);return i}
addImg('board',316,12,88); addImg('plant-tall',44,92,42); addImg('bookshelf',86,70,54); addImg('plant-bushy',642,96,42); addImg('partition',520,208,72); addImg('cooler',640,248,38); addImg('filing',620,330,42); addImg('couch',578,432,96); addImg('rug',560,510,126); addImg('shelf',40,504,118); addImg('printer',520,520,58); addImg('monitor-blue',356,56,44);
const __deskWrap={};function addDesk(st){const wrap=document.createElement('div');wrap.className='desk '+(st.boss?'boss':'');wrap.dataset.role=st.key;wrap.style.left=st.deskX+'px';wrap.style.top=st.deskY+'px';wrap.style.width=st.deskW+'px';const img=document.createElement('img');img.src=png(st.desk);img.style.width='100%';wrap.appendChild(img);const l=document.createElement('div');l.className='label';l.textContent=st.label;wrap.appendChild(l);stage.appendChild(wrap);__deskWrap[st.key]=wrap}
stations.forEach(addDesk);
const chars={}, anim={}; stations.forEach((st)=>{const ch=document.createElement('div');ch.className='char';ch.dataset.role=st.key;ch.style.left=st.seatX+'px';ch.style.top=st.seatY+'px';ch.dataset.homeX=st.seatX;ch.dataset.homeY=st.seatY;ch.dataset.row=st.row;ch.innerHTML='<img><div class="shadow"></div>';stage.appendChild(ch);chars[st.key]=ch;anim[st.key]={row:st.row,frame:0,dir:0,mode:'sit',face:st.face,route:0};const img=ch.querySelector('img');img.src=png('idle-r'+st.row+'-f0');img.style.transform=st.face==='R'?'scaleX(-1)':'none';});
function _rebuildStationIndex(){
Object.keys(stationByKey).forEach(k=>delete stationByKey[k]);
stations.forEach(st=>{ stationByKey[st.key]=st; });
}
// agentKey \u2192 station.key \uB85C \uB77C\uC6B0\uD305. roleMap \uC758 \uB3D9\uC801 \uBC84\uC804.
function findStationByAgent(agentId){
if(!agentId) return null;
const a = AGENT_ALIASES[agentId] || agentId;
for(const st of stations){ if(st.agentKey === a) return st; }
return null;
}
// \uC61B \uCF54\uB4DC \uD638\uD658 \u2014 roleMap[x] \uD638\uCD9C \uD328\uD134\uC744 \uD568\uC218\uB85C.
const roleMap=new Proxy({},{get:(_,k)=>{ const s=findStationByAgent(k); return s?s.key:null; }});
function addImg(name,x,y,w){
const i=document.createElement('img');
i.src=png(name); i.className='obj';
i.dataset.objId='obj_'+(__nextObjN++); i.dataset.objName=name;
if(w!=null) i.dataset.objW=w;
i.style.left=x+'px'; i.style.top=y+'px';
if(w!=null) i.style.width=w+'px';
stage.appendChild(i); return i;
}
function addDesk(st){
const wrap=document.createElement('div');
wrap.className='desk '+(st.boss?'boss':'');
wrap.dataset.role=st.key;
if(st.agentKey) wrap.dataset.agent=st.agentKey;
wrap.style.left=st.deskX+'px'; wrap.style.top=st.deskY+'px'; wrap.style.width=st.deskW+'px';
const img=document.createElement('img'); img.src=png(st.deskSprite); img.style.width='100%';
wrap.appendChild(img);
const l=document.createElement('div'); l.className='label'; l.textContent=st.label;
wrap.appendChild(l);
stage.appendChild(wrap);
__deskWrap[st.key]=wrap;
return wrap;
}
function addChar(st){
const ch=document.createElement('div');
ch.className='char'; ch.dataset.role=st.key;
if(st.agentKey) ch.dataset.agent=st.agentKey;
ch.style.left=st.seatX+'px'; ch.style.top=st.seatY+'px';
ch.dataset.homeX=st.seatX; ch.dataset.homeY=st.seatY; ch.dataset.row=st.charRow;
ch.innerHTML='<img><div class="shadow"></div>';
stage.appendChild(ch);
chars[st.key]=ch;
anim[st.key]={row:st.charRow,frame:0,dir:0,mode:'sit',face:st.face,route:0};
const img=ch.querySelector('img');
img.src=png('idle-r'+st.charRow+'-f0');
img.style.transform=st.face==='R'?'scaleX(-1)':'none';
return ch;
}
function buildStation(st){ addDesk(st); addChar(st); }
function _renderDefaultStations(){
// default 8 stations + default props.
stations = DEFAULT_STATIONS.map(s=>Object.assign({},s));
_rebuildStationIndex();
stations.forEach(buildStation);
DEFAULT_PROPS.forEach(p=>addImg(p.name,p.x,p.y,p.w));
}
_renderDefaultStations();
// ── 4방향 dir 매핑 ──
// PNG 파일명 규약: walk-r<row>-d<DIR>-f<frame>.png
// 사용자의 sprite 컨벤션이 다르면 이 4 상수만 수정하면 됨.
@@ -4186,12 +4298,13 @@ function apply(s){
} else {
mm.style.display = 'none';
}
// 활성 캐릭터 결정.
// 활성 캐릭터 결정. roleMap 은 agentKey → 실제 존재하는 station.key 로 lookup
// 하므로, 매핑된 책상이 없으면 null. 사용자가 default ceo 책상을 지워도 안전.
let role = null;
const m = (s?.message || '').match(/^([a-z0-9_-]+)/i);
if(m) role = roleMap[m[1]] || null;
if(['planning','analyzing','waiting_approval'].includes(st)) role = 'ceo';
if(st === 'reviewing') role = 'inspector';
if(['planning','analyzing','waiting_approval'].includes(st)) role = roleMap['ceo'] || role;
if(st === 'reviewing') role = roleMap['inspector'] || role;
// 활성 outline은 *항상* 즉시 반영 (시뮬레이션 없이도 누가 일하는지 보임).
activate(role);
// 시뮬레이션은 *status 전환 + critical 진입* 에서만.
@@ -4224,7 +4337,7 @@ function apply(s){
if(st === 'done'){
// 완료 — 모두 자리 복귀 후 단체 sit.
Object.keys(chars).forEach(k => sendHome(k, 'work'));
bubble('ceo', '완료!');
const ceoRole = roleMap['ceo']; if(ceoRole) bubble(ceoRole, '완료!');
setTimeout(() => Object.keys(chars).forEach(k => sendHome(k, 'sit')), 1800);
}
}
@@ -4373,10 +4486,21 @@ try{vscode.postMessage({type:'getPixelOfficeState'})}catch{}
let _editMode=false, _drag=null, _dragDX=0, _dragDY=0, _snapshotBeforeEdit=null, _selected=null;
function _snapshotLayout(){
// 현재 화면의 모든 좌표를 캡쳐 — 취소(Cancel) 시 복원용. 회전(rot) 도 포함.
// 새 schema v2: cells 가 단순 좌표 패치가 아니라 stations 전체 정의를 담는다.
// (책상 추가/제거가 가능하니 default 와의 delta 로는 표현 불가.) v1 호환을 위해
// _restoreLayout 이 양쪽 모두 처리.
return {
schema: 2,
cells: stations.map(st=>({
roleKey: st.key,
agentKey: st.agentKey || '',
label: st.label || '',
charRow: st.charRow ?? 0,
deskSprite: st.deskSprite || 'desk-main',
face: st.face || 'R',
boss: !!st.boss,
dock: st.dock,
roam: st.roam,
deskX: parseFloat(__deskWrap[st.key].style.left),
deskY: parseFloat(__deskWrap[st.key].style.top),
deskW: parseFloat(__deskWrap[st.key].style.width),
@@ -4399,6 +4523,15 @@ function _snapshotLayout(){
};
}
// stage 에 그려진 모든 desk/char/obj DOM 제거 (벽 창문 제외). 레이아웃 리빌드 직전에 호출.
function _clearStage(){
Array.from(stage.querySelectorAll('.desk,.char,img.obj')).forEach(el=>el.remove());
Object.keys(__deskWrap).forEach(k=>delete __deskWrap[k]);
Object.keys(chars).forEach(k=>delete chars[k]);
Object.keys(anim).forEach(k=>delete anim[k]);
stations = [];
}
function _applyRot(el, rot){
if(!el) return;
const r = typeof rot==='number' ? rot : 0;
@@ -4411,8 +4544,50 @@ function _applyZ(el, z){
el.dataset.z = String(z);
el.style.zIndex = z === 0 ? '' : String(z);
}
// v2 schema 또는 cell 에 desk 정의 필드가 있으면 stage 를 통째로 재구축.
// v1 (옛 포맷) 이면 좌표만 패치하는 in-place 갱신.
function _isV2Snap(snap){
if(!snap || !Array.isArray(snap.cells)) return false;
if(snap.schema === 2) return true;
return snap.cells.some(c => c && (typeof c.deskSprite === 'string' || typeof c.agentKey === 'string' || typeof c.charRow === 'number'));
}
function _restoreLayout(snap){
if(!snap) return;
if(_isV2Snap(snap)){
_clearStage();
(snap.cells||[]).forEach(c=>{
const st = {
key: c.roleKey,
agentKey: c.agentKey || '',
label: c.label || c.roleKey,
charRow: typeof c.charRow === 'number' ? c.charRow : 0,
deskSprite: c.deskSprite || 'desk-main',
face: c.face || 'R',
boss: !!c.boss,
dock: Array.isArray(c.dock) ? c.dock : [c.deskX+32, c.deskY+80],
roam: Array.isArray(c.roam) ? c.roam : [[c.deskX, c.deskY+120],[c.deskX+60, c.deskY+100]],
deskX: c.deskX, deskY: c.deskY, deskW: c.deskW || 112,
seatX: c.seatX, seatY: c.seatY,
};
stations.push(st);
const m = String(st.key).match(/^desk_(\\d+)$/);
if(m){ const n = parseInt(m[1],10); if(n >= __nextDeskN) __nextDeskN = n + 1; }
});
_rebuildStationIndex();
stations.forEach(buildStation);
(snap.cells||[]).forEach(c=>{
const wrap=__deskWrap[c.roleKey], ch=chars[c.roleKey];
if(wrap){ _applyRot(wrap, c.deskRot); _applyZ(wrap, c.deskZ); }
if(ch){ _applyRot(ch, c.charRot); _applyZ(ch, c.charZ); }
});
(snap.objs||[]).forEach(o=>{
const el = addImg(o.name, o.x, o.y, o.w);
if(o.id){ el.dataset.objId = o.id; }
_applyRot(el, o.rot); _applyZ(el, o.z);
});
return;
}
// v1 — 옛 포맷, in-place patch.
(snap.cells||[]).forEach(c=>{
const wrap=__deskWrap[c.roleKey];
if(wrap){
@@ -4429,7 +4604,6 @@ function _restoreLayout(snap){
_applyRot(ch, c.charRot);
_applyZ(ch, c.charZ);
}
// stations 배열 자체도 갱신 — sendHome 등이 이걸 참조.
const st=stationByKey[c.roleKey];
if(st){
if(typeof c.deskX==='number') st.deskX=c.deskX;
@@ -4466,6 +4640,185 @@ function _setEdit(on){
// 편집 종료 시 selected 강조 해제.
if(_selected){ _selected.classList.remove('selected'); _selected = null; }
}
_onSelectionChanged();
}
// ── 선택 변화 시 호출 — 속성 패널 / 삭제 버튼 상태 sync ──
function _onSelectionChanged(){
const panel = document.getElementById('propPanel');
const delBtn = document.getElementById('deleteSelBtn');
if(!_editMode || !_selected){
panel.classList.remove('show'); panel.innerHTML='';
if(delBtn) delBtn.disabled = true;
return;
}
// 캐릭터를 선택해도 그 책상의 속성을 편집한다 (캐릭터와 책상은 한 쌍).
let targetForProps = _selected;
if(targetForProps.classList.contains('char')){
const role = Object.keys(chars).find(k=>chars[k]===targetForProps);
if(role && __deskWrap[role]) targetForProps = __deskWrap[role];
}
if(targetForProps.classList.contains('desk')){
if(delBtn) delBtn.disabled = false;
_renderDeskProps(targetForProps);
} else if(_selected.classList.contains('obj')){
if(delBtn) delBtn.disabled = false;
_renderObjProps(_selected);
} else {
panel.classList.remove('show'); panel.innerHTML='';
if(delBtn) delBtn.disabled = true;
}
}
function _renderDeskProps(deskEl){
const role = deskEl.dataset.role;
const st = stationByKey[role]; if(!st) return;
const panel = document.getElementById('propPanel');
panel.classList.add('show');
// 에이전트 매핑 dropdown.
const agentOpts = AGENT_CHOICES.map(a=>'<option value="'+a+'"'+(a===(st.agentKey||'')?' selected':'')+'>'+(a||'(매핑 없음)')+'</option>').join('');
// 책상 sprite picker.
const deskOpts = DESK_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===st.deskSprite?' selected':'')+'>'+s+'</option>').join('');
// charRow 썸네일 picker (idle-r<n>-f0.png).
let thumbs='';
for(let r=0;r<8;r++){
thumbs += '<div class="pp-thumb'+(r===st.charRow?' active':'')+'" data-charrow="'+r+'" title="row '+r+'"><img src="'+png('idle-r'+r+'-f0')+'"></div>';
}
panel.innerHTML =
'<h4>책상 속성</h4>'+
'<div class="pp-row"><label>라벨</label><input id="ppLabel" value="'+(st.label||'').replace(/"/g,'&quot;')+'"></div>'+
'<div class="pp-row"><label>에이전트 매핑</label><select id="ppAgent">'+agentOpts+'</select></div>'+
'<div class="pp-row"><label>책상 sprite</label><select id="ppDesk">'+deskOpts+'</select></div>'+
'<div class="pp-row"><label>착석 캐릭터 (row 0~7)</label><div class="pp-thumbs" id="ppThumbs">'+thumbs+'</div></div>'+
'<div class="pp-row"><label>방향 (앉은 face)</label><select id="ppFace"><option value="L"'+(st.face==='L'?' selected':'')+'>← Left</option><option value="R"'+(st.face==='R'?' selected':'')+'>Right →</option></select></div>';
// 핸들러
panel.querySelector('#ppLabel').oninput = (ev)=>{
st.label = ev.target.value;
const lbl = deskEl.querySelector('.label'); if(lbl) lbl.textContent = st.label;
};
panel.querySelector('#ppAgent').onchange = (ev)=>{
st.agentKey = ev.target.value || '';
// CSS data-role 색깔은 agentKey 기준 — 매핑 변경 시 swap.
if(st.agentKey){ deskEl.dataset.agent = st.agentKey; if(chars[role]) chars[role].dataset.agent = st.agentKey; }
else { delete deskEl.dataset.agent; if(chars[role]) delete chars[role].dataset.agent; }
};
panel.querySelector('#ppDesk').onchange = (ev)=>{
st.deskSprite = ev.target.value;
const img = deskEl.querySelector('img'); if(img) img.src = png(st.deskSprite);
};
panel.querySelectorAll('.pp-thumb').forEach(t=>{
t.onclick = ()=>{
const r = parseInt(t.dataset.charrow,10);
st.charRow = r;
if(anim[role]){ anim[role].row = r; }
const ch = chars[role]; if(ch){ ch.dataset.row = r; const img=ch.querySelector('img'); if(img) img.src = png('idle-r'+r+'-f0'); }
panel.querySelectorAll('.pp-thumb').forEach(x=>x.classList.toggle('active', x===t));
};
});
panel.querySelector('#ppFace').onchange = (ev)=>{
st.face = ev.target.value;
if(anim[role]) anim[role].face = st.face;
const ch = chars[role]; if(ch){ const img=ch.querySelector('img'); if(img) img.style.transform = st.face==='R'?'scaleX(-1)':'none'; }
};
}
function _renderObjProps(el){
const panel = document.getElementById('propPanel');
panel.classList.add('show');
const name = el.dataset.objName || '';
const w = el.dataset.objW || '';
const opts = PROP_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===name?' selected':'')+'>'+s+'</option>').join('');
panel.innerHTML =
'<h4>프랍 속성</h4>'+
'<div class="pp-row"><label>sprite</label><select id="ppObjName">'+opts+'</select></div>'+
'<div class="pp-row"><label>너비 (px, 비우면 원본)</label><input id="ppObjW" type="number" value="'+w+'" placeholder="원본"></div>';
panel.querySelector('#ppObjName').onchange = (ev)=>{
el.dataset.objName = ev.target.value;
el.src = png(ev.target.value);
};
panel.querySelector('#ppObjW').oninput = (ev)=>{
const v = ev.target.value.trim();
if(v === ''){ el.style.removeProperty('width'); delete el.dataset.objW; }
else { el.style.width = parseFloat(v)+'px'; el.dataset.objW = String(parseFloat(v)); }
};
}
// ── 신규 책상 추가 ──
function _addNewDesk(){
const id = 'desk_'+(__nextDeskN++);
// 기본 위치: stage 중앙 근처, 다른 책상과 안 겹치게 살짝 오프셋.
const baseX = 280 + ((__nextDeskN%5)*16);
const baseY = 260 + ((__nextDeskN%5)*16);
const st = {
key: id, agentKey: '', label: '새 책상', charRow: 0, deskSprite: 'desk-main', face: 'R',
boss: false,
deskX: baseX, deskY: baseY, deskW: 112,
seatX: baseX+4, seatY: baseY+36,
dock: [baseX+32, baseY+80],
roam: [[baseX-20, baseY+120],[baseX+60, baseY+100]],
};
stations.push(st); _rebuildStationIndex();
buildStation(st);
// 새 책상을 자동 선택.
if(_selected) _selected.classList.remove('selected');
_selected = __deskWrap[id]; _selected.classList.add('selected');
_onSelectionChanged();
}
// ── 신규 프랍 추가 — sprite picker 모달 ──
function _openPropPicker(){
const overlay = document.createElement('div');
overlay.className = 'prop-picker';
const box = document.createElement('div');
box.className = 'prop-picker-box';
box.innerHTML = '<h3>프랍 추가 — sprite 선택</h3>'+
'<div class="prop-picker-grid">'+
PROP_SPRITE_CHOICES.map(n=>'<div class="prop-pick" data-name="'+n+'"><img src="'+png(n)+'"><div class="pp-name">'+n+'</div></div>').join('')+
'</div>';
overlay.appendChild(box);
overlay.onclick = (e)=>{ if(e.target === overlay) overlay.remove(); };
box.querySelectorAll('.prop-pick').forEach(p=>{
p.onclick = ()=>{
const name = p.dataset.name;
// stage 중앙 근처에 배치.
const x = 300 + ((__nextObjN%4)*12);
const y = 280 + ((__nextObjN%4)*12);
const el = addImg(name, x, y);
overlay.remove();
// 자동 선택.
if(_selected) _selected.classList.remove('selected');
_selected = el; _selected.classList.add('selected');
_onSelectionChanged();
};
});
document.body.appendChild(overlay);
}
// ── 선택 항목 삭제 ──
function _deleteSelected(){
if(!_editMode || !_selected) return;
// char 가 선택돼 있으면 그 desk 도 함께 묶어서 처리.
let target = _selected;
if(target.classList.contains('char')){
const role = Object.keys(chars).find(k=>chars[k]===target);
if(role && __deskWrap[role]) target = __deskWrap[role];
}
if(target.classList.contains('desk')){
const role = target.dataset.role;
if(!confirm('"'+(stationByKey[role]?.label||role)+'" 책상을 삭제할까요? 이 자리에 매핑된 에이전트도 자리가 사라집니다.')) return;
target.remove();
if(chars[role]) chars[role].remove();
delete __deskWrap[role]; delete chars[role]; delete anim[role];
const idx = stations.findIndex(s=>s.key===role);
if(idx>=0) stations.splice(idx,1);
_rebuildStationIndex();
} else if(target.classList.contains('obj')){
target.remove();
} else {
return;
}
_selected = null;
_onSelectionChanged();
}
function _findDraggable(el){
@@ -4485,7 +4838,7 @@ stage.addEventListener('mousedown', e=>{
const target = _findDraggable(e.target);
// 빈 공간 클릭 → 선택 해제.
if(!target){
if(_selected){ _selected.classList.remove('selected'); _selected=null; }
if(_selected){ _selected.classList.remove('selected'); _selected=null; _onSelectionChanged(); }
return;
}
e.preventDefault();
@@ -4493,6 +4846,7 @@ stage.addEventListener('mousedown', e=>{
if(_selected && _selected !== target) _selected.classList.remove('selected');
_selected = target;
target.classList.add('selected');
_onSelectionChanged();
_drag = target;
const rect = stage.getBoundingClientRect();
const tx = parseFloat(target.style.left)||0;
@@ -4595,6 +4949,16 @@ document.getElementById('resetBtn').addEventListener('click', ()=>{
if(!confirm('현재 배치를 버리고 기본 배치로 되돌릴까요?')) return;
try{ vscode.postMessage({type:'resetPixelOfficeLayout'}); }catch{}
});
document.getElementById('addDeskBtn').addEventListener('click', _addNewDesk);
document.getElementById('addPropBtn').addEventListener('click', _openPropPicker);
document.getElementById('deleteSelBtn').addEventListener('click', _deleteSelected);
// Delete / Backspace 키도 같은 동작 — 단 입력 필드 포커스 시 무시.
document.addEventListener('keydown', e=>{
if(!_editMode) return;
const tag = (e.target && e.target.tagName) || '';
if(tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
if(e.key === 'Delete' || e.key === 'Backspace'){ e.preventDefault(); _deleteSelected(); }
});
// 저장된 layout 로드 요청 + 응답 처리.
window.addEventListener('message', e=>{