@neolio42/pixel-office 0.1.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/LICENSE +21 -0
- package/README.md +136 -0
- package/bin.sh +16 -0
- package/bin.ts +162 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +7 -0
- package/package.json +51 -0
- package/postcss.config.mjs +7 -0
- package/public/assets/characters/char_0.png +0 -0
- package/public/assets/characters/char_1.png +0 -0
- package/public/assets/characters/char_2.png +0 -0
- package/public/assets/characters/char_3.png +0 -0
- package/public/assets/characters/char_4.png +0 -0
- package/public/assets/characters/char_5.png +0 -0
- package/public/assets/characters.png +0 -0
- package/public/assets/default-layout-1.json +92 -0
- package/public/assets/floors/floor_0.png +0 -0
- package/public/assets/floors/floor_1.png +0 -0
- package/public/assets/floors/floor_2.png +0 -0
- package/public/assets/floors/floor_3.png +0 -0
- package/public/assets/floors/floor_4.png +0 -0
- package/public/assets/floors/floor_5.png +0 -0
- package/public/assets/floors/floor_6.png +0 -0
- package/public/assets/floors/floor_7.png +0 -0
- package/public/assets/floors/floor_8.png +0 -0
- package/public/assets/furniture/BIN/BIN.png +0 -0
- package/public/assets/furniture/BIN/manifest.json +13 -0
- package/public/assets/furniture/BOOKSHELF/BOOKSHELF.png +0 -0
- package/public/assets/furniture/BOOKSHELF/manifest.json +13 -0
- package/public/assets/furniture/CACTUS/CACTUS.png +0 -0
- package/public/assets/furniture/CACTUS/manifest.json +13 -0
- package/public/assets/furniture/CLOCK/CLOCK.png +0 -0
- package/public/assets/furniture/CLOCK/manifest.json +13 -0
- package/public/assets/furniture/COFFEE/COFFEE.png +0 -0
- package/public/assets/furniture/COFFEE/manifest.json +13 -0
- package/public/assets/furniture/COFFEE_TABLE/COFFEE_TABLE.png +0 -0
- package/public/assets/furniture/COFFEE_TABLE/manifest.json +13 -0
- package/public/assets/furniture/CUSHIONED_BENCH/CUSHIONED_BENCH.png +0 -0
- package/public/assets/furniture/CUSHIONED_BENCH/manifest.json +13 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png +0 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_FRONT.png +0 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_SIDE.png +0 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/manifest.json +44 -0
- package/public/assets/furniture/DESK/DESK_FRONT.png +0 -0
- package/public/assets/furniture/DESK/DESK_SIDE.png +0 -0
- package/public/assets/furniture/DESK/manifest.json +33 -0
- package/public/assets/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png +0 -0
- package/public/assets/furniture/DOUBLE_BOOKSHELF/manifest.json +13 -0
- package/public/assets/furniture/HANGING_PLANT/HANGING_PLANT.png +0 -0
- package/public/assets/furniture/HANGING_PLANT/manifest.json +13 -0
- package/public/assets/furniture/LARGE_PAINTING/LARGE_PAINTING.png +0 -0
- package/public/assets/furniture/LARGE_PAINTING/manifest.json +13 -0
- package/public/assets/furniture/LARGE_PLANT/LARGE_PLANT.png +0 -0
- package/public/assets/furniture/LARGE_PLANT/manifest.json +13 -0
- package/public/assets/furniture/PC/PC_BACK.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_OFF.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_ON_1.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_ON_2.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_ON_3.png +0 -0
- package/public/assets/furniture/PC/PC_SIDE.png +0 -0
- package/public/assets/furniture/PC/manifest.json +88 -0
- package/public/assets/furniture/PLANT/PLANT.png +0 -0
- package/public/assets/furniture/PLANT/manifest.json +13 -0
- package/public/assets/furniture/PLANT_2/PLANT_2.png +0 -0
- package/public/assets/furniture/PLANT_2/manifest.json +13 -0
- package/public/assets/furniture/POT/POT.png +0 -0
- package/public/assets/furniture/POT/manifest.json +13 -0
- package/public/assets/furniture/SMALL_PAINTING/SMALL_PAINTING.png +0 -0
- package/public/assets/furniture/SMALL_PAINTING/manifest.json +13 -0
- package/public/assets/furniture/SMALL_PAINTING_2/SMALL_PAINTING_2.png +0 -0
- package/public/assets/furniture/SMALL_PAINTING_2/manifest.json +13 -0
- package/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_FRONT.png +0 -0
- package/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_SIDE.png +0 -0
- package/public/assets/furniture/SMALL_TABLE/manifest.json +33 -0
- package/public/assets/furniture/SOFA/SOFA_BACK.png +0 -0
- package/public/assets/furniture/SOFA/SOFA_FRONT.png +0 -0
- package/public/assets/furniture/SOFA/SOFA_SIDE.png +0 -0
- package/public/assets/furniture/SOFA/manifest.json +44 -0
- package/public/assets/furniture/TABLE_FRONT/TABLE_FRONT.png +0 -0
- package/public/assets/furniture/TABLE_FRONT/manifest.json +13 -0
- package/public/assets/furniture/WHITEBOARD/WHITEBOARD.png +0 -0
- package/public/assets/furniture/WHITEBOARD/manifest.json +13 -0
- package/public/assets/furniture/WOODEN_BENCH/WOODEN_BENCH.png +0 -0
- package/public/assets/furniture/WOODEN_BENCH/manifest.json +13 -0
- package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_BACK.png +0 -0
- package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_FRONT.png +0 -0
- package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_SIDE.png +0 -0
- package/public/assets/furniture/WOODEN_CHAIR/manifest.json +44 -0
- package/public/assets/walls/wall_0.png +0 -0
- package/scripts/setup.ts +158 -0
- package/server.ts +53 -0
- package/src/app/api/focus-terminal/route.ts +65 -0
- package/src/app/api/hooks/notification/route.ts +19 -0
- package/src/app/api/hooks/post-tool-use/route.ts +26 -0
- package/src/app/api/hooks/pre-tool-use/route.ts +189 -0
- package/src/app/api/hooks/session-end/route.ts +31 -0
- package/src/app/api/hooks/session-start/route.ts +47 -0
- package/src/app/api/hooks/stop/route.ts +19 -0
- package/src/app/api/hooks/user-prompt/route.ts +92 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +21 -0
- package/src/app/page.tsx +5 -0
- package/src/components/ApprovalToast.tsx +132 -0
- package/src/components/OfficeCanvas.tsx +311 -0
- package/src/components/Terminal.tsx +177 -0
- package/src/components/TerminalTile.tsx +181 -0
- package/src/components/WorkerPanel.tsx +261 -0
- package/src/components/WorkerPopup.tsx +116 -0
- package/src/game/asset-loader.ts +172 -0
- package/src/game/office-layout.ts +287 -0
- package/src/game/renderer.ts +369 -0
- package/src/game/sprites.ts +133 -0
- package/src/game/worker-entity.ts +219 -0
- package/src/hooks/usePixelOffice.ts +318 -0
- package/src/hooks/useRecentCwds.ts +27 -0
- package/src/lib/approval-queue.ts +67 -0
- package/src/lib/pty-manager.ts +267 -0
- package/src/lib/store.ts +181 -0
- package/src/lib/tool-classifier.ts +224 -0
- package/src/lib/transcript.ts +109 -0
- package/src/lib/types.ts +58 -0
- package/src/lib/ws-server.ts +270 -0
- package/tsconfig.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nedas Mackevičius
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Pixel Office
|
|
2
|
+
|
|
3
|
+
A real-time pixel art visualization of your [Claude Code](https://docs.anthropic.com/en/docs/claude-code) sessions. Each terminal running Claude Code appears as a worker sitting at a desk in a tiny office. You're the boss — watch them work, approve risky operations, and click workers to see what they're doing.
|
|
4
|
+
|
|
5
|
+
 
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx pixel-office
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That's it. First run automatically registers Claude Code hooks, starts the server, and opens your browser. Start a Claude Code session in any terminal — a worker appears at a desk.
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Claude Code Terminal → hooks (curl POST) → Pixel Office server
|
|
19
|
+
→ classify tool risk → auto-approve safe ops, block risky ones
|
|
20
|
+
→ WebSocket → browser renders workers on canvas
|
|
21
|
+
→ you approve/deny → Claude Code continues
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each Claude Code session registers as a pixel art worker via [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks). Workers animate at their desks — reading, typing, or waiting for your approval. Risky operations (git push, rm, sudo) require you to approve or deny from the UI before Claude can continue.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Real-time visualization** — workers appear, animate, and leave as Claude Code sessions start and end
|
|
29
|
+
- **Approval flow** — risky Bash commands (rm, sudo, git push) block until you approve/deny from the browser
|
|
30
|
+
- **Tool classification** — AST-based Bash parsing via `shfmt` to accurately classify compound commands
|
|
31
|
+
- **Multi-session** — run multiple Claude Code terminals, each gets their own desk
|
|
32
|
+
- **Break room** — idle workers walk to the break room and return when new work arrives
|
|
33
|
+
- **Worker panel** — click any worker to see their current tool, session info, and recent activity
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
- **Node.js** 20+
|
|
38
|
+
- **Claude Code** CLI installed and working
|
|
39
|
+
- **shfmt** (optional, for AST-based Bash command classification — falls back to whitespace splitting)
|
|
40
|
+
```bash
|
|
41
|
+
brew install shfmt # macOS
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Setup from source
|
|
45
|
+
|
|
46
|
+
If you want to develop or customize Pixel Office:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/nedas-dev/pixel-office.git
|
|
50
|
+
cd pixel-office
|
|
51
|
+
npm install
|
|
52
|
+
npm run dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
On first `npm start`, hooks are registered automatically. Or manage them manually:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run setup # Register hooks in ~/.claude/settings.json
|
|
59
|
+
npm run uninstall # Remove all Pixel Office hooks
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run dev # Dev server (custom Node server with WebSocket)
|
|
66
|
+
npm run build # Production build
|
|
67
|
+
npm start # Start server (auto-registers hooks on first run)
|
|
68
|
+
npm run setup # Manually register hooks
|
|
69
|
+
npm run uninstall # Remove all Pixel Office hooks
|
|
70
|
+
npm run lint # ESLint
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Architecture
|
|
74
|
+
|
|
75
|
+
**Stack:** Next.js 16 · React 19 · TypeScript · WebSocket · Canvas · Tailwind CSS 4
|
|
76
|
+
|
|
77
|
+
The app uses a custom Node server (`server.ts`) because Next.js doesn't natively support WebSocket. All state lives on `globalThis` to survive HMR re-evaluation.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
src/
|
|
81
|
+
├── app/api/hooks/ # 6 hook endpoints matching Claude Code events
|
|
82
|
+
├── lib/ # Server: session store, WebSocket, tool classifier, approval queue
|
|
83
|
+
├── game/ # Canvas engine: office layout, renderer, sprites, pathfinding
|
|
84
|
+
├── hooks/ # React hooks (game loop, WebSocket, approval state)
|
|
85
|
+
└── components/ # React overlays (canvas, approval toasts, worker panel)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Tool classification
|
|
89
|
+
|
|
90
|
+
Tools are classified into tiers:
|
|
91
|
+
- **Auto-approve:** Read, Grep, Glob, Edit, Write, safe Bash commands, safe git subcommands
|
|
92
|
+
- **Ask boss:** git push, rm, sudo, npm install, unknown Bash commands
|
|
93
|
+
- **Bash parsing:** Uses `shfmt --tojson` for AST-based analysis of compound commands; falls back to whitespace splitting
|
|
94
|
+
|
|
95
|
+
### Canvas rendering
|
|
96
|
+
|
|
97
|
+
- Logical resolution: 1280×960 (20×15 tiles, 16px, 4× scale)
|
|
98
|
+
- Painter's algorithm: floors → walls → furniture (Y-sorted) → workers (Y-sorted) → speech bubbles
|
|
99
|
+
- Pixelated rendering with `image-rendering: pixelated`
|
|
100
|
+
|
|
101
|
+
### Worker states
|
|
102
|
+
|
|
103
|
+
`idle` → `typing` → `reading` → `waiting` (needs approval) → `walking` → `leaving`
|
|
104
|
+
|
|
105
|
+
Workers animate at desks based on current tool type. Idle workers walk to the break room and return when new work arrives. Leaving workers walk to the door, then get removed.
|
|
106
|
+
|
|
107
|
+
## Hooks reference
|
|
108
|
+
|
|
109
|
+
Pixel Office uses 7 Claude Code hooks. All are fire-and-forget except `PreToolUse`, which blocks (up to 30s) for approval on risky operations.
|
|
110
|
+
|
|
111
|
+
| Hook | Endpoint | Blocking | Purpose |
|
|
112
|
+
|------|----------|----------|---------|
|
|
113
|
+
| `SessionStart` | `/api/hooks/session-start` | No | Register worker at desk |
|
|
114
|
+
| `PreToolUse` | `/api/hooks/pre-tool-use` | **Yes (30s)** | Classify tool, block if risky |
|
|
115
|
+
| `PostToolUse` | `/api/hooks/post-tool-use` | No | Update worker state |
|
|
116
|
+
| `Notification` | `/api/hooks/notification` | No | Show notification bubble |
|
|
117
|
+
| `UserPromptSubmit` | `/api/hooks/user-prompt` | No | Update worker's current focus |
|
|
118
|
+
| `Stop` | `/api/hooks/stop` | No | Set worker idle |
|
|
119
|
+
| `SessionEnd` | `/api/hooks/session-end` | No | Remove worker from office |
|
|
120
|
+
|
|
121
|
+
## Environment variable
|
|
122
|
+
|
|
123
|
+
Set `PIXEL_OFFICE_HAIKU=1` in any Claude Code session to skip Pixel Office hooks for that session (useful for lightweight/scripted runs).
|
|
124
|
+
|
|
125
|
+
## Publishing
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm login
|
|
129
|
+
npm publish
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
After publishing, anyone can run `npx pixel-office` to start.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
[MIT](LICENSE)
|
package/bin.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pixel Office CLI entry point
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")")" && pwd)"
|
|
4
|
+
|
|
5
|
+
# When installed via npm, the actual package is one level up from .bin
|
|
6
|
+
if [ -f "$SCRIPT_DIR/../bin.ts" ]; then
|
|
7
|
+
PKG_DIR="$SCRIPT_DIR/.."
|
|
8
|
+
elif [ -f "$SCRIPT_DIR/bin.ts" ]; then
|
|
9
|
+
PKG_DIR="$SCRIPT_DIR"
|
|
10
|
+
else
|
|
11
|
+
# npm link / global install: resolve from the symlink
|
|
12
|
+
REAL_PATH="$(readlink -f "$0" 2>/dev/null || python3 -c "import os; print(os.path.realpath('$0'))" 2>/dev/null)"
|
|
13
|
+
PKG_DIR="$(dirname "$REAL_PATH")/.."
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
exec npx tsx "$PKG_DIR/bin.ts" "$@"
|
package/bin.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
|
|
7
|
+
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
8
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
9
|
+
const BASE_URL = 'http://localhost:3000/api/hooks';
|
|
10
|
+
const MARKER = 'pixel-office';
|
|
11
|
+
|
|
12
|
+
// ---------- Hook registration (inlined from scripts/setup.ts) ----------
|
|
13
|
+
|
|
14
|
+
const TTY_PREFIX = [
|
|
15
|
+
'[ "$PIXEL_OFFICE_HAIKU" = "1" ] && exit 0',
|
|
16
|
+
'RAW_TTY=$(ps -o tty= -p $PPID 2>/dev/null | tr -d \' \')',
|
|
17
|
+
'[ -n "$RAW_TTY" ] && [ "$RAW_TTY" != "??" ] && TTY="/dev/$RAW_TTY" || TTY=""',
|
|
18
|
+
'INPUT=$(cat)',
|
|
19
|
+
].join('; ');
|
|
20
|
+
|
|
21
|
+
const SIMPLE_PREFIX = '[ "$PIXEL_OFFICE_HAIKU" = "1" ] && exit 0; INPUT=$(cat)';
|
|
22
|
+
|
|
23
|
+
function curlCmd(endpoint: string, maxTime: number, enrichTty: boolean): string {
|
|
24
|
+
if (enrichTty) {
|
|
25
|
+
const body = `$(echo "$INPUT" | jq -c --arg tty "$TTY" '. + {tty: $tty}' 2>/dev/null || echo "$INPUT")`;
|
|
26
|
+
return `${TTY_PREFIX}; curl -sf -X POST ${BASE_URL}/${endpoint} -H 'Content-Type: application/json' -d "${body}" --max-time ${maxTime} 2>/dev/null || true`;
|
|
27
|
+
}
|
|
28
|
+
return `${SIMPLE_PREFIX}; curl -sf -X POST ${BASE_URL}/${endpoint} -H 'Content-Type: application/json' -d "$INPUT" --max-time ${maxTime} 2>/dev/null || true`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface HookEntry {
|
|
32
|
+
matcher?: string;
|
|
33
|
+
hooks: { type: string; command: string; timeout: number }[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PIXEL_OFFICE_HOOKS: Record<string, HookEntry> = {
|
|
37
|
+
SessionStart: { hooks: [{ type: 'command', command: curlCmd('session-start', 5, true), timeout: 5 }] },
|
|
38
|
+
PreToolUse: { hooks: [{ type: 'command', command: curlCmd('pre-tool-use', 30, true), timeout: 30 }] },
|
|
39
|
+
PostToolUse: { hooks: [{ type: 'command', command: curlCmd('post-tool-use', 5, false), timeout: 5 }] },
|
|
40
|
+
Notification: { hooks: [{ type: 'command', command: curlCmd('notification', 5, false), timeout: 5 }] },
|
|
41
|
+
UserPromptSubmit: { hooks: [{ type: 'command', command: curlCmd('user-prompt', 5, false), timeout: 5 }] },
|
|
42
|
+
Stop: { hooks: [{ type: 'command', command: curlCmd('stop', 5, false), timeout: 5 }] },
|
|
43
|
+
SessionEnd: { hooks: [{ type: 'command', command: curlCmd('session-end', 5, false), timeout: 5 }] },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function hooksRegistered(): boolean {
|
|
47
|
+
if (!existsSync(SETTINGS_PATH)) return false;
|
|
48
|
+
try {
|
|
49
|
+
const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
50
|
+
const hooks = settings.hooks ?? {};
|
|
51
|
+
return Object.values(hooks).some((entries) =>
|
|
52
|
+
(entries as HookEntry[]).some((entry) =>
|
|
53
|
+
entry.hooks?.some((h) => typeof h.command === 'string' && h.command.includes(MARKER))
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function registerHooks(): void {
|
|
62
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
63
|
+
const settings = existsSync(SETTINGS_PATH)
|
|
64
|
+
? JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'))
|
|
65
|
+
: {};
|
|
66
|
+
const hooks = settings.hooks ?? {};
|
|
67
|
+
|
|
68
|
+
for (const [event, entry] of Object.entries(PIXEL_OFFICE_HOOKS)) {
|
|
69
|
+
const existing = hooks[event] ?? [];
|
|
70
|
+
const alreadyHas = existing.some((e: HookEntry) =>
|
|
71
|
+
e.hooks?.some((h: { command: string }) => h.command.includes(MARKER))
|
|
72
|
+
);
|
|
73
|
+
if (!alreadyHas) {
|
|
74
|
+
hooks[event] = [...existing, entry];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
settings.hooks = hooks;
|
|
79
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------- Main ----------
|
|
83
|
+
|
|
84
|
+
function openBrowser(url: string): void {
|
|
85
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
86
|
+
exec(`${cmd} ${url}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main(): Promise<void> {
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(' ┌──────────────────────────┐');
|
|
92
|
+
console.log(' │ Pixel Office │');
|
|
93
|
+
console.log(' └──────────────────────────┘');
|
|
94
|
+
console.log('');
|
|
95
|
+
|
|
96
|
+
// Auto-register hooks on first run
|
|
97
|
+
if (!hooksRegistered()) {
|
|
98
|
+
console.log(' First run — registering Claude Code hooks...');
|
|
99
|
+
registerHooks();
|
|
100
|
+
console.log(' ✓ Hooks registered in ~/.claude/settings.json');
|
|
101
|
+
console.log('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Start the server
|
|
105
|
+
const { createServer } = await import('http');
|
|
106
|
+
const next = (await import('next')).default;
|
|
107
|
+
const { initWSS, broadcast } = await import('./src/lib/ws-server');
|
|
108
|
+
const { cleanupStaleSessions, getAllSessions } = await import('./src/lib/store');
|
|
109
|
+
const { getAllPtyEntries, killPty } = await import('./src/lib/pty-manager');
|
|
110
|
+
|
|
111
|
+
const dev = process.env.NODE_ENV !== 'production';
|
|
112
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
113
|
+
|
|
114
|
+
const app = next({ dev, dir: __dirname });
|
|
115
|
+
const handle = app.getRequestHandler();
|
|
116
|
+
|
|
117
|
+
await app.prepare();
|
|
118
|
+
|
|
119
|
+
const server = createServer((req, res) => {
|
|
120
|
+
handle(req, res);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
initWSS(server);
|
|
124
|
+
|
|
125
|
+
// Stale session cleanup (same as server.ts)
|
|
126
|
+
if (!globalThis.__staleSessionCleanup) {
|
|
127
|
+
globalThis.__staleSessionCleanup = setInterval(() => {
|
|
128
|
+
const isAlivePty = (ptyId: string) => {
|
|
129
|
+
const e = getAllPtyEntries().find((p) => p.ptyId === ptyId);
|
|
130
|
+
return !!e && !e.exited;
|
|
131
|
+
};
|
|
132
|
+
const removed = cleanupStaleSessions(30 * 60_000, isAlivePty);
|
|
133
|
+
for (const sessionId of removed) {
|
|
134
|
+
console.log(`[cleanup] Removed stale session: ${sessionId}`);
|
|
135
|
+
broadcast({ type: 'session-remove', sessionId });
|
|
136
|
+
}
|
|
137
|
+
const activePtyIds = new Set(
|
|
138
|
+
getAllSessions()
|
|
139
|
+
.filter((s) => s.ptyId)
|
|
140
|
+
.map((s) => s.ptyId!)
|
|
141
|
+
);
|
|
142
|
+
for (const entry of getAllPtyEntries()) {
|
|
143
|
+
if (!activePtyIds.has(entry.ptyId) && entry.exited) {
|
|
144
|
+
killPty(entry.ptyId);
|
|
145
|
+
console.log(`[cleanup] Removed orphaned PTY: ${entry.ptyId}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}, 5 * 60_000);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
server.listen(port, () => {
|
|
152
|
+
console.log(` ✓ Ready on http://localhost:${port}`);
|
|
153
|
+
console.log(' Open a Claude Code terminal — a worker will appear.');
|
|
154
|
+
console.log('');
|
|
155
|
+
openBrowser(`http://localhost:${port}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((err) => {
|
|
160
|
+
console.error('Failed to start Pixel Office:', err);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
package/next-env.d.ts
ADDED
package/next.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neolio42/pixel-office",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visualize your Claude Code sessions as pixel art workers in a tiny office",
|
|
5
|
+
"author": "Nedas Mackevičius",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nedas-dev/pixel-office.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"claude",
|
|
13
|
+
"claude-code",
|
|
14
|
+
"pixel-art",
|
|
15
|
+
"visualization",
|
|
16
|
+
"hooks",
|
|
17
|
+
"developer-tools"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"pixel-office": "./bin.sh"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "npx tsx server.ts",
|
|
24
|
+
"build": "next build",
|
|
25
|
+
"start": "npx tsx bin.ts",
|
|
26
|
+
"lint": "eslint",
|
|
27
|
+
"setup": "npx tsx scripts/setup.ts",
|
|
28
|
+
"uninstall": "npx tsx scripts/setup.ts uninstall"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
32
|
+
"@xterm/xterm": "^6.0.0",
|
|
33
|
+
"next": "16.1.6",
|
|
34
|
+
"node-pty": "^1.2.0-beta.12",
|
|
35
|
+
"react": "19.2.3",
|
|
36
|
+
"react-dom": "19.2.3",
|
|
37
|
+
"tsx": "^4.21.0",
|
|
38
|
+
"ws": "^8.19.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@tailwindcss/postcss": "^4",
|
|
42
|
+
"@types/node": "^20",
|
|
43
|
+
"@types/react": "^19",
|
|
44
|
+
"@types/react-dom": "^19",
|
|
45
|
+
"@types/ws": "^8.18.1",
|
|
46
|
+
"eslint": "^9",
|
|
47
|
+
"eslint-config-next": "16.1.6",
|
|
48
|
+
"tailwindcss": "^4",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"cols": 21,
|
|
4
|
+
"rows": 22,
|
|
5
|
+
"layoutRevision": 1,
|
|
6
|
+
"tiles": [
|
|
7
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
8
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
9
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
10
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
11
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
12
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
13
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
14
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
15
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
16
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
17
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
|
|
18
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
19
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
20
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
21
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
22
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
23
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
24
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
25
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
|
|
26
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 255,
|
|
27
|
+
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 255,
|
|
28
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
|
|
29
|
+
],
|
|
30
|
+
"tileColors": [
|
|
31
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
32
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
33
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
34
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
35
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
36
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
37
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
38
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
39
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
40
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
|
41
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
42
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
43
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
44
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
45
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
46
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
47
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
48
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
49
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
50
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
51
|
+
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":214,"s":30,"b":-100,"c":-55}, null,
|
|
52
|
+
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null
|
|
53
|
+
],
|
|
54
|
+
"furniture": [
|
|
55
|
+
{"uid": "f-1773353910654-5cdg", "type": "TABLE_FRONT", "col": 4, "row": 16},
|
|
56
|
+
{"uid": "f-1773354646615-jhxl", "type": "COFFEE_TABLE", "col": 14, "row": 14},
|
|
57
|
+
{"uid": "f-1773354664329-hxsh", "type": "SOFA_SIDE", "col": 13, "row": 14},
|
|
58
|
+
{"uid": "f-1773354665989-zgrw", "type": "SOFA_BACK", "col": 14, "row": 16},
|
|
59
|
+
{"uid": "f-1773354668333-lo7w", "type": "SOFA_FRONT", "col": 14, "row": 13},
|
|
60
|
+
{"uid": "f-1773354670818-r1q2", "type": "SOFA_SIDE:left", "col": 16, "row": 14},
|
|
61
|
+
{"uid": "f-1773354686967-yiua", "type": "HANGING_PLANT", "col": 9, "row": 9},
|
|
62
|
+
{"uid": "f-1773354687677-hn2k", "type": "HANGING_PLANT", "col": 1, "row": 9},
|
|
63
|
+
{"uid": "f-1773354693077-f7aj", "type": "DOUBLE_BOOKSHELF", "col": 7, "row": 9},
|
|
64
|
+
{"uid": "f-1773354700513-f1zs", "type": "DOUBLE_BOOKSHELF", "col": 2, "row": 9},
|
|
65
|
+
{"uid": "f-1773354799984-j5ri", "type": "SMALL_PAINTING", "col": 12, "row": 9},
|
|
66
|
+
{"uid": "f-1773354827151-yox2", "type": "CLOCK", "col": 5, "row": 9},
|
|
67
|
+
{"uid": "f-1773354842615-f5md", "type": "PLANT", "col": 18, "row": 10},
|
|
68
|
+
{"uid": "f-1773354861273-67uo", "type": "COFFEE", "col": 14, "row": 15},
|
|
69
|
+
{"uid": "f-1773354877474-kt9s", "type": "WOODEN_CHAIR_SIDE", "col": 3, "row": 18},
|
|
70
|
+
{"uid": "f-1773354879805-px9b", "type": "WOODEN_CHAIR_SIDE", "col": 3, "row": 16},
|
|
71
|
+
{"uid": "f-1773354880309-yphd", "type": "WOODEN_CHAIR_SIDE:left", "col": 7, "row": 16},
|
|
72
|
+
{"uid": "f-1773354881902-9m50", "type": "WOODEN_CHAIR_SIDE:left", "col": 7, "row": 18},
|
|
73
|
+
{"uid": "f-1773354931010-8vvr", "type": "DESK_FRONT", "col": 2, "row": 12},
|
|
74
|
+
{"uid": "f-1773354932396-5uus", "type": "DESK_FRONT", "col": 6, "row": 12},
|
|
75
|
+
{"uid": "f-1773356768339-eo6u", "type": "CUSHIONED_BENCH", "col": 3, "row": 14},
|
|
76
|
+
{"uid": "f-1773356769007-a8jm", "type": "CUSHIONED_BENCH", "col": 7, "row": 14},
|
|
77
|
+
{"uid": "f-1773356781294-b69z", "type": "PC_FRONT_OFF", "col": 7, "row": 12},
|
|
78
|
+
{"uid": "f-1773356782055-vp70", "type": "PC_FRONT_OFF", "col": 3, "row": 12},
|
|
79
|
+
{"uid": "f-1773356784581-5jw9", "type": "PC_SIDE", "col": 4, "row": 16},
|
|
80
|
+
{"uid": "f-1773356785458-pyjn", "type": "PC_SIDE", "col": 4, "row": 18},
|
|
81
|
+
{"uid": "f-1773356787060-higb", "type": "PC_SIDE:left", "col": 6, "row": 16},
|
|
82
|
+
{"uid": "f-1773356787744-ykrz", "type": "PC_SIDE:left", "col": 6, "row": 18},
|
|
83
|
+
{"uid": "f-1773356878781-rncl", "type": "PLANT_2", "col": 11, "row": 10},
|
|
84
|
+
{"uid": "f-1773356974812-apra", "type": "LARGE_PAINTING", "col": 14, "row": 9},
|
|
85
|
+
{"uid": "f-1773357087399-3kfy", "type": "BIN", "col": 2, "row": 20},
|
|
86
|
+
{"uid": "f-1773357989802-thws", "type": "SMALL_TABLE_FRONT", "col": 17, "row": 19},
|
|
87
|
+
{"uid": "f-1773358001163-aqv4", "type": "SMALL_TABLE_SIDE", "col": 1, "row": 18},
|
|
88
|
+
{"uid": "f-1773358458100-4wm2", "type": "COFFEE", "col": 1, "row": 19},
|
|
89
|
+
{"uid": "f-1773358479734-biia", "type": "PLANT_2", "col": 1, "row": 17},
|
|
90
|
+
{"uid": "f-1773358485454-id8j", "type": "SMALL_PAINTING_2", "col": 17, "row": 9}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|