v2.2.16: Astra Office UI Overhaul & Operations Floor
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,15 @@
|
||||
<!-- ASTRA:AUTO-START -->
|
||||
|
||||
## Snapshot
|
||||
- **Workspace**: `ConnectAI` `v2.2.14` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
||||
- **Workspace**: `ConnectAI` `v2.2.15` _(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**: 248 source files, ~50,110 lines across 5 top-level modules.
|
||||
- **Stats**: 250 source files, ~50,558 lines across 5 top-level modules.
|
||||
|
||||
## Last Refresh
|
||||
- **Time**: 2026-05-16T13:04:11.625Z
|
||||
- **Files newly analysed**: 6
|
||||
- **Files reused from cache**: 242
|
||||
- **Time**: 2026-05-16T13:16:41.338Z
|
||||
- **Files newly analysed**: 3
|
||||
- **Files reused from cache**: 247
|
||||
|
||||
## Directory Map
|
||||
```mermaid
|
||||
@@ -41,7 +41,7 @@ flowchart LR
|
||||
media["media/<br/>6 files"]
|
||||
tests["tests/<br/>33 files"]
|
||||
core_py["core_py/<br/>6 files"]
|
||||
docs["docs/<br/>76 files"]
|
||||
docs["docs/<br/>78 files"]
|
||||
tests --> src
|
||||
```
|
||||
|
||||
@@ -64,7 +64,7 @@ flowchart LR
|
||||
|
||||
## Modules
|
||||
|
||||
### `src/` — 127 files, ~33,943 lines
|
||||
### `src/` — 127 files, ~34,341 lines
|
||||
|
||||
**Sub-directories**
|
||||
- `src/features/` (54) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
||||
@@ -97,7 +97,7 @@ flowchart LR
|
||||
- `src/features/company/dispatcher.ts` (1435 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)
|
||||
- `src/integrations/telegram/telegramClient.ts` (154 lines)
|
||||
- `src/features/astraOffice/view/runtime.ts` (1254 lines) — 자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.
|
||||
- `src/features/astraOffice/view/runtime.ts` (1350 lines) — 자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.
|
||||
- `src/features/company/agents.ts` (211 lines) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때 필요한 직군을 빠짐없이 커버하되 역할이 겹치지 않게 분리한다. 직군 구분 (혼동 방지): - 기획자(business) : 무엇을 만들지 정의
|
||||
- `src/features/company/pixelOfficeState.ts` (286 lines) — Pixel Office — Agent Work Pipeline 상태를 시각화하는 UI Layer 전용 모듈. ─────────────────── 설계 원칙 ─────────────────── 1. Agent 핵심 판단 로직을 절대 바꾸지 않는다. Pipeline 진행, contract 합의, 검수 cycle, 승인 게이트 — 모두 기존 dispatcher
|
||||
- `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
|
||||
@@ -160,10 +160,10 @@ flowchart LR
|
||||
- `core_py/optimizer.py` (55 lines)
|
||||
- `core_py/queue_worker.py` (82 lines)
|
||||
|
||||
### `docs/` — 76 files, ~3,084 lines
|
||||
### `docs/` — 78 files, ~3,134 lines
|
||||
|
||||
**Sub-directories**
|
||||
- `docs/records/` (63) — Astra Project Chronicle Records
|
||||
- `docs/records/` (65) — Astra Project Chronicle Records
|
||||
- `docs/docs/` (5) — docs Chronicle Records
|
||||
|
||||
**Key files**
|
||||
@@ -173,7 +173,7 @@ flowchart LR
|
||||
- `docs/EXPERIENCE_MEMORY_PLAN.md` (122 lines) — Experience Memory (Mistake / Lesson Loop) — Implementation Plan
|
||||
- `docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
||||
- `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/records/ConnectAI/timeline.md` (146 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
|
||||
@@ -224,7 +224,7 @@ flowchart LR
|
||||
- `g1nation.calendar.connect` — Astra: Google Calendar (iCal) 연결 📅
|
||||
- `g1nation.calendar.refresh` — Astra: Google Calendar 새로고침 📅
|
||||
- `g1nation.calendar.connectOAuth` — Astra: Google Calendar OAuth 연결 (쓰기) 🔐
|
||||
- **Configuration** (50 settings):
|
||||
- **Configuration** (56 settings):
|
||||
- `g1nation.multiAgentEnabled` *(boolean)* _(default: `false`)_ — Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks.
|
||||
- `g1nation.memoryEnabled` *(boolean)* _(default: `true`)_ — Enable layered memory injection before each model response.
|
||||
- `g1nation.memoryShortTermMessages` *(number)* _(default: `8`)_ — Number of recent conversation messages included as short-term memory.
|
||||
@@ -275,6 +275,12 @@ flowchart LR
|
||||
- `g1nation.selfReflector.executionVerification` *(boolean)* _(default: `false`)_ — Self-Reflector Phase C — after a code file is created via <create_file>, automatically run the language's syntax check (Python: py_compile, JS: node --check, TS: project tsc --noEmit). Failures are su
|
||||
- `g1nation.company.pixelOffice.enabled` *(boolean)* _(default: `true`)_ — Show the Pixel Office visualisation panel above the chat — a small pixel-office-style display that mirrors the agent's current pipeline status (analyzing, need_clarification, executing, reviewing, wai
|
||||
- `g1nation.company.pixelOffice.bubbles` *(boolean)* _(default: `true`)_ — Show short comic-style speech bubbles above the Pixel Office character on status changes / key events (e.g. '코드 들어간다', '잠깐, 이건 다시 보자', '좋아, 끝났다!'). Bubbles are purely narrative — they never influence
|
||||
- `g1nation.google.clientId` *(string)* _(default: `""`)_
|
||||
- `g1nation.google.clientSecret` *(string)* _(default: `""`)_
|
||||
- `g1nation.google.calendarId` *(string)* _(default: `"primary"`)_
|
||||
- `g1nation.google.defaultEventDurationMinutes` *(number)* _(default: `60`)_ — end / duration 둘 다 없는 일정의 기본 길이 (분). agent 가 회의록에서 시각만 추출하고 종료 시각은 명시 안 했을 때 적용.
|
||||
- `g1nation.google.icalUrl` *(string)* _(default: `""`)_
|
||||
- `g1nation.google.icalDaysAhead` *(number)* _(default: `14`)_ — iCal 캐시에 포함할 다가오는 일정 기간 (일). default 14 = 2주치.
|
||||
|
||||
## Dependencies
|
||||
- **Runtime** (2): `@lmstudio/sdk`, `pdf-parse`
|
||||
@@ -322,7 +328,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한
|
||||
**Designed for High-Performance Decision Making.**
|
||||
Copyright (C) **g1nation**. All rights reserved.
|
||||
|
||||
_Last auto-scan: 2026-05-16T13:04:11.625Z · signature `b9442404`_
|
||||
_Last auto-scan: 2026-05-16T13:16:41.338Z · signature `325106ed`_
|
||||
<!-- ASTRA:AUTO-END -->
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generatedAt": "2026-05-16T13:04:11.635Z",
|
||||
"generatedAt": "2026-05-16T13:16:41.347Z",
|
||||
"files": {
|
||||
"src/agent.ts": {
|
||||
"mtimeMs": 1778936503000,
|
||||
@@ -362,21 +362,21 @@
|
||||
"imports": []
|
||||
},
|
||||
"src/features/astraOffice/view/officeBody.ts": {
|
||||
"mtimeMs": 1778931340000,
|
||||
"size": 1804,
|
||||
"lines": 21,
|
||||
"role": "자동 분리: src/sidebarProvider.ts 3984-4001 에서 추출. 동작 동등.",
|
||||
"mtimeMs": 1778937173000,
|
||||
"size": 3988,
|
||||
"lines": 102,
|
||||
"role": "",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/astraOffice/view/officeStyles.ts": {
|
||||
"mtimeMs": 1778931340000,
|
||||
"size": 14309,
|
||||
"lines": 121,
|
||||
"role": "자동 분리: src/sidebarProvider.ts 3866-3982 에서 추출. 동작 동등. design doc: docs/ASTRAOFFICEREFACTOR.md",
|
||||
"mtimeMs": 1778937307000,
|
||||
"size": 20643,
|
||||
"lines": 342,
|
||||
"role": "",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/astraOffice/view/panelHtml.ts": {
|
||||
"mtimeMs": 1778931468000,
|
||||
"mtimeMs": 1778937313000,
|
||||
"size": 923,
|
||||
"lines": 26,
|
||||
"role": "Full Astra Office webview HTML composition. 옛 sidebarProvider.ts 의 거대한 pixelOfficePanelHtml 을 4개 파일로 분리한 entry. 이번 세션은 동작 동등 분리 만. 다음 세션에 mini view 와 공통 presenter 도입.",
|
||||
@@ -387,9 +387,9 @@
|
||||
]
|
||||
},
|
||||
"src/features/astraOffice/view/runtime.ts": {
|
||||
"mtimeMs": 1778933617000,
|
||||
"size": 58118,
|
||||
"lines": 1254,
|
||||
"mtimeMs": 1778937401000,
|
||||
"size": 62934,
|
||||
"lines": 1350,
|
||||
"role": "자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.",
|
||||
"imports": []
|
||||
},
|
||||
@@ -1860,7 +1860,7 @@
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/chronicle.config.json": {
|
||||
"mtimeMs": 1778932145000,
|
||||
"mtimeMs": 1778937290000,
|
||||
"size": 416,
|
||||
"lines": 11,
|
||||
"role": "JSON configuration",
|
||||
@@ -2111,6 +2111,20 @@
|
||||
"role": "Development Log: REFLECTOR 에이전트가 1인 기업 에이전트 목록에는 안보이는데",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/development/2026-05-16_astra-google-calendar-oauth-연결_implementation-2.md": {
|
||||
"mtimeMs": 1778937290000,
|
||||
"size": 1230,
|
||||
"lines": 22,
|
||||
"role": "Development Log: Astra: Google Calendar OAuth 연결",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/development/2026-05-16_astra-google-calendar-oauth-연결_implementation.md": {
|
||||
"mtimeMs": 1778937268000,
|
||||
"size": 1267,
|
||||
"lines": 22,
|
||||
"role": "Development Log: Astra: Google Calendar OAuth 연결",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트-작업-할-거야.md": {
|
||||
"mtimeMs": 1778690673000,
|
||||
"size": 652,
|
||||
@@ -2189,9 +2203,9 @@
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/timeline.md": {
|
||||
"mtimeMs": 1778902489000,
|
||||
"size": 9082,
|
||||
"lines": 140,
|
||||
"mtimeMs": 1778937290000,
|
||||
"size": 9336,
|
||||
"lines": 146,
|
||||
"role": "Project Timeline",
|
||||
"imports": []
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||
"createdAt": 1778936679275,
|
||||
"createdAt": 1778937508211,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
"createdAt": 1778936679275,
|
||||
"createdAt": 1778937508205,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||
"createdAt": 1778936679274,
|
||||
"createdAt": 1778937508200,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "---\nid: stress_conflict_1778936679262\ndate: 2026-05-16T13:04:39.276Z\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]** 핵심 정보 수집 및 분석 중... (0ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n",
|
||||
"createdAt": 1778936679276,
|
||||
"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",
|
||||
"createdAt": 1778937508211,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+10
-10
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"missionId": "stress_conflict_1778936679262",
|
||||
"missionId": "stress_conflict_1778937508187",
|
||||
"status": "completed",
|
||||
"startTime": "2026-05-16T13:04:39.262Z",
|
||||
"totalElapsedMs": 14,
|
||||
"startTime": "2026-05-16T13:18:28.187Z",
|
||||
"totalElapsedMs": 25,
|
||||
"results": {
|
||||
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
@@ -18,28 +18,28 @@
|
||||
"to": "planner",
|
||||
"durationMs": 12,
|
||||
"message": "전략 수립 중...",
|
||||
"ts": "2026-05-16T13:04:39.274Z"
|
||||
"ts": "2026-05-16T13:18:28.199Z"
|
||||
},
|
||||
{
|
||||
"from": "planner",
|
||||
"to": "researcher",
|
||||
"durationMs": 0,
|
||||
"durationMs": 1,
|
||||
"message": "핵심 정보 수집 및 분석 중...",
|
||||
"ts": "2026-05-16T13:04:39.274Z"
|
||||
"ts": "2026-05-16T13:18:28.200Z"
|
||||
},
|
||||
{
|
||||
"from": "researcher",
|
||||
"to": "writer",
|
||||
"durationMs": 1,
|
||||
"durationMs": 10,
|
||||
"message": "최종 리포트 작성 및 편집 중...",
|
||||
"ts": "2026-05-16T13:04:39.275Z"
|
||||
"ts": "2026-05-16T13:18:28.210Z"
|
||||
},
|
||||
{
|
||||
"from": "writer",
|
||||
"to": "completed",
|
||||
"durationMs": 1,
|
||||
"durationMs": 2,
|
||||
"message": "미션 완료",
|
||||
"ts": "2026-05-16T13:04:39.276Z"
|
||||
"ts": "2026-05-16T13:18:28.212Z"
|
||||
}
|
||||
],
|
||||
"resilienceMetrics": {
|
||||
@@ -1,5 +1,18 @@
|
||||
# Astra Patch Notes
|
||||
|
||||
## v2.2.16 (2026-05-16)
|
||||
### 🏢 Astra Office UI Overhaul: Operations Floor Experience
|
||||
- **차세대 오피스 UI 도입:** 단순한 뷰어를 넘어 실제 운영 본부(Operations Floor)의 느낌을 주는 대대적인 인터페이스 개편을 단행했습니다.
|
||||
- **데이터 기반 사이드 패널:** 에이전트 라인업(Team), 운영 브리프(Signal), 활동 도크(Activity Dock) 등 전문적인 정보 레이아웃을 탑재했습니다.
|
||||
- **비주얼 경험 고도화:** 글래스모피즘(Glassmorphism)과 그라데이션 백그라운드, 정교한 상태 배지(Pills)를 통해 더욱 프리미엄한 룩앤필을 구현했습니다.
|
||||
- **진행 상황 가시화:** 미션별 진행률, 단계 보고, 주의 신호 감지 등 에이전트의 작업 흐름을 한눈에 파악할 수 있는 대시보드 기능을 강화했습니다.
|
||||
- **구글 캘린더 OAuth 연동 기반 마련:** 구글 캘린더와의 정식 연동을 위한 인증 시스템 및 구현 기록을 통합했습니다.
|
||||
- **신규 패키징:** `astra-2.2.16.vsix` 패키지를 통해 더욱 강력하고 아름다워진 아스트라 오피스를 배포합니다.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.15 (2026-05-16)
|
||||
### 💎 Astra Office Refactor & Multi-Service Integration (Calendar, Sheets, Tasks)
|
||||
- **아스트라 오피스(Astra Office) 대규모 리팩토링:** 모놀리식 구조에서 기능 기반 모듈 구조(`src/features/astraOffice/`)로 전면 개편했습니다. `OfficeSnapshot` 데이터 모델 도입을 통해 에이전트의 활동 가시성과 레이아웃 관리 능력을 혁신적으로 개선했습니다.
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"corePurpose": "",
|
||||
"detailLevel": "standard",
|
||||
"createdAt": "2026-05-13T13:09:33.788Z",
|
||||
"updatedAt": "2026-05-16T11:49:05.841Z"
|
||||
"updatedAt": "2026-05-16T13:14:50.049Z"
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Development Log: Astra: Google Calendar OAuth 연결
|
||||
|
||||
## Purpose
|
||||
Record the implementation or verification outcome from the current conversation.
|
||||
|
||||
## Implementation Summary
|
||||
네, Google Calendar OAuth 연결이 완료되었다는 점 확인했습니다. 이 정보는 프로젝트의 기능 구현이나 일정 관리 부분에 중요한 전제 조건이 됩니다. 하지만 현재 아키텍처 최신화 작업으로 돌아가서 말씀드리자면, 제가 분석하고 업데이트해야 할 **`ARCHITECTURE_ANALYSIS.md` 문서 자체**의 내용이 필요합니다. 파일 접근 또는 내용 확인이 불가능하여 어떤 부분을 수정해야 하는지 판단할 근거가 없습니다. 문서 내용을 복사해서 붙여주시거나, 해당 파일을 다시 한번 읽을 수 있도록 조치해 주시면, 바로 분석에 착수하겠습니다.
|
||||
|
||||
## Architecture
|
||||
Captured automatically from the assistant response and active project context.
|
||||
|
||||
## Changed Files
|
||||
- ARCHITECTURE_ANALYSIS.md
|
||||
|
||||
## Dependency Notes
|
||||
No new dependency note was captured automatically.
|
||||
|
||||
## Bugs
|
||||
No bugs recorded.
|
||||
|
||||
## Lessons
|
||||
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Development Log: Astra: Google Calendar OAuth 연결
|
||||
|
||||
## Purpose
|
||||
Record the implementation or verification outcome from the current conversation.
|
||||
|
||||
## Implementation Summary
|
||||
제가 분석을 수행하려면 먼저 `ARCHITECTURE_ANALYSIS.md` 파일의 실제 내용이 필요합니다. 이전 확인 결과, 해당 파일을 읽는 과정에서 접근에 실패했거나 내용을 확인할 수 없었습니다. 따라서 제가 문서의 최신화 작업을 시작하고 어떤 부분을 업데이트해야 할지 판단하기 위해서는 **해당 파일의 전체 내용을 제공**해 주시거나, 파일이 실제로 접근 가능한 상태인지 다시 확인해 주셔야 합니다. 파일 내용을 제공해 주시면, 그 내용을 바탕으로 현재 프로젝트 상황과 기술적 맥락을 고려하여 최신화 방향과 구체적인 수정 사항을 제시해 드리겠습니다.
|
||||
|
||||
## Architecture
|
||||
Captured automatically from the assistant response and active project context.
|
||||
|
||||
## Changed Files
|
||||
- ARCHITECTURE_ANALYSIS.md
|
||||
|
||||
## Dependency Notes
|
||||
No new dependency note was captured automatically.
|
||||
|
||||
## Bugs
|
||||
No bugs recorded.
|
||||
|
||||
## Lessons
|
||||
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
|
||||
@@ -138,3 +138,9 @@
|
||||
|
||||
## 2026-05-15
|
||||
- Auto decision record created: decisions\ADR-0014-astra-office-부분-관련해서-개선할-부분이-너무-많아-개선점이-무었이-있는지-의견-주면-좋겠어.md
|
||||
|
||||
## 2026-05-16
|
||||
- Auto development record created: development/2026-05-16_astra-google-calendar-oauth-연결_implementation.md
|
||||
|
||||
## 2026-05-16
|
||||
- Auto development record created: development/2026-05-16_astra-google-calendar-oauth-연결_implementation-2.md
|
||||
|
||||
+38
-1
@@ -2,7 +2,7 @@
|
||||
"name": "astra",
|
||||
"displayName": "Astra",
|
||||
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
||||
"version": "2.2.15",
|
||||
"version": "2.2.16",
|
||||
"publisher": "g1nation",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
@@ -490,6 +490,43 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Show short comic-style speech bubbles above the Pixel Office character on status changes / key events (e.g. '코드 들어간다', '잠깐, 이건 다시 보자', '좋아, 끝났다!'). Bubbles are purely narrative — they never influence the agent's decisions. Disable for a quieter UI."
|
||||
},
|
||||
"g1nation.google.clientId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"scope": "machine",
|
||||
"markdownDescription": "Google OAuth Client ID — `console.cloud.google.com/apis/credentials` → OAuth 2.0 Client ID (Desktop app) 생성 후 복사. **`Astra: Google Calendar OAuth 연결 (쓰기) 🔐`** 명령을 한 번 실행하면 이 값이 자동으로 채워집니다. Calendar + Sheets 둘 다 이 자격증명을 공유.\n\n_scope: machine — Settings Sync 로 다른 기기에 공유되지 않음._"
|
||||
},
|
||||
"g1nation.google.clientSecret": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"scope": "machine",
|
||||
"markdownDescription": "Google OAuth Client Secret — Client ID 와 같은 페이지에서 발급. Desktop app OAuth 의 secret 은 Google 가이드상 *진짜 비밀이 아닌 식별자* 지만, settings.json 에 그대로 들어가므로 git 커밋 / 화면 공유 시 주의.\n\n_scope: machine — Settings Sync 안 됨._"
|
||||
},
|
||||
"g1nation.google.calendarId": {
|
||||
"type": "string",
|
||||
"default": "primary",
|
||||
"markdownDescription": "일정을 등록할 Google Calendar 식별자. 기본 `primary` (본인 메인 캘린더). 특정 캘린더 쓰려면 Calendar 설정 → 캘린더 통합 → 'Calendar ID' 복사 (예: `xxxxxxx@group.calendar.google.com`)."
|
||||
},
|
||||
"g1nation.google.defaultEventDurationMinutes": {
|
||||
"type": "number",
|
||||
"default": 60,
|
||||
"minimum": 5,
|
||||
"maximum": 1440,
|
||||
"description": "end / duration 둘 다 없는 일정의 기본 길이 (분). agent 가 회의록에서 시각만 추출하고 종료 시각은 명시 안 했을 때 적용."
|
||||
},
|
||||
"g1nation.google.icalUrl": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"scope": "machine",
|
||||
"markdownDescription": "Google Calendar **비공개 iCal URL** — 읽기 전용 모드용. `calendar.google.com/calendar/u/0/r/settings` → 본인 캘린더 → '캘린더 통합' → '비공개 주소(iCal 형식)' 복사. **이 URL 을 가진 사람은 본인 캘린더 모든 일정을 볼 수 있으니 절대 공개 금지.**\n\n_scope: machine — Settings Sync 안 됨. OAuth 와는 별개 — 둘 다 셋업해도 되고 한 쪽만 해도 됨._"
|
||||
},
|
||||
"g1nation.google.icalDaysAhead": {
|
||||
"type": "number",
|
||||
"default": 14,
|
||||
"minimum": 1,
|
||||
"maximum": 90,
|
||||
"description": "iCal 캐시에 포함할 다가오는 일정 기간 (일). default 14 = 2주치."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-4
@@ -1049,18 +1049,39 @@ async function runConnectGoogleCalendarOAuth(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const clientId = await vscode.window.showInputBox({
|
||||
// Settings 에 이미 채워져 있으면 그대로 쓰겠냐고 물어봄 — 매번 똑같은 값 다시 입력하기 귀찮음.
|
||||
const haveBoth = !!(cur.clientId && cur.clientSecret);
|
||||
let clientId: string | undefined = cur.clientId;
|
||||
let clientSecret: string | undefined = cur.clientSecret;
|
||||
if (haveBoth) {
|
||||
const useExisting = await vscode.window.showInformationMessage(
|
||||
`Settings (g1nation.google) 에 이미 Client ID/Secret 이 있습니다.\nID: ${cur.clientId!.slice(0, 20)}…\n\n이 값으로 OAuth 진행할까요?`,
|
||||
{ modal: false },
|
||||
'예 (Settings 값 사용)',
|
||||
'아니오 (새로 입력)',
|
||||
'취소',
|
||||
);
|
||||
if (useExisting === '취소' || !useExisting) return;
|
||||
if (useExisting === '아니오 (새로 입력)') {
|
||||
clientId = undefined;
|
||||
clientSecret = undefined;
|
||||
}
|
||||
}
|
||||
if (!clientId) {
|
||||
clientId = await vscode.window.showInputBox({
|
||||
title: 'Google OAuth Client ID',
|
||||
prompt: 'Credentials 페이지에서 복사한 Client ID',
|
||||
prompt: 'Credentials 페이지에서 복사한 Client ID — 자동으로 Settings(g1nation.google.clientId)에 저장됨',
|
||||
placeHolder: 'xxxxxxxx.apps.googleusercontent.com',
|
||||
value: cur.clientId,
|
||||
ignoreFocusOut: true,
|
||||
validateInput: (v) => (v || '').trim() ? null : '비어있어요',
|
||||
});
|
||||
if (!clientId) return;
|
||||
const clientSecret = await vscode.window.showInputBox({
|
||||
}
|
||||
if (!clientSecret) {
|
||||
clientSecret = await vscode.window.showInputBox({
|
||||
title: 'Google OAuth Client Secret',
|
||||
prompt: '같은 화면의 Client Secret',
|
||||
prompt: '같은 화면의 Client Secret — Settings(g1nation.google.clientSecret)에 저장됨',
|
||||
placeHolder: 'GOCSPX-...',
|
||||
value: cur.clientSecret,
|
||||
password: true,
|
||||
@@ -1068,6 +1089,7 @@ async function runConnectGoogleCalendarOAuth(context: vscode.ExtensionContext) {
|
||||
validateInput: (v) => (v || '').trim() ? null : '비어있어요',
|
||||
});
|
||||
if (!clientSecret) return;
|
||||
}
|
||||
|
||||
await vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
|
||||
@@ -1,21 +1,102 @@
|
||||
// 자동 분리: src/sidebarProvider.ts 3984-4001 에서 추출. 동작 동등.
|
||||
export const OFFICE_BODY = `
|
||||
<body>
|
||||
<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 class="office-app">
|
||||
<header class="topbar">
|
||||
<div class="brand-block">
|
||||
<div class="eyebrow">ASTRA OFFICE</div>
|
||||
<div class="h-title">Operations Floor</div>
|
||||
</div>
|
||||
<div class="topbar-center">
|
||||
<div class="phase-pill" id="phasePill"><span class="phase-dot"></span><span id="status">대기 중</span></div>
|
||||
<div class="agent-pill"><span class="agent-label">현재 담당</span><strong id="agent">Astra</strong></div>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<button id="editBtn" class="edit-btn" title="오피스 배치 편집 모드 토글">Customize</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="editToolbar" class="edit-toolbar" style="display:none;">
|
||||
<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>
|
||||
<button id="resetBtn" title="기본 배치로 복귀">↻ 디폴트</button>
|
||||
<button id="cancelBtn" title="저장 안 하고 종료">✕ 취소</button>
|
||||
<button id="deleteSelBtn" class="del" title="선택 항목 삭제" disabled>삭제</button>
|
||||
<button id="layerUpBtn" title="레이어 위로 (])">위로</button>
|
||||
<button id="layerDownBtn" title="레이어 아래로 ([)">아래로</button>
|
||||
<button id="saveBtn">저장</button>
|
||||
<button id="resetBtn" title="기본 배치로 복귀">초기화</button>
|
||||
<button id="cancelBtn" title="저장 안 하고 종료">취소</button>
|
||||
</div>
|
||||
|
||||
<main class="workspace">
|
||||
<aside class="side-panel team-panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<div class="panel-kicker">Team</div>
|
||||
<h2>오늘의 라인업</h2>
|
||||
</div>
|
||||
<div class="count-badge" id="rosterCount">0</div>
|
||||
</div>
|
||||
<div id="rosterList" class="roster-list"></div>
|
||||
</aside>
|
||||
|
||||
<section class="office-shell">
|
||||
<div class="mission-strip">
|
||||
<div>
|
||||
<div class="panel-kicker">Current Mission</div>
|
||||
<div class="mission-title" id="task">새 요청을 기다리고 있습니다.</div>
|
||||
</div>
|
||||
<div class="mission-step-wrap">
|
||||
<span>현재 단계</span>
|
||||
<strong id="step">대기 중</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div id="miniMap" class="mini-map" style="display:none;"></div>
|
||||
<div class="office-stage-wrap">
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="side-panel mission-panel">
|
||||
<div class="panel-head compact">
|
||||
<div>
|
||||
<div class="panel-kicker">Signal</div>
|
||||
<h2>운영 브리프</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="brief-grid">
|
||||
<section class="brief-card hero">
|
||||
<span>현재 흐름</span>
|
||||
<strong id="phaseLabel">대기 중</strong>
|
||||
<p id="phaseNote">새로운 작업 요청을 기다리고 있습니다.</p>
|
||||
</section>
|
||||
<section class="brief-card">
|
||||
<span>진행률</span>
|
||||
<strong id="progressLabel">0%</strong>
|
||||
<div class="progress"><div class="bar" id="bar"></div></div>
|
||||
</section>
|
||||
<section class="brief-card" id="attentionCard">
|
||||
<span>주의 신호</span>
|
||||
<strong id="attentionTitle">없음</strong>
|
||||
<p id="attentionBody">현재 막힘 없이 진행 중입니다.</p>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<footer class="activity-dock">
|
||||
<div class="activity-head">
|
||||
<div>
|
||||
<div class="panel-kicker">Activity</div>
|
||||
<strong>최근 실행</strong>
|
||||
</div>
|
||||
<div id="log" class="last-log">아직 기록된 활동이 없습니다.</div>
|
||||
</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="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>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,40 +1,236 @@
|
||||
// 자동 분리: src/sidebarProvider.ts 3866-3982 에서 추출. 동작 동등.
|
||||
// design doc: docs/ASTRA_OFFICE_REFACTOR.md
|
||||
export const OFFICE_CSS = `
|
||||
<style>
|
||||
:root{--bg:#0E1019;--wall:#202536;--floor:#302634;--floor2:#281F2C;--text:#F1F4FB;--muted:#A8B0C7;--accent:#7C83FF;}
|
||||
*{box-sizing:border-box} body{margin:0;height:100vh;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;display:flex;flex-direction:column;overflow:hidden}
|
||||
header{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;background:rgba(0,0,0,.22);border-bottom:1px solid rgba(255,255,255,.08)}
|
||||
.h-title{font-weight:800}.h-sub{font-size:11px;color:var(--muted)}.status{font-size:12px;padding:4px 10px;border:1px solid rgba(255,255,255,.18);border-radius:999px}
|
||||
.strip{display:flex;gap:16px;padding:8px 16px;font-size:12px;color:var(--muted);border-bottom:1px solid rgba(255,255,255,.06)}.strip b{color:var(--text)}
|
||||
.office{position:relative;flex:1;overflow:hidden;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,#21283a 0 16%,transparent 16%),radial-gradient(ellipse at 50% 0%,rgba(124,131,255,.12),transparent 42%),linear-gradient(135deg,#322835,#271f2a)}
|
||||
.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}
|
||||
.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)}
|
||||
:root{
|
||||
--bg:#070A12;
|
||||
--bg-soft:#0D1220;
|
||||
--surface:rgba(15,21,36,.78);
|
||||
--surface-strong:rgba(18,25,43,.94);
|
||||
--surface-faint:rgba(255,255,255,.045);
|
||||
--line:rgba(255,255,255,.09);
|
||||
--line-strong:rgba(255,255,255,.16);
|
||||
--text:#F5F7FC;
|
||||
--muted:#9BA6BF;
|
||||
--accent:#8A7CFF;
|
||||
--accent-2:#46D8FF;
|
||||
--success:#35D7A4;
|
||||
--warning:#F5C45A;
|
||||
--danger:#FF6B7A;
|
||||
--shadow:0 18px 60px rgba(0,0,0,.42);
|
||||
--radius-xl:24px;
|
||||
--radius-lg:18px;
|
||||
--radius-md:14px;
|
||||
--radius-sm:10px;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;
|
||||
background:
|
||||
radial-gradient(circle at 12% 0%,rgba(138,124,255,.18),transparent 28%),
|
||||
radial-gradient(circle at 90% 10%,rgba(70,216,255,.12),transparent 24%),
|
||||
radial-gradient(circle at 50% 100%,rgba(53,215,164,.08),transparent 32%),
|
||||
linear-gradient(180deg,#070A12 0%,#0A0F1A 100%);
|
||||
color:var(--text);
|
||||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
overflow:hidden;
|
||||
}
|
||||
button,input,select{font:inherit}
|
||||
.office-app{height:100vh;display:grid;grid-template-rows:auto auto minmax(0,1fr) auto;gap:14px;padding:18px}
|
||||
.topbar{
|
||||
min-height:70px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:18px;
|
||||
padding:14px 16px 14px 18px;
|
||||
border:1px solid var(--line);
|
||||
border-radius:var(--radius-xl);
|
||||
background:linear-gradient(180deg,rgba(255,255,255,.08),rgba(255,255,255,.035));
|
||||
backdrop-filter:blur(18px);
|
||||
box-shadow:var(--shadow);
|
||||
}
|
||||
.brand-block{display:flex;flex-direction:column;gap:3px;min-width:180px}
|
||||
.eyebrow,.panel-kicker{font-size:10px;line-height:1;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
|
||||
.h-title{font-size:22px;line-height:1.1;font-weight:750;letter-spacing:-.03em}
|
||||
.topbar-center{display:flex;align-items:center;justify-content:center;gap:10px;flex:1;min-width:0}
|
||||
.phase-pill,.agent-pill{
|
||||
min-height:38px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:9px;
|
||||
padding:0 14px;
|
||||
border:1px solid var(--line);
|
||||
border-radius:999px;
|
||||
background:rgba(255,255,255,.05);
|
||||
white-space:nowrap;
|
||||
}
|
||||
.phase-pill{font-weight:650}
|
||||
.phase-pill .phase-dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 16px rgba(138,124,255,.8)}
|
||||
.phase-pill[data-tone="success"] .phase-dot{background:var(--success);box-shadow:0 0 16px rgba(53,215,164,.8)}
|
||||
.phase-pill[data-tone="warning"] .phase-dot{background:var(--warning);box-shadow:0 0 16px rgba(245,196,90,.8)}
|
||||
.phase-pill[data-tone="danger"] .phase-dot{background:var(--danger);box-shadow:0 0 16px rgba(255,107,122,.8)}
|
||||
.agent-pill{color:var(--muted)}
|
||||
.agent-pill strong{color:var(--text);font-size:13px}
|
||||
.agent-label{font-size:11px}
|
||||
.topbar-actions{display:flex;align-items:center;justify-content:flex-end;min-width:180px}
|
||||
.edit-btn{
|
||||
height:38px;
|
||||
border-radius:999px;
|
||||
padding:0 15px;
|
||||
border:1px solid rgba(138,124,255,.35);
|
||||
color:var(--text);
|
||||
background:linear-gradient(180deg,rgba(138,124,255,.22),rgba(138,124,255,.12));
|
||||
cursor:pointer;
|
||||
transition:transform .16s ease,border-color .16s ease,background .16s ease;
|
||||
}
|
||||
.edit-btn:hover{transform:translateY(-1px);border-color:rgba(138,124,255,.62);background:linear-gradient(180deg,rgba(138,124,255,.3),rgba(138,124,255,.16))}
|
||||
.edit-toolbar{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
flex-wrap:wrap;
|
||||
padding:10px 12px;
|
||||
border:1px solid rgba(138,124,255,.28);
|
||||
border-radius:var(--radius-lg);
|
||||
background:rgba(138,124,255,.12);
|
||||
backdrop-filter:blur(16px);
|
||||
}
|
||||
.edit-toolbar .et-hint{flex:1;min-width:220px;color:#DCE2F2;font-size:12px}
|
||||
.edit-toolbar button{
|
||||
min-height:32px;
|
||||
padding:0 11px;
|
||||
border-radius:999px;
|
||||
border:1px solid var(--line-strong);
|
||||
color:var(--text);
|
||||
background:rgba(255,255,255,.07);
|
||||
cursor:pointer;
|
||||
}
|
||||
.edit-toolbar button:hover{background:rgba(255,255,255,.12)}
|
||||
.edit-toolbar button.add{border-color:rgba(53,215,164,.4);background:rgba(53,215,164,.12)}
|
||||
.edit-toolbar button.del{border-color:rgba(255,107,122,.42);background:rgba(255,107,122,.12)}
|
||||
.edit-toolbar button[disabled]{opacity:.42;cursor:not-allowed}
|
||||
.workspace{
|
||||
min-height:0;
|
||||
display:grid;
|
||||
grid-template-columns:260px minmax(620px,1fr) 280px;
|
||||
gap:14px;
|
||||
}
|
||||
.side-panel,.office-shell,.activity-dock{
|
||||
border:1px solid var(--line);
|
||||
border-radius:var(--radius-xl);
|
||||
background:var(--surface);
|
||||
backdrop-filter:blur(20px);
|
||||
box-shadow:var(--shadow);
|
||||
}
|
||||
.side-panel{padding:16px;min-height:0;overflow:hidden}
|
||||
.panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}
|
||||
.panel-head.compact{margin-bottom:16px}
|
||||
.panel-head h2{font-size:16px;line-height:1.2;letter-spacing:-.02em;margin:6px 0 0}
|
||||
.count-badge{
|
||||
min-width:28px;
|
||||
height:28px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(70,216,255,.25);
|
||||
background:rgba(70,216,255,.12);
|
||||
color:#BDEFFF;
|
||||
font-size:12px;
|
||||
font-weight:700;
|
||||
}
|
||||
.roster-list{display:flex;flex-direction:column;gap:9px;overflow:auto;padding-right:2px;max-height:calc(100vh - 220px)}
|
||||
.roster-item{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
gap:11px;
|
||||
align-items:center;
|
||||
padding:11px;
|
||||
border-radius:var(--radius-md);
|
||||
border:1px solid transparent;
|
||||
background:rgba(255,255,255,.035);
|
||||
transition:border-color .16s ease,background .16s ease,transform .16s ease;
|
||||
}
|
||||
.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-avatar{
|
||||
width:33px;height:33px;border-radius:12px;
|
||||
display:grid;place-items:center;
|
||||
background:rgba(255,255,255,.08);
|
||||
border:1px solid var(--line);
|
||||
color:var(--role-color,var(--accent));
|
||||
font-size:11px;font-weight:700;
|
||||
}
|
||||
.roster-copy{min-width:0;display:flex;flex-direction:column;gap:3px}
|
||||
.roster-copy strong{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.roster-meta{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:11px}
|
||||
.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))}
|
||||
.office-shell{min-width:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);overflow:hidden}
|
||||
.mission-strip{
|
||||
min-height:72px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:18px;
|
||||
padding:15px 18px;
|
||||
border-bottom:1px solid var(--line);
|
||||
}
|
||||
.mission-title{margin-top:6px;font-size:16px;font-weight:650;letter-spacing:-.02em;max-width:min(560px,52vw);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.mission-step-wrap{display:flex;flex-direction:column;align-items:flex-end;gap:5px;min-width:160px}
|
||||
.mission-step-wrap span{font-size:11px;color:var(--muted)}
|
||||
.mission-step-wrap strong{font-size:13px;font-weight:650;text-align:right;max-width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.mini-map{display:flex;align-items:center;gap:6px;min-height:42px;padding:0 18px;border-bottom:1px solid var(--line);overflow-x:auto;scrollbar-width:none}
|
||||
.mini-map::-webkit-scrollbar{display:none}
|
||||
.mini-map .mm-dot{position:relative;width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.18);flex-shrink:0;transition:all .2s ease}
|
||||
.mini-map .mm-dot[data-status="done"]{background:var(--success);border-color:var(--success);box-shadow:0 0 12px rgba(53,215,164,.35)}
|
||||
.mini-map .mm-dot[data-status="active"]{width:14px;height:14px;background:var(--accent);border-color:var(--accent);box-shadow:0 0 0 4px rgba(138,124,255,.16)}
|
||||
.mini-map .mm-bar{flex:1;height:1px;min-width:22px;background:linear-gradient(90deg,rgba(255,255,255,.08),rgba(255,255,255,.18))}
|
||||
.mini-map .mm-label{position:absolute;left:50%;top:-29px;transform:translateX(-50%);padding:4px 7px;border-radius:999px;background:rgba(6,9,16,.95);border:1px solid var(--line);font-size:10px;white-space:nowrap;color:var(--text);opacity:0;pointer-events:none;transition:opacity .14s ease;z-index:40}
|
||||
.mini-map .mm-dot:hover .mm-label{opacity:1}
|
||||
.mini-map .mm-counter{margin-left:6px;color:var(--muted);font-size:11px;white-space:nowrap}
|
||||
.office-stage-wrap{min-height:0;padding:14px;display:flex}
|
||||
.office{
|
||||
position:relative;
|
||||
flex:1;
|
||||
min-height:420px;
|
||||
overflow:hidden;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
border-radius:20px;
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
background:
|
||||
linear-gradient(180deg,rgba(18,28,48,.98) 0 16%,transparent 16%),
|
||||
radial-gradient(circle at 50% -10%,rgba(138,124,255,.24),transparent 32%),
|
||||
radial-gradient(circle at 18% 100%,rgba(70,216,255,.12),transparent 28%),
|
||||
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: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}
|
||||
.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.w1{left:84px}.wall-window.w2{left:550px}
|
||||
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
|
||||
.obj{filter:drop-shadow(3px 4px 0 rgba(0,0,0,.28));z-index:4}
|
||||
.desk{width:112px;z-index:5;filter:drop-shadow(4px 5px 0 rgba(0,0,0,.32))}.desk.boss{width:136px}.label{position:absolute;left:50%;bottom:-10px;transform:translateX(-50%);font-size:10px;color:rgba(241,244,251,.78);white-space:nowrap;text-shadow:1px 1px #000}
|
||||
.char{width:56px;height:72px;z-index:7;transition:left 1.17s cubic-bezier(.2,.7,.2,1),top 1.17s cubic-bezier(.2,.7,.2,1)}.char.walking{z-index:14}.char img{position:absolute;left:0;bottom:0;max-width:100%;max-height:100%;image-rendering:pixelated;filter:drop-shadow(2px 2px 0 rgba(0,0,0,.45));transform-origin:center bottom}
|
||||
.char.active:before{content:'';position:absolute;left:24px;top:-10px;width:8px;height:8px;background:var(--role-color,var(--accent));box-shadow:0 0 12px var(--role-color,var(--accent));animation:po-pulse 1.6s ease-in-out infinite}
|
||||
@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-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 는 line 3878 의 .obj,.desk,.char{position:absolute} 를 그대로 유지해야 한다.
|
||||
과거 .desk{position:relative} 가 cascade로 override 되어, 새로 추가한 책상이 normal-flow Y
|
||||
에 따라 stage 바깥으로 밀려나던 버그가 있었음. ::after pseudo 는 absolute parent 기준으로도 정상 동작. */
|
||||
.desk::after{content:'';position:absolute;inset:-2px;border-radius:4px;border:2px solid transparent;pointer-events:none;transition:border-color .3s}
|
||||
.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.boss{width:136px}
|
||||
.label{position:absolute;left:50%;bottom:-15px;transform:translateX(-50%);font-size:10px;color:rgba(245,247,252,.8);white-space:nowrap;text-shadow:0 1px 4px rgba(0,0,0,.8)}
|
||||
.char{width:56px;height:72px;z-index:7;transition:left .9s cubic-bezier(.2,.7,.2,1),top .9s cubic-bezier(.2,.7,.2,1)}
|
||||
.char.walking{z-index:14}
|
||||
.char img{position:absolute;left:0;bottom:0;max-width:100%;max-height:100%;image-rendering:pixelated;filter:drop-shadow(2px 3px 0 rgba(0,0,0,.42));transform-origin:center bottom}
|
||||
.char.active:before{content:'';position:absolute;left:19px;top:-14px;width:18px;height:18px;border-radius:50%;background:radial-gradient(circle,var(--role-color,var(--accent)) 0 28%,rgba(255,255,255,.18) 30%,transparent 72%);filter:blur(.2px);animation:focusPulse 1.8s ease-in-out infinite}
|
||||
.char.active::after{content:'';position:absolute;left:-4px;right:-4px;bottom:-7px;height:8px;border-radius:999px;background:radial-gradient(circle,var(--role-color,var(--accent)),transparent 70%);opacity:.82;filter:blur(1px)}
|
||||
.char.working img{animation:workLean 1.6s ease-in-out infinite}
|
||||
@keyframes focusPulse{0%,100%{transform:scale(.95);opacity:.88}50%{transform:scale(1.12);opacity:1}}
|
||||
@keyframes workLean{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}
|
||||
.char[data-agent="ceo"],.desk[data-agent="ceo"],.roster-item[data-agent="ceo"]{--role-color:#A78BFA}
|
||||
.char[data-agent="planner"],.desk[data-agent="planner"],.roster-item[data-agent="planner"]{--role-color:#60A5FA}
|
||||
.char[data-agent="researcher"],.desk[data-agent="researcher"],.roster-item[data-agent="researcher"]{--role-color:#35D7A4}
|
||||
.char[data-agent="designer"],.desk[data-agent="designer"],.roster-item[data-agent="designer"]{--role-color:#F472B6}
|
||||
.char[data-agent="developer"],.desk[data-agent="developer"],.roster-item[data-agent="developer"]{--role-color:#F5C45A}
|
||||
.char[data-agent="qa"],.desk[data-agent="qa"],.roster-item[data-agent="qa"]{--role-color:#46D8FF}
|
||||
.char[data-agent="inspector"],.desk[data-agent="inspector"],.roster-item[data-agent="inspector"]{--role-color:#FB923C}
|
||||
.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}
|
||||
.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}
|
||||
.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,
|
||||
@@ -42,80 +238,105 @@ header{display:flex;justify-content:space-between;align-items:center;padding:10p
|
||||
.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)}
|
||||
.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)}
|
||||
.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}
|
||||
.edit-btn{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.2);color:#F1F4FB;padding:4px 10px;border-radius:5px;cursor:pointer;font-size:11px}.edit-btn:hover{background:rgba(99,102,241,.25);border-color:#6366F1}
|
||||
/* ── B. 워크플로우 미니 맵 ── 헤더 아래 dot strip. 각 dot이 stage 하나. 완료=
|
||||
채워진 점, 활성=링 펄스, 대기=빈 점. 호버 시 라벨 표시. */
|
||||
.mini-map{display:flex;gap:5px;align-items:center;padding:7px 16px;background:rgba(0,0,0,.3);border-bottom:1px solid rgba(255,255,255,.06);overflow-x:auto;scrollbar-width:none}.mini-map::-webkit-scrollbar{display:none}
|
||||
.mini-map .mm-dot{position:relative;width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.12);border:1.5px solid rgba(255,255,255,.18);flex-shrink:0;cursor:default;transition:all .25s}
|
||||
.mini-map .mm-dot[data-status="done"]{background:#10B981;border-color:#10B981;box-shadow:0 0 4px rgba(16,185,129,.5)}
|
||||
.mini-map .mm-dot[data-status="active"]{background:var(--accent);border-color:var(--accent);width:14px;height:14px;box-shadow:0 0 0 3px rgba(99,102,241,.3);animation:mm-pulse 1.4s ease-in-out infinite}
|
||||
@keyframes mm-pulse{0%,100%{box-shadow:0 0 0 3px rgba(99,102,241,.3)}50%{box-shadow:0 0 0 6px rgba(99,102,241,.15)}}
|
||||
.mini-map .mm-bar{flex:1;height:1px;background:linear-gradient(90deg,rgba(255,255,255,.08),rgba(255,255,255,.16))}
|
||||
.mini-map .mm-label{position:absolute;left:50%;top:-22px;transform:translateX(-50%);font-size:10px;color:#F1F4FB;background:rgba(0,0,0,.85);padding:2px 6px;border-radius:3px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;z-index:50}
|
||||
.mini-map .mm-dot:hover .mm-label{opacity:1}
|
||||
.mini-map .mm-counter{flex-shrink:0;font-size:10px;color:#94A3B8;margin-left:8px;white-space:nowrap}
|
||||
/* ── E. Activity Ticker ── action-tag executor 결과를 하단 strip으로 흘림.
|
||||
사용자가 에이전트의 *실제 행동*(파일 쓰기, 명령 실행)을 실시간으로 보며 신뢰. */
|
||||
.ticker{position:relative;padding:5px 16px;background:rgba(99,102,241,.08);border-top:1px solid rgba(99,102,241,.18);overflow:hidden;font-size:11px;font-family:ui-monospace,monospace;height:24px}
|
||||
.tk-track{display:flex;gap:18px;white-space:nowrap;animation:tk-roll 22s linear infinite;will-change:transform}
|
||||
.ticker:hover .tk-track{animation-play-state:paused}
|
||||
.tk-item{flex-shrink:0;color:#D7DBEA}
|
||||
.tk-item.tk-ok{color:#10B981}
|
||||
.tk-item.tk-warn{color:#F5C518}
|
||||
.tk-item.tk-err{color:#EF4444}
|
||||
.tk-item .tk-agent{color:#A78BFA;margin-right:5px;font-weight:600}
|
||||
@keyframes tk-roll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
/* ── D. 캐릭터 컨텍스트 메뉴 ── 편집 모드 X일 때 캐릭터 클릭하면 작은 메뉴 popup.
|
||||
현재 turn 제어 + 최근 활동 보기. */
|
||||
.ctx-menu{position:fixed;z-index:1000;background:#13162A;border:1px solid #2A2E3F;border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.6);padding:4px;min-width:170px;font-size:12px;color:#F1F4FB}
|
||||
.ctx-menu-head{padding:6px 10px 4px;font-size:10px;color:#94A3B8;border-bottom:1px solid rgba(255,255,255,.08);margin-bottom:4px}
|
||||
.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}
|
||||
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
||||
.brief-card{
|
||||
padding:14px;
|
||||
border-radius:var(--radius-md);
|
||||
border:1px solid var(--line);
|
||||
background:rgba(255,255,255,.035);
|
||||
}
|
||||
.brief-card.hero{background:linear-gradient(180deg,rgba(138,124,255,.16),rgba(255,255,255,.03));border-color:rgba(138,124,255,.28)}
|
||||
.brief-card span{display:block;font-size:11px;color:var(--muted);margin-bottom:6px}
|
||||
.brief-card strong{display:block;font-size:17px;line-height:1.2;letter-spacing:-.02em}
|
||||
.brief-card p{margin:8px 0 0;color:#D6DDF0;font-size:12px;line-height:1.5}
|
||||
#attentionCard[data-tone="warning"]{border-color:rgba(245,196,90,.32);background:rgba(245,196,90,.08)}
|
||||
#attentionCard[data-tone="danger"]{border-color:rgba(255,107,122,.34);background:rgba(255,107,122,.08)}
|
||||
.progress{height:8px;margin-top:12px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden}
|
||||
.bar{height:100%;width:0;border-radius:inherit;background:linear-gradient(90deg,var(--accent),var(--accent-2));transition:width .22s ease}
|
||||
.activity-dock{padding:14px 16px 15px;display:flex;flex-direction:column;gap:10px}
|
||||
.activity-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px}
|
||||
.activity-head strong{display:block;font-size:14px;margin-top:5px}
|
||||
.last-log{max-width:65%;font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.ticker{position:relative;overflow:hidden}
|
||||
.tk-track{display:flex;gap:8px;overflow-x:auto;scrollbar-width:none;padding-bottom:1px}
|
||||
.tk-track::-webkit-scrollbar{display:none}
|
||||
.tk-item{
|
||||
flex-shrink:0;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:7px;
|
||||
min-height:30px;
|
||||
padding:0 11px;
|
||||
border-radius:999px;
|
||||
border:1px solid var(--line);
|
||||
background:rgba(255,255,255,.045);
|
||||
color:#DCE3F3;
|
||||
font-size:11px;
|
||||
}
|
||||
.tk-item.tk-ok{border-color:rgba(53,215,164,.26)}
|
||||
.tk-item.tk-warn{border-color:rgba(245,196,90,.28)}
|
||||
.tk-item.tk-err{border-color:rgba(255,107,122,.3)}
|
||||
.tk-item .tk-agent{color:#C6BEFF;font-weight:650}
|
||||
.ctx-menu{position:fixed;z-index:1000;background:rgba(10,14,24,.96);border:1px solid var(--line-strong);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.5);padding:6px;min-width:180px;font-size:12px;color:var(--text)}
|
||||
.ctx-menu-head{padding:7px 10px 8px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);margin-bottom:4px}
|
||||
.ctx-menu-head .cmh-role{color:var(--role-color,#A78BFA);font-weight:700;text-transform:uppercase}
|
||||
.ctx-menu-item{display:flex;align-items:center;gap:8px;padding:7px 10px;cursor:pointer;border-radius:4px;transition:background .12s}
|
||||
.ctx-menu-item:hover{background:rgba(99,102,241,.18)}
|
||||
.ctx-menu-item.danger:hover{background:rgba(239,68,68,.18);color:#FCA5A5}
|
||||
.ctx-menu-divider{height:1px;background:rgba(255,255,255,.08);margin:3px 4px}
|
||||
.ctx-menu-item{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;border-radius:10px;transition:background .12s ease}
|
||||
.ctx-menu-item:hover{background:rgba(138,124,255,.16)}
|
||||
.ctx-menu-item.danger:hover{background:rgba(255,107,122,.14);color:#FFC3CC}
|
||||
.ctx-menu-divider{height:1px;background:var(--line);margin:4px}
|
||||
body[data-edit-mode="true"] .ctx-menu{display:none!important}
|
||||
body:not([data-edit-mode="true"]) .char{cursor:pointer}
|
||||
.ctx-detail{position:fixed;z-index:1001;background:#13162A;border:1px solid #2A2E3F;border-radius:8px;box-shadow:0 12px 36px rgba(0,0,0,.7);padding:16px 18px;color:#F1F4FB;min-width:320px;max-width:520px;max-height:60vh;overflow-y:auto;font-size:12px;line-height:1.5}
|
||||
.ctx-detail h3{margin:0 0 8px;font-size:14px;color:var(--role-color,#A78BFA);text-transform:uppercase;letter-spacing:.04em}
|
||||
.ctx-detail .cd-close{position:absolute;top:8px;right:10px;background:transparent;border:none;color:#94A3B8;font-size:16px;cursor:pointer}
|
||||
.ctx-detail{position:fixed;z-index:1001;background:rgba(10,14,24,.96);border:1px solid var(--line-strong);border-radius:18px;box-shadow:0 20px 56px rgba(0,0,0,.56);padding:18px;color:var(--text);min-width:320px;max-width:520px;max-height:60vh;overflow-y:auto;font-size:12px;line-height:1.5}
|
||||
.ctx-detail h3{margin:0 0 10px;font-size:14px;color:var(--role-color,#A78BFA);letter-spacing:.02em}
|
||||
.ctx-detail .cd-close{position:absolute;top:10px;right:12px;background:transparent;border:none;color:var(--muted);font-size:16px;cursor:pointer}
|
||||
.ctx-detail dl{margin:0;display:grid;grid-template-columns:auto 1fr;gap:4px 14px}
|
||||
.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;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}
|
||||
.ctx-detail dt{color:var(--muted);font-weight:600;white-space:nowrap}
|
||||
.ctx-detail dd{margin:0;color:var(--text);overflow-wrap:anywhere}
|
||||
.ctx-detail .cd-logs{margin-top:10px;padding:8px 10px;background:rgba(255,255,255,.045);border-radius:12px;font-family:ui-monospace,monospace;font-size:10.5px;max-height:120px;overflow-y:auto}
|
||||
.prop-panel{position:absolute;right:14px;top:14px;width:250px;background:rgba(10,14,24,.96);border:1px solid var(--line-strong);border-radius:18px;box-shadow:0 18px 48px rgba(0,0,0,.5);padding:14px;font-size:11px;color:var(--text);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 h4{margin:0 0 10px;font-size:12px;color:#C6BEFF;letter-spacing:.04em}
|
||||
.prop-panel .pp-row{margin-bottom:9px}
|
||||
.prop-panel label{display:block;font-size:10px;color:var(--muted);margin-bottom:4px}
|
||||
.prop-panel select,.prop-panel input{width:100%;background:rgba(255,255,255,.04);color:var(--text);border:1px solid var(--line);border-radius:10px;padding:6px 8px;font-size:11px}
|
||||
.prop-panel .pp-thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:5px;margin-top:5px}
|
||||
.prop-panel .pp-thumb{width:100%;aspect-ratio:1/1;background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:3px}
|
||||
.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-panel .pp-thumb.active{border-color:rgba(138,124,255,.7);box-shadow:0 0 0 2px rgba(138,124,255,.18)}
|
||||
.prop-picker{position:fixed;inset:0;background:rgba(3,5,10,.68);z-index:1100;display:flex;align-items:center;justify-content:center}
|
||||
.prop-picker-box{background:rgba(10,14,24,.98);border:1px solid var(--line-strong);border-radius:20px;padding:16px;max-width:520px;max-height:80vh;overflow-y:auto;color:var(--text)}
|
||||
.prop-picker-box h3{margin:0 0 12px;font-size:13px;color:#C6BEFF}
|
||||
.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{background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:14px;padding:7px;cursor:pointer;text-align:center}
|
||||
.prop-pick:hover{border-color:rgba(138,124,255,.6)}
|
||||
.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)}
|
||||
body[data-edit-mode="true"] .desk:hover,body[data-edit-mode="true"] .char:hover,body[data-edit-mode="true"] .obj:hover{outline:2px solid #6366F1;z-index:30}
|
||||
body[data-edit-mode="true"] .dragging{cursor:grabbing!important;opacity:.7;outline:2px solid #FB923C!important;z-index:40}
|
||||
body[data-edit-mode="true"] .selected{outline:2px solid #F472B6!important;box-shadow:0 0 0 4px rgba(244,114,182,.25);z-index:35}
|
||||
.prop-pick .pp-name{font-size:10px;color:var(--muted);margin-top:5px;word-break:break-all}
|
||||
body[data-edit-mode="true"] .stage{background-image:linear-gradient(rgba(138,124,255,.18) 1px,transparent 1px),linear-gradient(90deg,rgba(138,124,255,.18) 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(138,124,255,.45)}
|
||||
body[data-edit-mode="true"] .desk:hover,body[data-edit-mode="true"] .char:hover,body[data-edit-mode="true"] .obj:hover{outline:2px solid rgba(138,124,255,.8);z-index:30}
|
||||
body[data-edit-mode="true"] .dragging{cursor:grabbing!important;opacity:.72;outline:2px solid var(--warning)!important;z-index:40}
|
||||
body[data-edit-mode="true"] .selected{outline:2px solid #F472B6!important;box-shadow:0 0 0 4px rgba(244,114,182,.22);z-index:35}
|
||||
body[data-edit-mode="true"] .char .shadow{display:none}
|
||||
footer{padding:8px 16px 12px;border-top:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.25);font-size:11px;color:var(--muted)}.progress{height:5px;background:rgba(255,255,255,.08);margin-bottom:6px}.bar{height:100%;width:0;background:var(--accent);transition:width .25s}
|
||||
@media (max-width:1180px){
|
||||
.workspace{grid-template-columns:220px minmax(560px,1fr)}
|
||||
.mission-panel{display:none}
|
||||
}
|
||||
@media (max-width:900px){
|
||||
body{overflow:auto}
|
||||
.office-app{height:auto;min-height:100vh;grid-template-rows:auto auto auto auto;padding:14px}
|
||||
.topbar{flex-wrap:wrap}
|
||||
.topbar-center{order:3;width:100%;justify-content:flex-start}
|
||||
.workspace{grid-template-columns:1fr}
|
||||
.team-panel{display:none}
|
||||
.mission-panel{display:block}
|
||||
.office-shell{min-height:620px}
|
||||
.mission-title{max-width:calc(100vw - 140px)}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -138,6 +138,7 @@ function setSprite(role,mode,frame=0,dir=0){
|
||||
const a=anim[role]; if(!a) return;
|
||||
a.mode=mode;a.frame=frame;a.dir=dir;
|
||||
ch.classList.toggle('walking',mode==='walk');
|
||||
ch.classList.toggle('working',mode==='work');
|
||||
const img=ch.querySelector('img');
|
||||
if(mode==='walk'){
|
||||
// 4방향 walk sprite — 좌우 sprite를 따로 제공하므로 scaleX 반전은 *안* 한다.
|
||||
@@ -148,7 +149,9 @@ function setSprite(role,mode,frame=0,dir=0){
|
||||
img.src=png('walk-r'+a.row+'-d'+_faceToWalkDir(a.face)+'-f0');
|
||||
img.style.transform='none';
|
||||
} else if(mode==='work'){
|
||||
img.src=png('work-r'+a.row+'-f'+frame);
|
||||
// 기존 work sprite 는 불꽃 연출이 과도해 제품 톤을 깨뜨렸다.
|
||||
// 작업 중임은 CSS focus treatment 로 표현하고 캐릭터 본체는 idle 계열을 유지.
|
||||
img.src=png('idle-r'+a.row+'-f'+(frame%2));
|
||||
img.style.transform=(a.face==='R'?'scaleX(-1)':'none');
|
||||
} else {
|
||||
img.src=png('idle-r'+a.row+'-f'+frame);
|
||||
@@ -266,13 +269,14 @@ function sendHome(role,mode='sit'){
|
||||
walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode));
|
||||
}
|
||||
setInterval(()=>{
|
||||
if(!['idle','done'].includes(_prevStatus || 'idle')) return;
|
||||
const free=Object.keys(chars).filter(k=>anim[k]?.mode==='sit'&&!chars[k].classList.contains('active'));
|
||||
if(!free.length)return;
|
||||
const k=free[Math.floor(Math.random()*free.length)],st=stationByKey[k];
|
||||
if(!st || !Array.isArray(st.roam) || !st.roam.length || !Array.isArray(st.dock)) return;
|
||||
const pt=st.roam[Math.floor(Math.random()*st.roam.length)];
|
||||
walkPath(k,[st.dock,pt,st.dock,[st.seatX,st.seatY]],()=>setSprite(k,'sit'));
|
||||
},5600);
|
||||
},9000);
|
||||
function activate(role){Object.keys(chars).forEach(k=>chars[k].classList.toggle('active',k===role))}
|
||||
function bubble(role,text){const ch=chars[role];if(!ch||!text)return;const b=document.createElement('div');b.className='bubble';b.textContent=text;b.style.left=(parseFloat(ch.style.left)+28)+'px';b.style.top=(parseFloat(ch.style.top)-6)+'px';stage.appendChild(b);setTimeout(()=>b.remove(),2400)}
|
||||
|
||||
@@ -346,19 +350,43 @@ function routeBubble(b){
|
||||
const role = roleMap[b?.agentId] || 'ceo';
|
||||
bubble(role, b?.text || '');
|
||||
}
|
||||
const STATUS_COPY = {
|
||||
idle: { label:'대기 중', note:'새로운 작업 요청을 기다리고 있습니다.', tone:'neutral' },
|
||||
intake: { label:'요청 수신', note:'요청을 읽고 작업 범위를 정리하고 있습니다.', tone:'neutral' },
|
||||
analyzing: { label:'의도 분석', note:'목표와 제약을 정리하는 중입니다.', tone:'neutral' },
|
||||
need_clarification: { label:'확인 필요', note:'결정을 위해 추가 입력이 필요합니다.', tone:'warning' },
|
||||
contract_ready: { label:'브리프 확정', note:'요구사항을 실행 가능한 형태로 정리했습니다.', tone:'neutral' },
|
||||
planning: { label:'설계 중', note:'실행 순서와 담당을 배치하고 있습니다.', tone:'neutral' },
|
||||
executing: { label:'실행 중', note:'담당 에이전트가 실제 작업을 진행 중입니다.', tone:'neutral' },
|
||||
reviewing: { label:'검수 중', note:'산출물의 완성도와 리스크를 점검하고 있습니다.', tone:'neutral' },
|
||||
waiting_approval: { label:'승인 대기', note:'다음 진행을 위해 사용자의 판단을 기다립니다.', tone:'warning' },
|
||||
error: { label:'주의 필요', note:'흐름을 멈춘 이슈를 확인해야 합니다.', tone:'danger' },
|
||||
done: { label:'완료', note:'이번 작업 라운드가 정리되었습니다.', tone:'success' },
|
||||
};
|
||||
function _statusMeta(status){
|
||||
return STATUS_COPY[status] || STATUS_COPY.idle;
|
||||
}
|
||||
function _pct(v){
|
||||
return Math.max(0, Math.min(100, Math.round((typeof v === 'number' ? v : 0) * 100)));
|
||||
}
|
||||
let _prevStatus = null;
|
||||
let _lastRenderedLog = null;
|
||||
function apply(s){
|
||||
_lastState = s; // D. 컨텍스트 메뉴 / 세부보기에서 사용.
|
||||
const st = s?.status || 'idle';
|
||||
const meta = _statusMeta(st);
|
||||
// 정적 갱신은 *항상* — 헤더/태스크/단계/로그/프로그레스.
|
||||
document.getElementById('status').textContent = st;
|
||||
document.getElementById('status').className = 'status s-' + st;
|
||||
const statusEl = document.getElementById('status');
|
||||
const phasePill = document.getElementById('phasePill');
|
||||
if(statusEl) statusEl.textContent = meta.label;
|
||||
if(phasePill) phasePill.dataset.tone = meta.tone;
|
||||
document.getElementById('agent').textContent = s?.agentName || 'Astra';
|
||||
document.getElementById('task').textContent = s?.currentTask || '—';
|
||||
document.getElementById('step').textContent = s?.currentStep || '—';
|
||||
document.getElementById('task').textContent = s?.currentTask || '새 요청을 기다리고 있습니다.';
|
||||
document.getElementById('step').textContent = s?.currentStep || meta.label;
|
||||
document.getElementById('phaseLabel').textContent = meta.label;
|
||||
document.getElementById('phaseNote').textContent = meta.note;
|
||||
const lastLog = (s?.recentLogs||[]).slice(-1)[0];
|
||||
document.getElementById('log').textContent = lastLog || '—';
|
||||
document.getElementById('log').textContent = lastLog || '아직 기록된 활동이 없습니다.';
|
||||
// 작업 중 (executing/reviewing) 상태에 활성 에이전트 머리 위 말풍선 — 로그 변할 때마다.
|
||||
if(lastLog && lastLog !== _lastRenderedLog){
|
||||
_lastRenderedLog = lastLog;
|
||||
@@ -372,7 +400,29 @@ function apply(s){
|
||||
_bubbleFromLog(r, lastLog);
|
||||
}
|
||||
}
|
||||
document.getElementById('bar').style.width = Math.round((s?.progress||0)*100) + '%';
|
||||
const progressPct = _pct(s?.progress);
|
||||
document.getElementById('bar').style.width = progressPct + '%';
|
||||
document.getElementById('progressLabel').textContent = progressPct + '%';
|
||||
const attentionCard = document.getElementById('attentionCard');
|
||||
const attentionTitle = document.getElementById('attentionTitle');
|
||||
const attentionBody = document.getElementById('attentionBody');
|
||||
if(s?.awaitingApproval){
|
||||
attentionCard.dataset.tone = 'warning';
|
||||
attentionTitle.textContent = '승인 필요';
|
||||
attentionBody.textContent = s.awaitingApproval;
|
||||
} else if(Array.isArray(s?.needUserInput) && s.needUserInput.length){
|
||||
attentionCard.dataset.tone = 'warning';
|
||||
attentionTitle.textContent = '추가 입력 필요';
|
||||
attentionBody.textContent = s.needUserInput[0];
|
||||
} else if(st === 'error'){
|
||||
attentionCard.dataset.tone = 'danger';
|
||||
attentionTitle.textContent = '이슈 감지';
|
||||
attentionBody.textContent = lastLog || meta.note;
|
||||
} else {
|
||||
attentionCard.dataset.tone = '';
|
||||
attentionTitle.textContent = '없음';
|
||||
attentionBody.textContent = '현재 막힘 없이 진행 중입니다.';
|
||||
}
|
||||
// B. 미니 맵 렌더 — pipelineStages가 있을 때만 보임.
|
||||
const mm = document.getElementById('miniMap');
|
||||
const stages = s?.pipelineStages;
|
||||
@@ -555,13 +605,12 @@ function _renderTicker(){
|
||||
const track = document.getElementById('tickerTrack');
|
||||
if(_tickerItems.length === 0){ wrap.style.display = 'none'; return; }
|
||||
wrap.style.display = 'block';
|
||||
// 무한 스크롤처럼 보이게 동일 리스트 2벌 연결.
|
||||
const html = _tickerItems.map(it => {
|
||||
const cls = _classifyTickerItem(it.text);
|
||||
const ag = it.agentId ? '<span class="tk-agent">'+ it.agentId +'</span>' : '';
|
||||
return '<span class="tk-item '+ cls +'">'+ ag + (it.text || '').replace(/[<>]/g,'') +'</span>';
|
||||
}).join('');
|
||||
track.innerHTML = html + html;
|
||||
track.innerHTML = html;
|
||||
}
|
||||
// ─────────────── Dual-mode message handler (refactor #D) ───────────────
|
||||
// 옛 pixelOfficeUpdate/Activity 와 새 officeSnapshot 둘 다 listen. 첫 officeSnapshot
|
||||
@@ -576,6 +625,51 @@ function _phaseToStatus(phase){
|
||||
if(phase === 'intake') return 'analyzing';
|
||||
return phase || 'idle';
|
||||
}
|
||||
const ROLE_LABELS = {
|
||||
ceo:'CEO',
|
||||
planner:'기획',
|
||||
researcher:'리서치',
|
||||
designer:'디자인',
|
||||
developer:'개발',
|
||||
qa:'QA',
|
||||
inspector:'감리',
|
||||
support:'지원',
|
||||
writer:'문서',
|
||||
};
|
||||
const ROLE_SHORT = {
|
||||
ceo:'CEO',
|
||||
planner:'PL',
|
||||
researcher:'RS',
|
||||
designer:'UX',
|
||||
developer:'DEV',
|
||||
qa:'QA',
|
||||
inspector:'PO',
|
||||
support:'PM',
|
||||
writer:'WR',
|
||||
};
|
||||
function _renderRoster(roster, activeAgentId){
|
||||
const wrap = document.getElementById('rosterList');
|
||||
const count = document.getElementById('rosterCount');
|
||||
if(!wrap || !count) return;
|
||||
const list = Array.isArray(roster) ? roster : [];
|
||||
count.textContent = String(list.length);
|
||||
if(!list.length){
|
||||
wrap.innerHTML = '<div class="roster-item"><div class="roster-copy"><strong>등록된 팀이 없습니다</strong><span class="roster-meta">회사 모드를 켜면 라인업이 표시됩니다.</span></div></div>';
|
||||
return;
|
||||
}
|
||||
wrap.innerHTML = list.map((r)=>{
|
||||
const active = r.agentId === activeAgentId;
|
||||
const role = ROLE_LABELS[r.roleCategory] || r.roleCategory || '지원';
|
||||
const short = ROLE_SHORT[r.roleCategory] || 'AG';
|
||||
return '<div class="roster-item '+(active?'active':'')+'" data-agent="'+(r.agentId||'')+'">'+
|
||||
'<div class="roster-avatar">'+short+'</div>'+
|
||||
'<div class="roster-copy">'+
|
||||
'<strong>'+(r.agentName || r.agentId || 'Agent')+'</strong>'+
|
||||
'<div class="roster-meta"><span class="roster-status"></span><span>'+role+'</span></div>'+
|
||||
'</div>'+
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
// refactor #G-full — roster 에 있는 agent 중 desk 가 없는 경우 자동 생성.
|
||||
// 한 번 처리된 agentId 는 _autoDeskedFor 에 기록 → 사용자가 그 desk 를 지워도 재생성 안 함.
|
||||
const _autoDeskedFor = new Set();
|
||||
@@ -589,6 +683,7 @@ function _roleCategoryToCharRow(role){
|
||||
case 'qa': return 5;
|
||||
case 'inspector': return 6;
|
||||
case 'support': return 7;
|
||||
case 'writer': return 1;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
@@ -641,6 +736,7 @@ function _ensureRosterDesks(roster){
|
||||
function applyFromSnapshot(snap){
|
||||
if(!snap) return;
|
||||
const roster = Array.isArray(snap.roster) ? snap.roster : [];
|
||||
_renderRoster(roster, snap.activeAgentId);
|
||||
_ensureRosterDesks(roster);
|
||||
const active = (snap.activeAgentId && roster.find(a => a.agentId === snap.activeAgentId)) || roster[0];
|
||||
const synthetic = {
|
||||
@@ -872,7 +968,7 @@ function _setEdit(on){
|
||||
_editMode=!!on;
|
||||
document.body.dataset.editMode = _editMode?'true':'false';
|
||||
document.getElementById('editToolbar').style.display = _editMode?'flex':'none';
|
||||
document.getElementById('editBtn').textContent = _editMode?'✓ 편집 종료':'✏️ 편집';
|
||||
document.getElementById('editBtn').textContent = _editMode?'편집 종료':'Customize';
|
||||
if(_editMode){
|
||||
_snapshotBeforeEdit = _snapshotLayout();
|
||||
} else {
|
||||
|
||||
@@ -49,28 +49,99 @@ export interface CalendarConfig {
|
||||
connectedAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings + globalState 두 곳에서 읽어 merge.
|
||||
*
|
||||
* • VS Code Settings (g1nation.google.*) — 사용자가 직접 편집 가능한 필드:
|
||||
* clientId, clientSecret, calendarId, defaultDurationMinutes, icalUrl, daysAhead
|
||||
* 이 값이 채워져 있으면 globalState 의 같은 필드보다 *우선*.
|
||||
*
|
||||
* • globalState (CAL_CONFIG_KEY) — 마법사가 자동 관리하는 secret / runtime 필드:
|
||||
* refreshToken, accessToken, accessTokenExpiresAt, connectedAs, connectedAt, lastFetchAt
|
||||
* 사용자는 settings 에서 안 보임. 마법사가 OAuth 완료 후 자동 기록.
|
||||
*
|
||||
* • 옛 사용자 호환: globalState 에 clientId 같은 게 남아있어도 settings 가 비면
|
||||
* globalState 값으로 fallback. 명시적으로 settings 에 비워두면 globalState 도 무시.
|
||||
*/
|
||||
export function readCalendarConfig(context: vscode.ExtensionContext): CalendarConfig {
|
||||
const raw = context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig> | undefined;
|
||||
const raw = (context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig> | undefined) ?? {};
|
||||
const s = vscode.workspace.getConfiguration('g1nation.google');
|
||||
const fromSettings = <T>(key: string): T | undefined => {
|
||||
const v = s.get<T>(key);
|
||||
// 빈 문자열은 "미설정" 으로 취급 — 사용자가 지운 케이스.
|
||||
if (typeof v === 'string' && v.trim() === '') return undefined;
|
||||
return v;
|
||||
};
|
||||
const clientId = fromSettings<string>('clientId') ?? (typeof raw.clientId === 'string' ? raw.clientId : undefined);
|
||||
const clientSecret = fromSettings<string>('clientSecret') ?? (typeof raw.clientSecret === 'string' ? raw.clientSecret : undefined);
|
||||
const calendarId = fromSettings<string>('calendarId') ?? (typeof raw.calendarId === 'string' ? raw.calendarId : undefined);
|
||||
const defaultDurationMinutes = fromSettings<number>('defaultEventDurationMinutes')
|
||||
?? (typeof raw.defaultDurationMinutes === 'number' ? raw.defaultDurationMinutes : undefined);
|
||||
const icalUrl = fromSettings<string>('icalUrl') ?? (typeof raw.icalUrl === 'string' ? raw.icalUrl : '');
|
||||
const daysAhead = fromSettings<number>('icalDaysAhead') ?? (typeof raw.daysAhead === 'number' && raw.daysAhead > 0 ? raw.daysAhead : 14);
|
||||
return {
|
||||
icalUrl: typeof raw?.icalUrl === 'string' ? raw.icalUrl : '',
|
||||
daysAhead: typeof raw?.daysAhead === 'number' && raw.daysAhead > 0 ? raw.daysAhead : 14,
|
||||
lastFetchAt: typeof raw?.lastFetchAt === 'string' ? raw.lastFetchAt : undefined,
|
||||
clientId: typeof raw?.clientId === 'string' ? raw.clientId : undefined,
|
||||
clientSecret: typeof raw?.clientSecret === 'string' ? raw.clientSecret : undefined,
|
||||
refreshToken: typeof raw?.refreshToken === 'string' ? raw.refreshToken : undefined,
|
||||
calendarId: typeof raw?.calendarId === 'string' ? raw.calendarId : undefined,
|
||||
defaultDurationMinutes: typeof raw?.defaultDurationMinutes === 'number' ? raw.defaultDurationMinutes : undefined,
|
||||
accessToken: typeof raw?.accessToken === 'string' ? raw.accessToken : undefined,
|
||||
accessTokenExpiresAt: typeof raw?.accessTokenExpiresAt === 'number' ? raw.accessTokenExpiresAt : undefined,
|
||||
connectedAs: typeof raw?.connectedAs === 'string' ? raw.connectedAs : undefined,
|
||||
connectedAt: typeof raw?.connectedAt === 'string' ? raw.connectedAt : undefined,
|
||||
icalUrl: icalUrl ?? '',
|
||||
daysAhead: daysAhead ?? 14,
|
||||
lastFetchAt: typeof raw.lastFetchAt === 'string' ? raw.lastFetchAt : undefined,
|
||||
clientId,
|
||||
clientSecret,
|
||||
refreshToken: typeof raw.refreshToken === 'string' ? raw.refreshToken : undefined,
|
||||
calendarId,
|
||||
defaultDurationMinutes,
|
||||
accessToken: typeof raw.accessToken === 'string' ? raw.accessToken : undefined,
|
||||
accessTokenExpiresAt: typeof raw.accessTokenExpiresAt === 'number' ? raw.accessTokenExpiresAt : undefined,
|
||||
connectedAs: typeof raw.connectedAs === 'string' ? raw.connectedAs : undefined,
|
||||
connectedAt: typeof raw.connectedAt === 'string' ? raw.connectedAt : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch 의 필드를 settings 또는 globalState 의 적절한 곳에 분기 저장.
|
||||
* • settings 로 가는 것 (사용자 편집 가능): clientId, clientSecret, calendarId,
|
||||
* defaultDurationMinutes, icalUrl, daysAhead
|
||||
* • globalState 로 가는 것 (secret / runtime): refreshToken, accessToken,
|
||||
* accessTokenExpiresAt, connectedAs, connectedAt, lastFetchAt
|
||||
*
|
||||
* 한 번에 양쪽을 patch 해도 OK — 분기 자동.
|
||||
*/
|
||||
export async function writeCalendarConfig(context: vscode.ExtensionContext, patch: Partial<CalendarConfig>): Promise<void> {
|
||||
const cur = readCalendarConfig(context);
|
||||
const next: CalendarConfig = { ...cur, ...patch };
|
||||
await context.globalState.update(CAL_CONFIG_KEY, next);
|
||||
// Settings (g1nation.google.*) 로 가는 필드들.
|
||||
const s = vscode.workspace.getConfiguration('g1nation.google');
|
||||
const settingsKeys: Array<[keyof CalendarConfig, string]> = [
|
||||
['clientId', 'clientId'],
|
||||
['clientSecret', 'clientSecret'],
|
||||
['calendarId', 'calendarId'],
|
||||
['defaultDurationMinutes', 'defaultEventDurationMinutes'],
|
||||
['icalUrl', 'icalUrl'],
|
||||
['daysAhead', 'icalDaysAhead'],
|
||||
];
|
||||
for (const [src, dst] of settingsKeys) {
|
||||
if (src in patch) {
|
||||
const v = (patch as any)[src];
|
||||
// undefined → settings 에서 제거 (default 로 복귀). 빈 문자열도 동일 취급.
|
||||
const toWrite = (v === undefined || v === '') ? undefined : v;
|
||||
try { await s.update(dst, toWrite, vscode.ConfigurationTarget.Global); }
|
||||
catch { /* settings 쓰기 실패 시 globalState 로 fallback (다음 read 가 globalState 봄). */
|
||||
const cur = (context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig>) ?? {};
|
||||
await context.globalState.update(CAL_CONFIG_KEY, { ...cur, [src]: v });
|
||||
}
|
||||
}
|
||||
}
|
||||
// GlobalState 로 가는 secret / runtime 필드.
|
||||
const globalKeys: Array<keyof CalendarConfig> = [
|
||||
'refreshToken', 'accessToken', 'accessTokenExpiresAt',
|
||||
'connectedAs', 'connectedAt', 'lastFetchAt',
|
||||
];
|
||||
const cur = (context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig>) ?? {};
|
||||
const next: Partial<CalendarConfig> = { ...cur };
|
||||
let dirty = false;
|
||||
for (const k of globalKeys) {
|
||||
if (k in patch) {
|
||||
(next as any)[k] = (patch as any)[k];
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if (dirty) await context.globalState.update(CAL_CONFIG_KEY, next);
|
||||
}
|
||||
|
||||
/** 회사 디렉토리 내부 캐시 파일 경로. workspace 없으면 globalStorage 로 fallback. */
|
||||
|
||||
+17
-1
@@ -669,6 +669,22 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
})();
|
||||
this._view?.webview.postMessage(payload);
|
||||
this._pixelOfficePanel?.webview.postMessage(payload);
|
||||
if (cfg.companyPixelOfficeEnabled) {
|
||||
try {
|
||||
const state = payload.value.state ?? undefined;
|
||||
const snapshot = presentOfficeSnapshot({
|
||||
activeState: state ?? undefined,
|
||||
recentBubbles: [],
|
||||
recentActivity: this._pixelOfficeActivity,
|
||||
roster: this._buildOfficeRoster(),
|
||||
});
|
||||
const snapMsg = { type: 'officeSnapshot' as const, value: snapshot };
|
||||
this._view?.webview.postMessage(snapMsg);
|
||||
this._pixelOfficePanel?.webview.postMessage(snapMsg);
|
||||
} catch {
|
||||
// Snapshot 재전송 실패가 기존 fallback 경로를 깨면 안 됨.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -683,7 +699,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
}
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'astra.pixelOffice',
|
||||
'Pixel Office',
|
||||
'Astra Office',
|
||||
column,
|
||||
{ enableScripts: true, localResourceRoots: [this._extensionUri], retainContextWhenHidden: true },
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user