opencube 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/README.md +106 -0
- package/assets/opencode-icon-3d-three-preview.html +168 -0
- package/assets/opencode-icon.png +0 -0
- package/assets/opencode-pet-3d-spin-concept.svg +99 -0
- package/assets/opencode-pet-3d-spin-preview.html +324 -0
- package/assets/pixel-opencode-pet.svg +74 -0
- package/package.json +30 -0
- package/src/main.js +1282 -0
- package/src/pixel-pet-reference.cjs +134 -0
- package/src/plugin-server.cjs +169 -0
- package/src/plugin-shared.cjs +230 -0
- package/src/plugin-tui.cjs +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/opencode-icon.png" width="96" height="96" alt="OpenCub icon" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# OpenCub
|
|
6
|
+
|
|
7
|
+
OpenCub is a tiny desktop pet for [opencode](https://opencode.ai/).
|
|
8
|
+
|
|
9
|
+
It watches opencode session activity and renders a small Three.js cube on your desktop:
|
|
10
|
+
|
|
11
|
+
- busy sessions light up cube faces
|
|
12
|
+
- idle sessions release their face glow after the cube slows down
|
|
13
|
+
- `/pet_say_hello` flashes one free face
|
|
14
|
+
- `/pet_fancy_say_hello` plays a randomized light show on free faces
|
|
15
|
+
|
|
16
|
+
OpenCub is packaged as an opencode plugin plus an Electron desktop process.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
> OpenCub is not published yet. These are the intended install commands once published.
|
|
21
|
+
|
|
22
|
+
Install globally through opencode:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
opencode plugin opencub --global
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then restart opencode and run:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
/pet
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
You can also add it manually to `~/.config/opencode/opencode.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"plugin": ["opencub"]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
| Command | Description |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `/pet` | Show or start OpenCub. |
|
|
47
|
+
| `/pet_stop` | Quit OpenCub. |
|
|
48
|
+
| `/pet_say_hello` | Flash one currently free face three times with a random color. |
|
|
49
|
+
| `/pet_fancy_say_hello` | Run a denser randomized light show across currently free faces. |
|
|
50
|
+
|
|
51
|
+
These commands are handled by the plugin and do not get sent to the model.
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
OpenCub has two parts in one npm package:
|
|
56
|
+
|
|
57
|
+
1. `src/plugin-server.cjs` — the opencode plugin entrypoint.
|
|
58
|
+
2. `src/main.js` — the Electron desktop pet.
|
|
59
|
+
|
|
60
|
+
The plugin registers slash commands, listens for opencode `session.status` events, and sends events to the desktop pet over a local HTTP API:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
opencode plugin -> http://127.0.0.1:47832 -> Electron OpenCub
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The Electron process owns the window, Three.js renderer, cube rotation, face glow state, and inbox/debug endpoints.
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- opencode
|
|
71
|
+
- macOS, Windows, or Linux capable of running Electron
|
|
72
|
+
- network access to install npm dependencies during first plugin install
|
|
73
|
+
|
|
74
|
+
Users do not need to run `npm install` manually when installing via `opencode plugin`. opencode installs npm plugins and dependencies automatically.
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- The first install may take a while because Electron is downloaded as a runtime dependency.
|
|
79
|
+
- If commands do not appear after installation, restart opencode.
|
|
80
|
+
- OpenCub uses a local-only HTTP server on `127.0.0.1:47832`.
|
|
81
|
+
- If that port is already in use, set `OPENCODE_PET_PORT` before starting opencode.
|
|
82
|
+
|
|
83
|
+
## Local development
|
|
84
|
+
|
|
85
|
+
From this repository:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npm install
|
|
89
|
+
npm run start
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For local opencode plugin testing, point your opencode config at the package directory:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"plugin": ["/path/to/opencub"]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
After changing plugin files or opencode config, restart opencode.
|
|
101
|
+
|
|
102
|
+
To inspect the package contents before publishing:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
npm pack --dry-run
|
|
106
|
+
```
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>opencode icon 3D three.js preview</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
background: #ecece7;
|
|
14
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
15
|
+
}
|
|
16
|
+
#app { width: 100%; height: 100%; }
|
|
17
|
+
.hud {
|
|
18
|
+
position: fixed;
|
|
19
|
+
left: 20px;
|
|
20
|
+
top: 18px;
|
|
21
|
+
padding: 12px 14px;
|
|
22
|
+
border: 2px solid rgba(0,0,0,.16);
|
|
23
|
+
border-radius: 14px;
|
|
24
|
+
background: rgba(255,255,255,.72);
|
|
25
|
+
backdrop-filter: blur(8px);
|
|
26
|
+
color: #111;
|
|
27
|
+
font-size: 13px;
|
|
28
|
+
line-height: 1.55;
|
|
29
|
+
box-shadow: 0 12px 36px rgba(0,0,0,.08);
|
|
30
|
+
user-select: none;
|
|
31
|
+
}
|
|
32
|
+
.hud b { font-size: 15px; }
|
|
33
|
+
.hud .muted { color: rgba(0,0,0,.58); }
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<div id="app"></div>
|
|
38
|
+
<div class="hud">
|
|
39
|
+
<b>3D opencode icon</b><br />
|
|
40
|
+
<span class="muted">只用六张原生 opencode icon 贴图围成立方体</span><br />
|
|
41
|
+
拖动旋转 · 滚轮缩放 · idle 可作为正面静止态
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<script type="module">
|
|
45
|
+
import * as THREE from "https://esm.sh/three@0.164.1"
|
|
46
|
+
import { OrbitControls } from "https://esm.sh/three@0.164.1/examples/jsm/controls/OrbitControls.js"
|
|
47
|
+
|
|
48
|
+
const container = document.getElementById("app")
|
|
49
|
+
const scene = new THREE.Scene()
|
|
50
|
+
scene.background = new THREE.Color(0xecece7)
|
|
51
|
+
|
|
52
|
+
const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerHeight, 0.1, 100)
|
|
53
|
+
camera.position.set(4.3, 3.2, 6.4)
|
|
54
|
+
|
|
55
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
|
56
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
|
57
|
+
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
58
|
+
renderer.shadowMap.enabled = true
|
|
59
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
|
60
|
+
container.appendChild(renderer.domElement)
|
|
61
|
+
|
|
62
|
+
const controls = new OrbitControls(camera, renderer.domElement)
|
|
63
|
+
controls.enableDamping = true
|
|
64
|
+
controls.dampingFactor = 0.08
|
|
65
|
+
controls.target.set(0, 0.08, 0)
|
|
66
|
+
controls.minDistance = 3.4
|
|
67
|
+
controls.maxDistance = 12
|
|
68
|
+
|
|
69
|
+
const group = new THREE.Group()
|
|
70
|
+
scene.add(group)
|
|
71
|
+
|
|
72
|
+
const iconTexture = new THREE.TextureLoader().load("./opencode-icon.png", (texture) => {
|
|
73
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
74
|
+
texture.anisotropy = renderer.capabilities.getMaxAnisotropy()
|
|
75
|
+
texture.needsUpdate = true
|
|
76
|
+
})
|
|
77
|
+
const iconFaceMaterial = new THREE.MeshStandardMaterial({
|
|
78
|
+
map: iconTexture,
|
|
79
|
+
roughness: 0.48,
|
|
80
|
+
metalness: 0.02,
|
|
81
|
+
transparent: true,
|
|
82
|
+
alphaTest: 0.02,
|
|
83
|
+
side: THREE.DoubleSide,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// 只用 6 张 PlaneGeometry 手动拼 cube:没有中间黑盒子。
|
|
87
|
+
// 原生 PNG 自带圆角/透明边缘,所以圆角处会自然镂空,
|
|
88
|
+
// 看起来更像几张 icon 面片围成的立方体。
|
|
89
|
+
// PlaneGeometry 默认只有正面可见;这里用 DoubleSide,
|
|
90
|
+
// 否则透过圆角看进去时,看不到其它 icon 面片的背面。
|
|
91
|
+
const size = 3.2
|
|
92
|
+
const half = size / 2
|
|
93
|
+
|
|
94
|
+
function addIconFace(name, position, rotation) {
|
|
95
|
+
const face = new THREE.Mesh(new THREE.PlaneGeometry(size, size), iconFaceMaterial.clone())
|
|
96
|
+
face.name = name
|
|
97
|
+
face.position.set(...position)
|
|
98
|
+
face.rotation.set(...rotation)
|
|
99
|
+
face.castShadow = true
|
|
100
|
+
face.receiveShadow = true
|
|
101
|
+
group.add(face)
|
|
102
|
+
return face
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
addIconFace("front", [0, 0, half + 0.001], [0, 0, 0])
|
|
106
|
+
addIconFace("back", [0, 0, -half - 0.001], [0, Math.PI, 0])
|
|
107
|
+
addIconFace("right", [half + 0.001, 0, 0], [0, Math.PI / 2, 0])
|
|
108
|
+
addIconFace("left", [-half - 0.001, 0, 0], [0, -Math.PI / 2, 0])
|
|
109
|
+
addIconFace("top", [0, half + 0.001, 0], [-Math.PI / 2, 0, 0])
|
|
110
|
+
addIconFace("bottom", [0, -half - 0.001, 0], [Math.PI / 2, 0, 0])
|
|
111
|
+
|
|
112
|
+
group.rotation.x = -0.08
|
|
113
|
+
group.rotation.y = -0.38
|
|
114
|
+
|
|
115
|
+
const hemi = new THREE.HemisphereLight(0xffffff, 0xb8b8b2, 1.45)
|
|
116
|
+
scene.add(hemi)
|
|
117
|
+
|
|
118
|
+
const key = new THREE.DirectionalLight(0xffffff, 3.2)
|
|
119
|
+
key.position.set(4, 6, 5)
|
|
120
|
+
key.castShadow = true
|
|
121
|
+
key.shadow.mapSize.set(2048, 2048)
|
|
122
|
+
key.shadow.camera.near = 0.5
|
|
123
|
+
key.shadow.camera.far = 20
|
|
124
|
+
key.shadow.camera.left = -6
|
|
125
|
+
key.shadow.camera.right = 6
|
|
126
|
+
key.shadow.camera.top = 6
|
|
127
|
+
key.shadow.camera.bottom = -6
|
|
128
|
+
scene.add(key)
|
|
129
|
+
|
|
130
|
+
const fill = new THREE.DirectionalLight(0xfff1df, 1.0)
|
|
131
|
+
fill.position.set(-4, 2.5, 3)
|
|
132
|
+
scene.add(fill)
|
|
133
|
+
|
|
134
|
+
const rim = new THREE.DirectionalLight(0xffffff, 1.2)
|
|
135
|
+
rim.position.set(-3, 5, -4)
|
|
136
|
+
scene.add(rim)
|
|
137
|
+
|
|
138
|
+
const floor = new THREE.Mesh(
|
|
139
|
+
new THREE.CircleGeometry(4.6, 96),
|
|
140
|
+
new THREE.ShadowMaterial({ opacity: 0.18 }),
|
|
141
|
+
)
|
|
142
|
+
floor.rotation.x = -Math.PI / 2
|
|
143
|
+
floor.position.y = -2.25
|
|
144
|
+
floor.receiveShadow = true
|
|
145
|
+
scene.add(floor)
|
|
146
|
+
|
|
147
|
+
let autoSpin = true
|
|
148
|
+
renderer.domElement.addEventListener("pointerdown", () => {
|
|
149
|
+
autoSpin = false
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
function resize() {
|
|
153
|
+
camera.aspect = window.innerWidth / window.innerHeight
|
|
154
|
+
camera.updateProjectionMatrix()
|
|
155
|
+
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
156
|
+
}
|
|
157
|
+
window.addEventListener("resize", resize)
|
|
158
|
+
|
|
159
|
+
function animate() {
|
|
160
|
+
requestAnimationFrame(animate)
|
|
161
|
+
if (autoSpin) group.rotation.y += 0.008
|
|
162
|
+
controls.update()
|
|
163
|
+
renderer.render(scene, camera)
|
|
164
|
+
}
|
|
165
|
+
animate()
|
|
166
|
+
</script>
|
|
167
|
+
</body>
|
|
168
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="520" viewBox="0 0 720 520" role="img" aria-label="opencode pet 3D busy spin concept">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#f7f7f5"/>
|
|
5
|
+
<stop offset="1" stop-color="#e8e8e3"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="side" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0" stop-color="#050505"/>
|
|
9
|
+
<stop offset="1" stop-color="#1d1d1d"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<filter id="softShadow" x="-50%" y="-50%" width="200%" height="200%">
|
|
12
|
+
<feGaussianBlur in="SourceAlpha" stdDeviation="7"/>
|
|
13
|
+
<feOffset dx="0" dy="8" result="offset"/>
|
|
14
|
+
<feComponentTransfer>
|
|
15
|
+
<feFuncA type="linear" slope="0.25"/>
|
|
16
|
+
</feComponentTransfer>
|
|
17
|
+
<feMerge>
|
|
18
|
+
<feMergeNode/>
|
|
19
|
+
<feMergeNode in="SourceGraphic"/>
|
|
20
|
+
</feMerge>
|
|
21
|
+
</filter>
|
|
22
|
+
<marker id="arrow" markerWidth="10" markerHeight="10" refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
|
|
23
|
+
<path d="M0,0 L0,6 L8,3 z" fill="#ff5d73"/>
|
|
24
|
+
</marker>
|
|
25
|
+
</defs>
|
|
26
|
+
|
|
27
|
+
<rect width="720" height="520" fill="url(#bg)"/>
|
|
28
|
+
|
|
29
|
+
<text x="40" y="52" fill="#111" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="22" font-weight="700">busy 状态:opencode 小人 3D 转身</text>
|
|
30
|
+
<text x="40" y="82" fill="#555" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="14">任何 session busy → 绕竖直 Y 轴持续旋转;全部 idle → 停回正面</text>
|
|
31
|
+
|
|
32
|
+
<!-- 3D floor / orbit ring -->
|
|
33
|
+
<ellipse cx="360" cy="382" rx="172" ry="48" fill="#d7d7d0" opacity="0.8"/>
|
|
34
|
+
<ellipse cx="360" cy="382" rx="132" ry="34" fill="none" stroke="#ff5d73" stroke-width="5" stroke-dasharray="18 10" opacity="0.85"/>
|
|
35
|
+
<path d="M 248 376 C 286 333, 423 321, 492 363" fill="none" stroke="#ff5d73" stroke-width="6" marker-end="url(#arrow)" opacity="0.9"/>
|
|
36
|
+
|
|
37
|
+
<!-- vertical Y axis -->
|
|
38
|
+
<line x1="360" y1="110" x2="360" y2="405" stroke="#111" stroke-width="3" stroke-dasharray="8 8" opacity="0.35"/>
|
|
39
|
+
<text x="374" y="124" fill="#111" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="13">Y 轴</text>
|
|
40
|
+
|
|
41
|
+
<!-- ghost frames showing rotation -->
|
|
42
|
+
<g opacity="0.18" transform="translate(218 212) skewY(-8) scale(.72 1)">
|
|
43
|
+
<rect x="0" y="0" width="84" height="120" fill="#050505"/>
|
|
44
|
+
<rect x="14" y="20" width="56" height="72" fill="#f6f4ec"/>
|
|
45
|
+
<rect x="30" y="36" width="24" height="44" fill="#050505"/>
|
|
46
|
+
</g>
|
|
47
|
+
<g opacity="0.22" transform="translate(450 212) skewY(8) scale(.72 1)">
|
|
48
|
+
<rect x="0" y="0" width="84" height="120" fill="#050505"/>
|
|
49
|
+
<rect x="14" y="20" width="56" height="72" fill="#f6f4ec"/>
|
|
50
|
+
<rect x="30" y="36" width="24" height="44" fill="#050505"/>
|
|
51
|
+
</g>
|
|
52
|
+
|
|
53
|
+
<!-- main 3D pet body -->
|
|
54
|
+
<g filter="url(#softShadow)">
|
|
55
|
+
<!-- back/side depth -->
|
|
56
|
+
<polygon points="306,159 432,159 458,184 458,340 432,360 306,360 282,338 282,184" fill="#030303"/>
|
|
57
|
+
<polygon points="432,159 458,184 458,340 432,360 432,159" fill="url(#side)"/>
|
|
58
|
+
<polygon points="306,159 432,159 458,184 330,184" fill="#242424"/>
|
|
59
|
+
|
|
60
|
+
<!-- ears / side blocks -->
|
|
61
|
+
<rect x="260" y="224" width="34" height="72" fill="#050505"/>
|
|
62
|
+
<rect x="268" y="236" width="18" height="48" fill="#242424"/>
|
|
63
|
+
<rect x="448" y="224" width="34" height="72" fill="#050505"/>
|
|
64
|
+
<rect x="448" y="236" width="18" height="48" fill="#202020"/>
|
|
65
|
+
|
|
66
|
+
<!-- front face -->
|
|
67
|
+
<rect x="306" y="176" width="126" height="164" fill="#050505"/>
|
|
68
|
+
<rect x="318" y="188" width="102" height="22" fill="#252525"/>
|
|
69
|
+
<rect x="318" y="316" width="102" height="18" fill="#171717"/>
|
|
70
|
+
|
|
71
|
+
<!-- opencode white plate: deliberately vertical rectangle -->
|
|
72
|
+
<rect x="324" y="216" width="88" height="104" fill="#2d2d2d"/>
|
|
73
|
+
<rect x="330" y="222" width="76" height="92" fill="#d8d5cc"/>
|
|
74
|
+
<rect x="338" y="230" width="60" height="76" fill="#ffffff"/>
|
|
75
|
+
<rect x="354" y="248" width="30" height="50" fill="#050505"/>
|
|
76
|
+
<rect x="354" y="248" width="30" height="13" fill="#111"/>
|
|
77
|
+
|
|
78
|
+
<!-- arms / hands -->
|
|
79
|
+
<rect x="236" y="308" width="54" height="18" fill="#050505"/>
|
|
80
|
+
<rect x="226" y="298" width="26" height="26" fill="#050505"/>
|
|
81
|
+
<rect x="232" y="303" width="16" height="11" fill="#303030"/>
|
|
82
|
+
<rect x="444" y="308" width="54" height="18" fill="#050505"/>
|
|
83
|
+
<rect x="484" y="298" width="26" height="26" fill="#050505"/>
|
|
84
|
+
<rect x="486" y="303" width="16" height="11" fill="#303030"/>
|
|
85
|
+
|
|
86
|
+
<!-- feet -->
|
|
87
|
+
<rect x="318" y="356" width="32" height="32" fill="#050505"/>
|
|
88
|
+
<rect x="390" y="356" width="32" height="32" fill="#050505"/>
|
|
89
|
+
<rect x="326" y="356" width="22" height="10" fill="#242424"/>
|
|
90
|
+
<rect x="398" y="356" width="22" height="10" fill="#242424"/>
|
|
91
|
+
</g>
|
|
92
|
+
|
|
93
|
+
<!-- notes -->
|
|
94
|
+
<g font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="13" fill="#333">
|
|
95
|
+
<text x="72" y="438">idle:正面静止</text>
|
|
96
|
+
<text x="72" y="462">busy:scaleX + skew + 明暗面,模拟 3D 绕 Y 轴旋转</text>
|
|
97
|
+
<text x="72" y="486">不再需要球,busy 反馈更简单直接</text>
|
|
98
|
+
</g>
|
|
99
|
+
</svg>
|