claude-ai-office 0.29.5 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/ARCHITECTURE.md +4 -1
- package/lib/state.js +57 -2
- package/package.json +1 -1
- package/public/js/i18n.js +2 -0
- package/public/js/scene/actors.js +4 -1
- package/public/js/store.js +2 -0
- package/scripts/update-homebrew.js +2 -1
package/README.en.md
CHANGED
|
@@ -61,7 +61,7 @@ node bin/ai-office.js # or: npm run install-hooks && npm start
|
|
|
61
61
|
|
|
62
62
|
| Area | Contents |
|
|
63
63
|
|---|---|
|
|
64
|
-
| **Left: Floors** | One floor per git project (repository root). Worktrees share the floor and show their branch (⎇). Opening a floor lists its sessions with `waiting` and `viewing` badges. By default only the selected floor is open; **Expand all ↔ Collapse all** in the header opens or closes every floor's session list at once (clicking a floor while all are collapsed opens that floor; remembered in the browser). 🎨 on the selected floor picks that floor's interior preset (Warm Wood · Cool Gray · Mint · Lavender · Sand · Pastel; assigned automatically by default). "Follow the latest session" (`A`) moves the selection to the most recently active session; when it is off, the last floor and session you looked at are restored next time |
|
|
64
|
+
| **Left: Floors** | One floor per git project (repository root). Worktrees share the floor and show their branch (⎇). A session started in a parent folder that holds several repositories (not itself a git repo) moves to the floor of the repository it actually works in: the one with the most edited files, or the most read files before any edit (ties keep the current floor). Resume commands still use the folder it was started in. Opening a floor lists its sessions with `waiting` and `viewing` badges. By default only the selected floor is open; **Expand all ↔ Collapse all** in the header opens or closes every floor's session list at once (clicking a floor while all are collapsed opens that floor; remembered in the browser). 🎨 on the selected floor picks that floor's interior preset (Warm Wood · Cool Gray · Mint · Lavender · Sand · Pastel; assigned automatically by default). "Follow the latest session" (`A`) moves the selection to the most recently active session; when it is off, the last floor and session you looked at are restored next time |
|
|
65
65
|
| **Center: Office** | Drag to pan, wheel or buttons to zoom, `⟲` to reset, pinch on touch screens. Click a character to select its session; hover for a tooltip (current action, target file, prompt, branch, path). 📍 **Follow** (`F`) keeps the camera on the selected session's Claude. 🏢 **Building view** (`O`) appears when there are two or more floors |
|
|
66
66
|
| **Right panel** | Session card (project path · session id · character name · copy the resume command · 🗑 dismiss ended sessions), people in the office (room · speech bubble · elapsed time), activity log (search with `/`, selected session only), legend (click the title to collapse) |
|
|
67
67
|
|
|
@@ -159,7 +159,7 @@ With two or more floors, 🏢 **Building view** (`O`) appears in the header. Flo
|
|
|
159
159
|
|
|
160
160
|
A resident app that shows the web UI in a window. It starts the Node server itself (or reuses one that is already running) and lives in the menu bar / system tray; left- or right-click the icon for the menu: Open, Hide, Always on top, Launch at login, Reinstall hooks, Check for updates, Apply update and Quit. When a session is waiting for you or an update is ready, a **red dot** appears on the menu bar icon and on the Dock (Windows: taskbar) icon, like a chat app; there is no count, and the tooltip says what it is. **Launch at login** is turned on at first run; when started at login the app stays in the menu bar without a window. You can turn it off from the menu. Closing the window hides it; Quit stops the server too. Node is bundled: on first launch the app copies the server files to `~/.ai-office/app` and the Node binary to `~/.ai-office/node`, then registers the hooks, so moving or deleting the app does not break them.
|
|
161
161
|
|
|
162
|
-
**Updates**: the app checks for a new version 10 seconds after launch and every 6 hours and only **downloads** it in the background (signature verified; a failed check is retried after 5 minutes), then shows the red dot and enables "Apply vX update (restart)". Applying stops the server, installs and restarts; nothing is installed without asking and the running app is never replaced underneath you. "Check for updates"
|
|
162
|
+
**Updates**: the app checks for a new version 10 seconds after launch and every 6 hours and only **downloads** it in the background (signature verified; a failed check is retried after 5 minutes), then shows the red dot and enables "Apply vX update (restart)". Applying stops the server, installs and restarts; nothing is installed without asking and the running app is never replaced underneath you. Clicking **"Check for updates"** opens a small update window: it checks, and if there is a new version shows a `Downloading… 12.3MB / 42.8MB` progress bar (cancelable), then installing and restart; otherwise it says you are up to date or why the check failed. Applying a downloaded update shows "Installing…" in the same window. Releases are built by GitHub Actions for macOS (Apple Silicon) and Windows (x64) and signed for the updater.
|
|
163
163
|
|
|
164
164
|
> **Download**: [macOS AI-Office-mac-arm64.dmg](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-mac-arm64.dmg) · [Windows AI-Office-windows-x64-setup.exe](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-windows-x64-setup.exe) (always the latest; older versions in [Releases](https://github.com/cozyuj/AI-office/releases)). The macOS app is not notarized: **right-click → Open** the first time; on macOS 15+ use System Settings → Privacy & Security → **Open Anyway**, and if it still says "damaged", run `xattr -dr com.apple.quarantine "/Applications/AI Office.app"` once. To build it yourself:
|
|
165
165
|
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ node bin/ai-office.js # 또는 npm run install-hooks && npm start
|
|
|
63
63
|
|
|
64
64
|
| 영역 | 내용 |
|
|
65
65
|
|---|---|
|
|
66
|
-
| **좌측 층별 안내** | git 프로젝트(저장소 루트) = 층. worktree 는 같은 층으로 묶이고 브랜치(⎇)가 함께 보인다. 층을 열면 세션 목록 — `답 기다림`, `지금 보는 중` 표시. 기본은 선택한 층만 열리고, 머리의 **모두 펼치기 ↔ 모두 접기** 로 모든 층의 세션 목록을 한꺼번에 열고 닫는다(모두 접힌 상태에서 층을 누르면 그 층이 열림, 브라우저에 기억). 선택한 층의 🎨 로 그 층 인테리어 스타일(따뜻한 우드·쿨 그레이·민트·라벤더·샌드·파스텔, 기본은 층마다 자동 배정)을 고른다. "최근 세션 자동으로 따라가기"(`A`)를 켜면 최신 활동 세션으로 선택이 옮겨가고, 끄면 마지막에 보던 층·세션을 다음에 열 때 기억한다 |
|
|
66
|
+
| **좌측 층별 안내** | git 프로젝트(저장소 루트) = 층. worktree 는 같은 층으로 묶이고 브랜치(⎇)가 함께 보인다. 여러 저장소를 담은 상위 폴더(git 저장소 아님)에서 연 세션은 실제로 작업하는 저장소 층으로 옮겨 간다 — 고친 파일이 가장 많은 저장소, 아직 고친 게 없으면 가장 많이 본 저장소(동점이면 그대로). 이어가기 명령은 처음 연 폴더 기준. 층을 열면 세션 목록 — `답 기다림`, `지금 보는 중` 표시. 기본은 선택한 층만 열리고, 머리의 **모두 펼치기 ↔ 모두 접기** 로 모든 층의 세션 목록을 한꺼번에 열고 닫는다(모두 접힌 상태에서 층을 누르면 그 층이 열림, 브라우저에 기억). 선택한 층의 🎨 로 그 층 인테리어 스타일(따뜻한 우드·쿨 그레이·민트·라벤더·샌드·파스텔, 기본은 층마다 자동 배정)을 고른다. "최근 세션 자동으로 따라가기"(`A`)를 켜면 최신 활동 세션으로 선택이 옮겨가고, 끄면 마지막에 보던 층·세션을 다음에 열 때 기억한다 |
|
|
67
67
|
| **중앙 사무실** | 드래그 이동, 휠/버튼 줌, `⟲` 초기 위치. 캐릭터 클릭 → 세션 선택, 캐릭터에 마우스를 올리면 툴팁(지금 하는 일·대상 파일·요청·브랜치·경로). 📍 **따라가기**(`F`) = 카메라가 선택 세션의 Claude 를 쫓아감. 🏢 **전체 보기**(`O`)는 층이 둘 이상일 때 나타난다 |
|
|
68
68
|
| **우측 패널** | 세션 카드(프로젝트 경로 · 세션 id · 캐릭터 이름 · 이어가기 명령 복사 · 끝난 세션 🗑 치우기), 사무실 사람들(방·말풍선·경과), 활동 로그(검색 `/`, 선택 세션만), 범례(제목 클릭으로 접기) |
|
|
69
69
|
|
|
@@ -161,7 +161,7 @@ node bin/ai-office.js # 또는 npm run install-hooks && npm start
|
|
|
161
161
|
|
|
162
162
|
웹 버전을 그대로 창에 담은 상주 앱입니다(맥 메뉴바 · Windows 트레이). 앱이 Node 서버를 직접 띄우고(이미 떠 있으면 재사용) 아이콘을 왼쪽이나 오른쪽 클릭하면 메뉴가 뜹니다. 메뉴는 열기 · 숨기기 · 항상 위 · 로그인 시 자동 실행 · 훅 다시 설치 · 업데이트 확인 · 업데이트 적용 · 종료입니다. 답을 기다리는 세션이 있거나 받아 둔 업데이트가 있으면 카카오톡처럼 메뉴바 아이콘과 Dock(Windows 는 작업 표시줄) 아이콘에 **빨간 점**이 뜹니다. 숫자는 없고, 무엇인지는 아이콘에 마우스를 올리면 툴팁으로 보입니다. **로그인 시 자동 실행**은 처음 실행 때 켜지며, 자동으로 켜질 땐 창 없이 메뉴바에만 뜹니다. 메뉴에서 끌 수 있습니다. 창을 닫아도 메뉴바에 남고, 종료하면 서버도 함께 내려갑니다(앱이 강제 종료돼도 서버가 스스로 끝납니다). 앱에는 Node 가 내장되어 있고, 첫 실행 때 서버 파일을 `~/.ai-office/app` 에, Node 를 `~/.ai-office/node` 에 복사하고 그 경로로 훅을 등록하므로 앱을 옮기거나 지워도 훅이 깨지지 않습니다.
|
|
163
163
|
|
|
164
|
-
**자동 업데이트**: 앱이 시작 10초 뒤와 6시간마다 새 버전을 확인해 백그라운드로 **받아 두기만** 하고(서명 검증까지, 확인에 실패하면 5분 뒤 다시), 빨간 점을 띄우며 메뉴의 "vX 업데이트 적용 (다시 시작)" 이 켜집니다. 누르면 서버를 내리고 설치한 뒤 다시 시작합니다. 묻지 않고 설치하거나 쓰던 중에 앱 파일을 바꾸지 않습니다. 메뉴의 "업데이트 확인"
|
|
164
|
+
**자동 업데이트**: 앱이 시작 10초 뒤와 6시간마다 새 버전을 확인해 백그라운드로 **받아 두기만** 하고(서명 검증까지, 확인에 실패하면 5분 뒤 다시), 빨간 점을 띄우며 메뉴의 "vX 업데이트 적용 (다시 시작)" 이 켜집니다. 누르면 서버를 내리고 설치한 뒤 다시 시작합니다. 묻지 않고 설치하거나 쓰던 중에 앱 파일을 바꾸지 않습니다. 메뉴의 **"업데이트 확인"** 을 누르면 작은 업데이트 창이 떠서 확인 → 새 버전이면 `다운로드 중… 12.3MB / 42.8MB` 진행 막대(취소 가능) → 설치 중 → 다시 시작까지 보여 주고, 최신이거나 실패하면 그 이유를 보여 줍니다. 받아 둔 업데이트를 적용할 때도 같은 창에 "설치 중" 이 뜹니다. 릴리스는 태그를 올리면 GitHub Actions 가 맥(Apple Silicon)·Windows(x64) 로 빌드하고 서명해 올립니다.
|
|
165
165
|
|
|
166
166
|
> **다운로드**: ⬇️ **[맥 AI-Office-mac-arm64.dmg](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-mac-arm64.dmg)** · **[Windows AI-Office-windows-x64-setup.exe](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-windows-x64-setup.exe)** (항상 최신) · 이전 버전은 [Releases](https://github.com/cozyuj/AI-office/releases). DMG 를 열어 `AI Office.app` 을 응용 프로그램 폴더에 넣으세요. 코드 서명이 없어 처음엔 "확인되지 않은 개발자" 경고가 뜨니 앱을 **우클릭 → 열기** 로 한 번 허용하고, macOS 15 이상에서 안 열리면 시스템 설정 → 개인정보 보호 및 보안 → **그래도 열기**, 그래도 "손상됨" 이라고 나오면 터미널에서 `xattr -dr com.apple.quarantine "/Applications/AI Office.app"` 를 한 번 실행하세요. Intel 맥·Linux 는 위의 `npx claude-ai-office` 한 줄로 웹 버전을 쓰세요. 직접 빌드하려면:
|
|
167
167
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -48,7 +48,8 @@ Floor { // 프로젝트 = 층. id = git 저장소 루트 (l
|
|
|
48
48
|
|
|
49
49
|
Session {
|
|
50
50
|
id: string // hook.session_id
|
|
51
|
-
floorId: string // 처음 정해진 층 유지 (cwd 가 하위 폴더로 바뀌어도)
|
|
51
|
+
floorId: string // 처음 정해진 층 유지 (cwd 가 하위 폴더로 바뀌어도). 예외: git 저장소가 아닌 폴더에서 연 세션은
|
|
52
|
+
// 도구 경로(file_path·path·Bash 앞머리 cd)의 저장소를 세어 고친 파일 최다 → 본 횟수 최다 저장소 층으로 옮긴다 (BE-22, lib/state.js follow)
|
|
52
53
|
title: string // 훅 session_title > transcript 의 마지막 custom-title > 첫 요청 앞부분
|
|
53
54
|
firstPrompt, lastPrompt // 첫 요청(transcript 앞 1MB 에서 복원), 최근 요청
|
|
54
55
|
status: 'thinking' | 'working' | 'waiting' | 'idle' | 'ended'
|
|
@@ -257,6 +258,8 @@ AI Office.app / AI Office.exe (app/src-tauri/src/lib.rs)
|
|
|
257
258
|
│ (macOS 는 status item 의 effectiveAppearance 로 글자 색 결정), Dock 배지 / Windows 오버레이 아이콘도 같은 규칙
|
|
258
259
|
├─ 업데이터 (tauri-plugin-updater): 백그라운드에서 내려받기만 → 메뉴 "업데이트 적용" 때 서버 종료 → 설치 → 재시작.
|
|
259
260
|
│ 성공 전엔 5분마다, 성공 뒤엔 6시간마다 확인
|
|
261
|
+
│ 메뉴 "업데이트 확인"·"업데이트 적용" 은 진행 창(app/dist/update.html)을 띄운다: 확인 → 다운로드 막대(취소) → 설치 중 → 재시작.
|
|
262
|
+
│ 창은 IPC 권한 없이 Rust 가 eval 로 상태를 넣고, 버튼은 가짜 주소 이동 → on_navigation 이 받는다
|
|
260
263
|
└─ 로그인 시 자동 실행 (tauri-plugin-autostart, LaunchAgent / Run 키, --hidden). 첫 실행에 한 번 켜고 메뉴 체크박스로 끔
|
|
261
264
|
```
|
|
262
265
|
|
package/lib/state.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
7
8
|
const EventEmitter = require('events');
|
|
8
9
|
const { normalize, describeTool, ACTIVITY_ICON, ROOMS: ROOM_LABEL, cut } = require('./normalize');
|
|
9
10
|
const FS = require('./floorStyle');
|
|
@@ -36,6 +37,20 @@ function rest(bubble, sessionId, floor) {
|
|
|
36
37
|
return { room, activity: 'rest', bubble: bubble || info.bubble, icon: info.icon };
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// 작업 위치로 층 옮기기 (BE-22): 도구가 다루는 경로 → { p, edit }. 경로가 없는 도구는 null
|
|
41
|
+
const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
|
|
42
|
+
function workPath(ev) {
|
|
43
|
+
const ti = ev.toolInput || {};
|
|
44
|
+
const fp = ti.file_path || ti.notebook_path;
|
|
45
|
+
if (fp) return { p: path.dirname(fp), edit: EDIT_TOOLS.has(ev.toolName) };
|
|
46
|
+
if (ti.path && (ev.toolName === 'Grep' || ev.toolName === 'Glob' || ev.toolName === 'LS')) return { p: ti.path, edit: false };
|
|
47
|
+
if (ev.toolName === 'Bash' && ti.command) {
|
|
48
|
+
const m = /^\s*cd\s+(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))/.exec(ti.command);
|
|
49
|
+
if (m) return { p: m[1] || m[2] || m[3], edit: false };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
function defaultStat(file) {
|
|
40
55
|
try { return { mtimeMs: fs.statSync(file).mtimeMs }; } catch { return null; }
|
|
41
56
|
}
|
|
@@ -90,6 +105,8 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
|
|
|
90
105
|
const log = [];
|
|
91
106
|
const timers = []; // { at, fn } sweep(now) 가 실행
|
|
92
107
|
let floorSeq = 0;
|
|
108
|
+
// git 저장소가 아닌 폴더(여러 저장소를 담은 묶음 폴더)에서 연 세션만 (BE-22): sessionId → Map(저장소 root → { edits, reads, branch, worktree })
|
|
109
|
+
const roaming = new Map();
|
|
93
110
|
|
|
94
111
|
// ---------- 층 인테리어 프리셋 (FE-36): 직접 선택(styleFile 에 저장) > 층 id 해시 자동 배정 ----------
|
|
95
112
|
const styleOverrides = new Map(); // floorId → presetId
|
|
@@ -163,6 +180,7 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
|
|
|
163
180
|
};
|
|
164
181
|
sessions.set(s.id, s);
|
|
165
182
|
floor.sessions.add(s.id);
|
|
183
|
+
if (!proj && ev.cwd && !ev.cwd.startsWith('/demo/')) roaming.set(s.id, new Map());
|
|
166
184
|
}
|
|
167
185
|
// 제목: 훅의 session_title 이 바뀔 때마다 따른다 (분리된 세션은 부모 제목으로 시작했다가 이름이 바뀐다)
|
|
168
186
|
if (ev.sessionTitle && ev.sessionTitle !== s.hookTitle) { s.hookTitle = ev.sessionTitle; retitle(s, floor, ev.sessionTitle, now); }
|
|
@@ -172,6 +190,41 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
|
|
|
172
190
|
return { session: s, floor };
|
|
173
191
|
}
|
|
174
192
|
|
|
193
|
+
/**
|
|
194
|
+
* 묶음 폴더에서 연 세션을 실제로 작업하는 저장소 층으로 옮긴다 (BE-22).
|
|
195
|
+
* 층 = 고친 파일이 가장 많은 저장소, 아직 고친 게 없으면 가장 많이 본 저장소. 지금 층보다 많아질 때만 옮긴다(동점은 그대로).
|
|
196
|
+
* 저장소 세션은 옮기지 않는다. s.cwd(이어가기 위치)는 그대로. 옮긴 뒤의 층을 반환한다
|
|
197
|
+
*/
|
|
198
|
+
function follow(s, floor, ev, now) {
|
|
199
|
+
const counts = roaming.get(s.id);
|
|
200
|
+
const w = counts && workPath(ev);
|
|
201
|
+
if (!w) return floor;
|
|
202
|
+
let p = w.p.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
203
|
+
if (!path.isAbsolute(p)) { if (!s.cwd) return floor; p = path.resolve(s.cwd, p); }
|
|
204
|
+
const proj = resolveProject(p);
|
|
205
|
+
if (!proj) return floor;
|
|
206
|
+
const c = counts.get(proj.root) || { edits: 0, reads: 0 };
|
|
207
|
+
if (w.edit) c.edits++; else c.reads++;
|
|
208
|
+
c.branch = proj.branch; c.worktree = proj.worktree ? path.basename(proj.worktreeDir) : '';
|
|
209
|
+
counts.set(proj.root, c);
|
|
210
|
+
if (proj.root === s.floorId) return floor;
|
|
211
|
+
const cur = counts.get(s.floorId);
|
|
212
|
+
// 한 번에 한 저장소만 늘어나므로 지금 층(최다)과 그 저장소만 비교하면 된다
|
|
213
|
+
if (cur && (c.edits < cur.edits || (c.edits === cur.edits && c.reads <= cur.reads))) return floor;
|
|
214
|
+
const to = floorFor(proj.root, now);
|
|
215
|
+
floor.sessions.delete(s.id);
|
|
216
|
+
to.sessions.add(s.id);
|
|
217
|
+
s.floorId = to.id; s.branch = c.branch; s.worktree = c.worktree;
|
|
218
|
+
// 새 층 update 를 먼저 보낸다 → 화면이 선택을 새 층으로 옮긴 뒤 빈 층이 사라진다
|
|
219
|
+
emitUpdate(s, to, pushLog(now, to, s, s.agents.main || null, '🛗', `${to.name} 층으로 이동`));
|
|
220
|
+
if (floor.sessions.size === 0) {
|
|
221
|
+
floors.delete(floor.id);
|
|
222
|
+
emitter.emit('remove', { sessionId: null, floorId: floor.id, floorRemoved: true });
|
|
223
|
+
}
|
|
224
|
+
for (const f of new Set([...renameFloors(), ...refreshStyles(), ...(floors.has(floor.id) ? [floor] : [])])) if (f !== to) emitter.emit('floor', publicFloor(f));
|
|
225
|
+
return to;
|
|
226
|
+
}
|
|
227
|
+
|
|
175
228
|
function mainAgent(s, now) {
|
|
176
229
|
let a = s.agents.main;
|
|
177
230
|
if (!a) {
|
|
@@ -241,7 +294,8 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
|
|
|
241
294
|
if (!payload || typeof payload !== 'object') return;
|
|
242
295
|
const ev = normalize(payload);
|
|
243
296
|
if (ev.event === 'SessionEnd' && !sessions.has(ev.sessionId)) return; // 모르는 세션의 종료(치운 세션 재생 등)로 빈 층을 만들지 않는다
|
|
244
|
-
const { session: s, floor } = sessionFor(ev, now);
|
|
297
|
+
const { session: s, floor: home } = sessionFor(ev, now);
|
|
298
|
+
const floor = ev.event === 'PreToolUse' ? follow(s, home, ev, now) : home;
|
|
245
299
|
let entry = null;
|
|
246
300
|
|
|
247
301
|
switch (ev.event) {
|
|
@@ -313,7 +367,7 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
|
|
|
313
367
|
schedule(now + SUBAGENT_LEAVE_MS, () => {
|
|
314
368
|
if (s.agents[ev.agentId] && s.agents[ev.agentId].leaving) {
|
|
315
369
|
delete s.agents[ev.agentId];
|
|
316
|
-
emitUpdate(s, floor, null);
|
|
370
|
+
emitUpdate(s, floors.get(s.floorId) || floor, null);
|
|
317
371
|
}
|
|
318
372
|
});
|
|
319
373
|
break;
|
|
@@ -376,6 +430,7 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
|
|
|
376
430
|
const s = sessions.get(sessionId);
|
|
377
431
|
if (!s) return;
|
|
378
432
|
sessions.delete(sessionId);
|
|
433
|
+
roaming.delete(sessionId);
|
|
379
434
|
const f = floors.get(s.floorId);
|
|
380
435
|
if (f) {
|
|
381
436
|
f.sessions.delete(sessionId);
|
package/package.json
CHANGED
package/public/js/i18n.js
CHANGED
|
@@ -94,6 +94,8 @@ const RULES = [
|
|
|
94
94
|
[/^(.+) 사용 중$/, (m) => `using ${m[1]}`],
|
|
95
95
|
[/^(.+) 실패: (.+)$/, (m) => `${m[1]} failed: ${m[2]}`],
|
|
96
96
|
[/^허락 요청: (.+)$/, (m) => `permission request: ${m[1]}`],
|
|
97
|
+
[/^(.+) 층으로 이동$/, (m) => `moved to ${m[1]}`],
|
|
98
|
+
[/^(.+) 로 이동$/, (m) => `moving to ${m[1]}`],
|
|
97
99
|
[/^제목: (.+)$/, (m) => `title: ${m[1]}`],
|
|
98
100
|
[/^완료: (.+)$/, (m) => `done: ${m[1]}`],
|
|
99
101
|
[/^알림 수신 \((.+)\)$/, (m) => `notification (${m[1]})`],
|
|
@@ -250,7 +250,10 @@ export function createActors(store, scene, avatars) {
|
|
|
250
250
|
for (const [key, a] of actors) {
|
|
251
251
|
if (seen.has(key) || a.dead) continue;
|
|
252
252
|
a.dead = true; a.deadAt = Date.now();
|
|
253
|
-
|
|
253
|
+
// 다른 층으로 옮겨 간 세션(BE-22)은 퇴근이 아니라 엘리베이터로 이동
|
|
254
|
+
const moved = store.sessions.get(a.sessionId);
|
|
255
|
+
const to = moved && moved.floorId !== floorId ? store.floors.get(moved.floorId) : null;
|
|
256
|
+
a.bubble = to ? `${to.no}F ${to.name} 로 이동` : '퇴근합니다'; a.icon = to ? '🛗' : '👋'; a.activity = 'leave';
|
|
254
257
|
slots.release(key);
|
|
255
258
|
a.goTo([SPAWN.x, SPAWN.y]);
|
|
256
259
|
}
|
package/public/js/store.js
CHANGED
|
@@ -86,6 +86,8 @@ class Store {
|
|
|
86
86
|
this.selectedFloorId = floor.id;
|
|
87
87
|
this.selectedSessionId = session.id;
|
|
88
88
|
}
|
|
89
|
+
// 선택한 세션이 작업 저장소 층으로 옮겨 가면 선택도 따라간다 (BE-22)
|
|
90
|
+
if (prev && prev.floorId !== session.floorId && this.selectedSessionId === session.id) this.selectedFloorId = session.floorId;
|
|
89
91
|
this.reconcileSelection();
|
|
90
92
|
this.emit('update', { session, floor, log });
|
|
91
93
|
// 도구 실패 / 세션 종료 전이 — 알림 소리 범위 'all' 에서 쓴다 (첫 등장부터 ended 인 세션은 전이가 아니므로 제외)
|
|
@@ -10,7 +10,8 @@ const crypto = require('crypto');
|
|
|
10
10
|
const arg = (name, def) => { const i = process.argv.indexOf(`--${name}`); return i >= 0 ? process.argv[i + 1] : def; };
|
|
11
11
|
const ROOT = path.join(__dirname, '..');
|
|
12
12
|
const TAP = path.resolve(arg('tap', path.join(ROOT, '..', 'homebrew-tap')));
|
|
13
|
-
const VERSION = arg('version', require(path.join(ROOT, 'package.json')).version);
|
|
13
|
+
const VERSION = arg('version', require(path.join(ROOT, 'package.json')).version).replace(/^v/, '');
|
|
14
|
+
if (!/^\d+\.\d+\.\d+$/.test(VERSION)) { console.error(`버전 형식이 아닙니다: '${VERSION}' — 숫자 x.y.z 로 넣으세요 (예: 0.29.5)`); process.exit(1); }
|
|
14
15
|
|
|
15
16
|
async function sha256Of(url) {
|
|
16
17
|
const res = await fetch(url, { redirect: 'follow' });
|