opencube 0.3.1 → 0.4.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 +22 -116
- package/package.json +1 -1
- package/src/main.js +126 -14
- package/src/plugin-server.cjs +53 -2
package/README.md
CHANGED
|
@@ -4,152 +4,64 @@
|
|
|
4
4
|
|
|
5
5
|
# OpenCube
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A tiny desktop cube pet for [opencode](https://opencode.ai/).
|
|
8
8
|
|
|
9
|
-
It
|
|
10
|
-
|
|
11
|
-
- busy sessions light up cube faces
|
|
12
|
-
- active tool calls emit bright face-colored sparks from the corresponding cube face
|
|
13
|
-
- right-click drag applies extra rotational friction with small braking particles
|
|
14
|
-
- idle sessions keep a subtle torque-breathing motion and release face glow after the cube slows down
|
|
15
|
-
- a macOS-style tray/menu bar control can show, hide, quit, or open the Inbox
|
|
16
|
-
- the Inbox shows raw opencode hook events plus local mouse/keyboard diagnostics
|
|
17
|
-
- `/pet_say_hello` flashes one free face
|
|
18
|
-
- `/pet_fancy_say_hello` plays a randomized light show on free faces
|
|
19
|
-
|
|
20
|
-
OpenCube is packaged as an opencode plugin plus an Electron desktop process.
|
|
9
|
+
It lights up for busy sessions, tool calls, permissions, and questions.
|
|
21
10
|
|
|
22
11
|
## Install
|
|
23
12
|
|
|
24
|
-
Install the latest published version globally through opencode:
|
|
25
|
-
|
|
26
13
|
```sh
|
|
27
14
|
opencode plugin opencube --global
|
|
28
15
|
```
|
|
29
16
|
|
|
30
|
-
|
|
17
|
+
Restart opencode, then run:
|
|
31
18
|
|
|
32
19
|
```text
|
|
33
20
|
/pet
|
|
34
21
|
```
|
|
35
22
|
|
|
36
|
-
You can also add it manually to `~/.config/opencode/opencode.json`:
|
|
37
|
-
|
|
38
|
-
```json
|
|
39
|
-
{
|
|
40
|
-
"plugin": ["opencube"]
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
23
|
## Update
|
|
45
24
|
|
|
46
|
-
If you already installed OpenCube and want to upgrade, reinstall the target version with `--force`:
|
|
47
|
-
|
|
48
25
|
```sh
|
|
49
|
-
opencode plugin opencube@0.
|
|
26
|
+
opencode plugin opencube@0.4.0 --global --force
|
|
50
27
|
```
|
|
51
28
|
|
|
52
|
-
|
|
29
|
+
Then restart opencode and run `/pet` again.
|
|
53
30
|
|
|
54
|
-
|
|
55
|
-
opencode plugin opencube@latest --global --force
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Then fully restart opencode and run `/pet` again. OpenCube and opencode plugins are loaded at startup, so the running desktop pet is not hot-replaced in place.
|
|
59
|
-
|
|
60
|
-
You can ask OpenCube to check npm for the latest published version:
|
|
31
|
+
You can also ask OpenCube for the latest published version:
|
|
61
32
|
|
|
62
33
|
```text
|
|
63
34
|
/pet_update
|
|
64
35
|
```
|
|
65
36
|
|
|
66
|
-
`/pet_update` does not hot-replace the running plugin. It reports whether a newer version exists and prints an explicit `opencode plugin opencube@<version> --global --force` command to run.
|
|
67
|
-
|
|
68
37
|
## Commands
|
|
69
38
|
|
|
70
|
-
| Command | Description |
|
|
71
|
-
| --- | --- |
|
|
72
|
-
| `/pet` | Show or start OpenCube. |
|
|
73
|
-
| `/pet_stop` | Quit OpenCube. |
|
|
74
|
-
| `/pet_say_hello` | Flash one currently free face three times with a random color. |
|
|
75
|
-
| `/pet_fancy_say_hello` | Run a denser randomized light show across currently free faces. |
|
|
76
|
-
| `/pet_update` | Check npm for a newer OpenCube release and show the upgrade command. |
|
|
77
|
-
| `/pet_upgrade` | Alias for `/pet_update`. |
|
|
78
|
-
|
|
79
|
-
These commands are handled by the plugin and do not get sent to the model.
|
|
80
|
-
|
|
81
|
-
## What you can see
|
|
82
|
-
|
|
83
|
-
OpenCube turns opencode lifecycle events into small desktop signals:
|
|
84
|
-
|
|
85
|
-
| opencode activity | OpenCube signal |
|
|
86
|
-
| --- | --- |
|
|
87
|
-
| Session becomes busy | One cube face lights up with a stable session color. |
|
|
88
|
-
| Session becomes idle | The face glow is released once the cube slows down. |
|
|
89
|
-
| A tool call starts | The session's face emits fast, bright sparks in that face's current outward direction. |
|
|
90
|
-
| A tool call finishes | Spark emission stops; existing sparks fade out naturally. |
|
|
91
|
-
| Right mouse hold on the cube | Friction increases and braking particles appear. |
|
|
92
|
-
| Tray menu → Show Inbox | Opens a two-column event/debug panel. |
|
|
93
|
-
|
|
94
|
-
Multiple busy sessions can light multiple faces. If more than six sessions are busy, OpenCube shows the latest six.
|
|
95
|
-
|
|
96
|
-
## How it works
|
|
97
|
-
|
|
98
|
-
OpenCube has two parts in one npm package:
|
|
99
|
-
|
|
100
|
-
1. `src/plugin-server.cjs` — the opencode plugin entrypoint.
|
|
101
|
-
2. `src/main.js` — the Electron desktop pet.
|
|
102
|
-
|
|
103
|
-
The plugin registers slash commands, listens for opencode `session.status` events and `tool.execute.*` hooks, and sends events to the desktop pet over a local HTTP API:
|
|
104
|
-
|
|
105
39
|
```text
|
|
106
|
-
|
|
40
|
+
/pet start or show OpenCube
|
|
41
|
+
/pet_stop quit OpenCube
|
|
42
|
+
/pet-size show size usage
|
|
43
|
+
/pet-size 0.7 set cube size, range 0.3–3
|
|
44
|
+
/pet_update check npm for updates
|
|
45
|
+
/pet_say_hello flash a free face
|
|
46
|
+
/pet_fancy_say_hello run a small light show
|
|
107
47
|
```
|
|
108
48
|
|
|
109
|
-
The Electron process owns the window, tray menu, Three.js renderer, cube rotation, face glow state, particle effects, and inbox/debug endpoints.
|
|
110
|
-
|
|
111
|
-
## Local API
|
|
112
|
-
|
|
113
|
-
OpenCube exposes a local-only HTTP API while it is running:
|
|
114
|
-
|
|
115
|
-
| Endpoint | Purpose |
|
|
116
|
-
| --- | --- |
|
|
117
|
-
| `GET /health` | Check whether OpenCube is running. |
|
|
118
|
-
| `GET /debug-render` | Inspect renderer state, busy faces, active tools, and particle counters. |
|
|
119
|
-
| `POST /event` | Receive opencode lifecycle/tool/hello events. |
|
|
120
|
-
| `POST /interaction` | Receive local renderer mouse/keyboard diagnostics. |
|
|
121
|
-
| `POST /show` | Show the cube window. |
|
|
122
|
-
| `POST /quit` | Quit OpenCube. |
|
|
123
|
-
|
|
124
|
-
The API binds to `127.0.0.1` only.
|
|
125
|
-
|
|
126
|
-
## Requirements
|
|
127
|
-
|
|
128
|
-
- opencode
|
|
129
|
-
- macOS, Windows, or Linux capable of running Electron
|
|
130
|
-
- network access to install npm dependencies during first plugin install
|
|
131
|
-
|
|
132
|
-
Users do not need to run `npm install` manually when installing via `opencode plugin`. opencode installs npm plugins and dependencies automatically.
|
|
133
|
-
|
|
134
49
|
## Notes
|
|
135
50
|
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
- If commands do not appear
|
|
139
|
-
-
|
|
140
|
-
- If that port is already in use, set `OPENCODE_PET_PORT` before starting opencode.
|
|
141
|
-
- In opencode desktop, OpenCube currently uses a command-abort sentinel to keep local slash commands out of the model flow; depending on opencode version, the desktop UI may show an error toast even though the command was handled.
|
|
51
|
+
- OpenCube runs a local Electron process.
|
|
52
|
+
- Local API: `http://127.0.0.1:47832`.
|
|
53
|
+
- If commands do not appear, restart opencode.
|
|
54
|
+
- First install can take a bit because Electron is downloaded.
|
|
142
55
|
|
|
143
|
-
##
|
|
144
|
-
|
|
145
|
-
From this repository:
|
|
56
|
+
## Development
|
|
146
57
|
|
|
147
58
|
```sh
|
|
148
59
|
npm install
|
|
149
|
-
npm
|
|
60
|
+
npm start
|
|
61
|
+
npm pack --dry-run
|
|
150
62
|
```
|
|
151
63
|
|
|
152
|
-
For local
|
|
64
|
+
For local plugin testing:
|
|
153
65
|
|
|
154
66
|
```json
|
|
155
67
|
{
|
|
@@ -157,10 +69,4 @@ For local opencode plugin testing, point your opencode config at the package dir
|
|
|
157
69
|
}
|
|
158
70
|
```
|
|
159
71
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
To inspect the package contents before publishing:
|
|
163
|
-
|
|
164
|
-
```sh
|
|
165
|
-
npm pack --dry-run
|
|
166
|
-
```
|
|
72
|
+
Restart opencode after changing plugin code or config.
|
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -16,6 +16,9 @@ const ICON_PATH = process.env.OPENCODE_PET_ICON || path.join(__dirname, "..", "a
|
|
|
16
16
|
const MAX_EVENTS = 100
|
|
17
17
|
const MAX_INTERACTION_EVENTS = 80
|
|
18
18
|
const IDLE_TTL_MS = 5 * 60 * 1000
|
|
19
|
+
const PET_WINDOW_SIZE = 120
|
|
20
|
+
const MIN_PET_SIZE_SCALE = 0.3
|
|
21
|
+
const MAX_PET_SIZE_SCALE = 3
|
|
19
22
|
|
|
20
23
|
let petWindow = null
|
|
21
24
|
let panelWindow = null
|
|
@@ -30,18 +33,70 @@ let pendingPermissionsByRequest = new Map()
|
|
|
30
33
|
let pendingQuestionsByRequest = new Map()
|
|
31
34
|
let cleanupTimer = null
|
|
32
35
|
let dragState = null
|
|
36
|
+
let petSizeScale = 1
|
|
37
|
+
|
|
38
|
+
function normalizeDragPoint(point) {
|
|
39
|
+
const screenX = Number(point?.screenX)
|
|
40
|
+
const screenY = Number(point?.screenY)
|
|
41
|
+
if (!Number.isFinite(screenX) || !Number.isFinite(screenY)) return null
|
|
42
|
+
return { screenX, screenY }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clamp(value, min, max) {
|
|
46
|
+
return Math.max(min, Math.min(max, value))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getVirtualWorkArea() {
|
|
50
|
+
const displays = screen.getAllDisplays()
|
|
51
|
+
if (!displays.length) return screen.getPrimaryDisplay().workArea
|
|
52
|
+
|
|
53
|
+
return displays.reduce((area, display) => {
|
|
54
|
+
const next = display.workArea
|
|
55
|
+
const left = Math.min(area.x, next.x)
|
|
56
|
+
const top = Math.min(area.y, next.y)
|
|
57
|
+
const right = Math.max(area.x + area.width, next.x + next.width)
|
|
58
|
+
const bottom = Math.max(area.y + area.height, next.y + next.height)
|
|
59
|
+
return { x: left, y: top, width: right - left, height: bottom - top }
|
|
60
|
+
}, displays[0].workArea)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeWindowPosition(x, y, win) {
|
|
64
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !win || win.isDestroyed()) return null
|
|
65
|
+
|
|
66
|
+
const bounds = win.getBounds()
|
|
67
|
+
const area = getVirtualWorkArea()
|
|
68
|
+
const minX = area.x
|
|
69
|
+
const minY = area.y
|
|
70
|
+
const maxX = area.x + area.width - Math.max(1, bounds.width)
|
|
71
|
+
const maxY = area.y + area.height - Math.max(1, bounds.height)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
x: Math.round(clamp(x, Math.min(minX, maxX), Math.max(minX, maxX))),
|
|
75
|
+
y: Math.round(clamp(y, Math.min(minY, maxY), Math.max(minY, maxY))),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
33
78
|
|
|
34
79
|
ipcMain.on("opencube-drag-start", (event, point) => {
|
|
35
80
|
if (!petWindow || petWindow.isDestroyed()) return
|
|
81
|
+
const dragPoint = normalizeDragPoint(point)
|
|
82
|
+
if (!dragPoint) return
|
|
36
83
|
const [x, y] = petWindow.getPosition()
|
|
37
|
-
dragState = { windowX: x, windowY: y, screenX:
|
|
84
|
+
dragState = { windowX: x, windowY: y, screenX: dragPoint.screenX, screenY: dragPoint.screenY }
|
|
38
85
|
})
|
|
39
86
|
|
|
40
87
|
ipcMain.on("opencube-drag-move", (event, point) => {
|
|
41
88
|
if (!petWindow || petWindow.isDestroyed() || !dragState) return
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
const dragPoint = normalizeDragPoint(point)
|
|
90
|
+
if (!dragPoint) return
|
|
91
|
+
const nextX = Math.round(dragState.windowX + dragPoint.screenX - dragState.screenX)
|
|
92
|
+
const nextY = Math.round(dragState.windowY + dragPoint.screenY - dragState.screenY)
|
|
93
|
+
const nextPosition = normalizeWindowPosition(nextX, nextY, petWindow)
|
|
94
|
+
if (!nextPosition) return
|
|
95
|
+
try {
|
|
96
|
+
petWindow.setPosition(nextPosition.x, nextPosition.y, false)
|
|
97
|
+
} catch {
|
|
98
|
+
dragState = null
|
|
99
|
+
}
|
|
45
100
|
})
|
|
46
101
|
|
|
47
102
|
ipcMain.on("opencube-drag-end", () => {
|
|
@@ -52,12 +107,22 @@ function ensureDataDir() {
|
|
|
52
107
|
fs.mkdirSync(DATA_DIR, { recursive: true })
|
|
53
108
|
}
|
|
54
109
|
|
|
110
|
+
function readState() {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
|
|
113
|
+
} catch {
|
|
114
|
+
return {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
55
118
|
function writeState(extra = {}) {
|
|
56
119
|
ensureDataDir()
|
|
120
|
+
const previous = readState()
|
|
57
121
|
fs.writeFileSync(
|
|
58
122
|
STATE_FILE,
|
|
59
123
|
JSON.stringify(
|
|
60
124
|
{
|
|
125
|
+
...previous,
|
|
61
126
|
pid: process.pid,
|
|
62
127
|
startedAt: Date.now(),
|
|
63
128
|
iconPath: ICON_PATH,
|
|
@@ -69,6 +134,34 @@ function writeState(extra = {}) {
|
|
|
69
134
|
)
|
|
70
135
|
}
|
|
71
136
|
|
|
137
|
+
function normalizePetSizeScale(value) {
|
|
138
|
+
const scale = Number(value)
|
|
139
|
+
if (!Number.isFinite(scale)) return undefined
|
|
140
|
+
return Math.max(MIN_PET_SIZE_SCALE, Math.min(MAX_PET_SIZE_SCALE, scale))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getPetWindowSize() {
|
|
144
|
+
return Math.round(PET_WINDOW_SIZE * petSizeScale)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function loadPetSizeScale() {
|
|
148
|
+
petSizeScale = normalizePetSizeScale(readState().sizeScale) ?? 1
|
|
149
|
+
return petSizeScale
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function applyPetSizeScale(scale) {
|
|
153
|
+
const nextScale = normalizePetSizeScale(scale)
|
|
154
|
+
if (nextScale === undefined) throw new Error(`sizeScale must be a number between ${MIN_PET_SIZE_SCALE} and ${MAX_PET_SIZE_SCALE}`)
|
|
155
|
+
petSizeScale = nextScale
|
|
156
|
+
const size = getPetWindowSize()
|
|
157
|
+
writeState({ sizeScale: petSizeScale, size: { width: size, height: size } })
|
|
158
|
+
if (petWindow && !petWindow.isDestroyed()) {
|
|
159
|
+
petWindow.setSize(size, size, false)
|
|
160
|
+
petWindow.loadFile(writePetHtmlFile())
|
|
161
|
+
}
|
|
162
|
+
return { scale: petSizeScale, width: size, height: size }
|
|
163
|
+
}
|
|
164
|
+
|
|
72
165
|
function shouldQuit(argv = process.argv) {
|
|
73
166
|
return argv.includes("--quit") || argv.includes("--stop") || argv.includes("stop") || argv.includes("quit")
|
|
74
167
|
}
|
|
@@ -680,6 +773,7 @@ function petHtml3D() {
|
|
|
680
773
|
const initialStateJson = JSON.stringify(getPetState()).replaceAll("<", "\\u003c")
|
|
681
774
|
const iconUrl = createIconDataUrl()
|
|
682
775
|
const threeCjsPath = JSON.stringify(path.join(path.dirname(require.resolve("three")), "three.cjs"))
|
|
776
|
+
const renderSize = getPetWindowSize()
|
|
683
777
|
return `<!doctype html>
|
|
684
778
|
<html>
|
|
685
779
|
<head>
|
|
@@ -704,8 +798,8 @@ function petHtml3D() {
|
|
|
704
798
|
#scene {
|
|
705
799
|
position: absolute;
|
|
706
800
|
inset: 0;
|
|
707
|
-
width:
|
|
708
|
-
height:
|
|
801
|
+
width: ${renderSize}px;
|
|
802
|
+
height: ${renderSize}px;
|
|
709
803
|
pointer-events: none;
|
|
710
804
|
}
|
|
711
805
|
#hit-layer {
|
|
@@ -734,6 +828,7 @@ function petHtml3D() {
|
|
|
734
828
|
const THREE = require(${threeCjsPath})
|
|
735
829
|
const { ipcRenderer } = require("electron")
|
|
736
830
|
|
|
831
|
+
const PET_RENDER_SIZE = ${renderSize}
|
|
737
832
|
window.__PET_STATE = ${initialStateJson}
|
|
738
833
|
const stage = document.querySelector(".stage")
|
|
739
834
|
const hitLayer = document.getElementById("hit-layer")
|
|
@@ -765,7 +860,7 @@ function petHtml3D() {
|
|
|
765
860
|
camera.position.set(0, 0.04, 3.25)
|
|
766
861
|
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true })
|
|
767
862
|
renderer.setClearColor(0x000000, 0)
|
|
768
|
-
renderer.setPixelRatio(Math.min(
|
|
863
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
|
769
864
|
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
770
865
|
|
|
771
866
|
const cubeGroup = new THREE.Group()
|
|
@@ -1093,10 +1188,8 @@ function petHtml3D() {
|
|
|
1093
1188
|
}
|
|
1094
1189
|
|
|
1095
1190
|
function resize() {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
renderer.setSize(width, height, false)
|
|
1099
|
-
camera.aspect = width / height
|
|
1191
|
+
renderer.setSize(PET_RENDER_SIZE, PET_RENDER_SIZE, false)
|
|
1192
|
+
camera.aspect = 1
|
|
1100
1193
|
camera.updateProjectionMatrix()
|
|
1101
1194
|
}
|
|
1102
1195
|
window.addEventListener("resize", resize)
|
|
@@ -1631,6 +1724,16 @@ function petHtml3D() {
|
|
|
1631
1724
|
busyFaces,
|
|
1632
1725
|
helloFlashes,
|
|
1633
1726
|
permissionGlows,
|
|
1727
|
+
renderSize: {
|
|
1728
|
+
fixed: PET_RENDER_SIZE,
|
|
1729
|
+
innerWidth: window.innerWidth,
|
|
1730
|
+
innerHeight: window.innerHeight,
|
|
1731
|
+
devicePixelRatio: window.devicePixelRatio || 1,
|
|
1732
|
+
canvasWidth: canvas.width,
|
|
1733
|
+
canvasHeight: canvas.height,
|
|
1734
|
+
canvasClientWidth: canvas.clientWidth,
|
|
1735
|
+
canvasClientHeight: canvas.clientHeight,
|
|
1736
|
+
},
|
|
1634
1737
|
}
|
|
1635
1738
|
requestAnimationFrame(tick)
|
|
1636
1739
|
}
|
|
@@ -1658,7 +1761,7 @@ function restorePosition(win) {
|
|
|
1658
1761
|
try {
|
|
1659
1762
|
const raw = fs.readFileSync(STATE_FILE, "utf8")
|
|
1660
1763
|
const state = JSON.parse(raw)
|
|
1661
|
-
if (
|
|
1764
|
+
if (Number.isFinite(state.x) && Number.isFinite(state.y)) {
|
|
1662
1765
|
win.setPosition(state.x, state.y, false)
|
|
1663
1766
|
return
|
|
1664
1767
|
}
|
|
@@ -1675,9 +1778,10 @@ function restorePosition(win) {
|
|
|
1675
1778
|
|
|
1676
1779
|
function createPetWindow() {
|
|
1677
1780
|
if (petWindow && !petWindow.isDestroyed()) return petWindow
|
|
1781
|
+
const size = getPetWindowSize()
|
|
1678
1782
|
petWindow = new BrowserWindow({
|
|
1679
|
-
width:
|
|
1680
|
-
height:
|
|
1783
|
+
width: size,
|
|
1784
|
+
height: size,
|
|
1681
1785
|
show: false,
|
|
1682
1786
|
frame: false,
|
|
1683
1787
|
resizable: false,
|
|
@@ -1833,6 +1937,7 @@ function startServer() {
|
|
|
1833
1937
|
port: PORT,
|
|
1834
1938
|
events: events.length,
|
|
1835
1939
|
pet: "opencube",
|
|
1940
|
+
size: { scale: petSizeScale, width: getPetWindowSize(), height: getPetWindowSize() },
|
|
1836
1941
|
sessions: getPetState().sessions.map(({ sessionID, state, busyIndex, idleIndex, color }) => ({
|
|
1837
1942
|
sessionID,
|
|
1838
1943
|
state,
|
|
@@ -1882,6 +1987,12 @@ function startServer() {
|
|
|
1882
1987
|
return json(res, 200, { ok: true })
|
|
1883
1988
|
}
|
|
1884
1989
|
|
|
1990
|
+
if (req.method === "POST" && url.pathname === "/size") {
|
|
1991
|
+
const body = await readRequestJson(req)
|
|
1992
|
+
const size = applyPetSizeScale(body?.scale)
|
|
1993
|
+
return json(res, 200, { ok: true, size })
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1885
1996
|
if (req.method === "POST" && url.pathname === "/toggle-panel") {
|
|
1886
1997
|
togglePanel()
|
|
1887
1998
|
return json(res, 200, { ok: true, visible: panelWindow?.isVisible() === true })
|
|
@@ -1906,6 +2017,7 @@ function startServer() {
|
|
|
1906
2017
|
|
|
1907
2018
|
function start() {
|
|
1908
2019
|
app.dock?.hide()
|
|
2020
|
+
loadPetSizeScale()
|
|
1909
2021
|
writeState({ visible: false, mode: "floating" })
|
|
1910
2022
|
createTray()
|
|
1911
2023
|
startServer()
|
package/src/plugin-server.cjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
const { quitPet, sendEvent, showPet } = require("./plugin-shared.cjs")
|
|
1
|
+
const { quitPet, requestPet, sendEvent, showPet } = require("./plugin-shared.cjs")
|
|
2
2
|
const pkg = require("../package.json")
|
|
3
3
|
|
|
4
4
|
const COMMAND_HANDLED_SENTINEL = "__OPENCODE_PET_COMMAND_HANDLED__"
|
|
5
5
|
const CUB_ICON = "◈"
|
|
6
6
|
const UPDATE_CHECK_TIMEOUT_MS = 5000
|
|
7
|
+
const MIN_PET_SIZE_SCALE = 0.3
|
|
8
|
+
const MAX_PET_SIZE_SCALE = 3
|
|
7
9
|
|
|
8
10
|
function handled() {
|
|
9
11
|
throw new Error(COMMAND_HANDLED_SENTINEL)
|
|
@@ -33,6 +35,31 @@ function isUpdateCheck(input) {
|
|
|
33
35
|
return input.command === "pet_update" || input.command === "pet_upgrade"
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
function isPetSize(input) {
|
|
39
|
+
return input.command === "pet-size" || input.command === "pet_size"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function petSizeUsage() {
|
|
43
|
+
return cubNotice(
|
|
44
|
+
[
|
|
45
|
+
"Set OpenCube size with: /pet-size 0.7",
|
|
46
|
+
`Allowed scale: ${MIN_PET_SIZE_SCALE} to ${MAX_PET_SIZE_SCALE}.`,
|
|
47
|
+
"Run /pet first if OpenCube is sleeping.",
|
|
48
|
+
].join("\n"),
|
|
49
|
+
"↔",
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parsePetSizeScale(args) {
|
|
54
|
+
const text = textOfArguments(args)
|
|
55
|
+
if (!text) return undefined
|
|
56
|
+
const first = text.split(/\s+/)[0]
|
|
57
|
+
const scale = Number(first)
|
|
58
|
+
if (!Number.isFinite(scale)) return undefined
|
|
59
|
+
if (scale < MIN_PET_SIZE_SCALE || scale > MAX_PET_SIZE_SCALE) return undefined
|
|
60
|
+
return scale
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
function compareVersions(a, b) {
|
|
37
64
|
const left = String(a || "").split(".").map((part) => Number.parseInt(part, 10) || 0)
|
|
38
65
|
const right = String(b || "").split(".").map((part) => Number.parseInt(part, 10) || 0)
|
|
@@ -161,10 +188,18 @@ module.exports = {
|
|
|
161
188
|
template: "/pet_upgrade",
|
|
162
189
|
description: "Alias for /pet_update.",
|
|
163
190
|
}
|
|
191
|
+
cfg.command["pet-size"] = {
|
|
192
|
+
template: "/pet-size",
|
|
193
|
+
description: "Show or set OpenCube size, for example /pet-size 0.7.",
|
|
194
|
+
}
|
|
195
|
+
cfg.command.pet_size = {
|
|
196
|
+
template: "/pet_size",
|
|
197
|
+
description: "Alias for /pet-size.",
|
|
198
|
+
}
|
|
164
199
|
},
|
|
165
200
|
|
|
166
201
|
"command.execute.before": async (input, output) => {
|
|
167
|
-
if (!["pet", "pet_stop", "pet_say_hello", "pet_fancy_say_hello", "pet_update", "pet_upgrade"].includes(input.command)) return
|
|
202
|
+
if (!["pet", "pet_stop", "pet_say_hello", "pet_fancy_say_hello", "pet_update", "pet_upgrade", "pet-size", "pet_size"].includes(input.command)) return
|
|
168
203
|
|
|
169
204
|
// There is no official cancel primitive in command.execute.before yet.
|
|
170
205
|
// Throwing this sentinel aborts the command flow before opencode sends
|
|
@@ -238,6 +273,22 @@ module.exports = {
|
|
|
238
273
|
}
|
|
239
274
|
|
|
240
275
|
await injectNotice(client, input.sessionID, formatUpdateNotice(latest))
|
|
276
|
+
} else if (isPetSize(input)) {
|
|
277
|
+
const scale = parsePetSizeScale(input.arguments)
|
|
278
|
+
if (scale === undefined) {
|
|
279
|
+
await injectNotice(client, input.sessionID, petSizeUsage())
|
|
280
|
+
} else {
|
|
281
|
+
const result = await requestPet("/size", { method: "POST", body: { scale }, timeoutMs: 1000 })
|
|
282
|
+
if (result?.ok && result.size) {
|
|
283
|
+
await injectNotice(
|
|
284
|
+
client,
|
|
285
|
+
input.sessionID,
|
|
286
|
+
cubNotice(`OpenCube size set to ${result.size.scale} (${result.size.width}×${result.size.height}).`, "↔"),
|
|
287
|
+
)
|
|
288
|
+
} else {
|
|
289
|
+
await injectNotice(client, input.sessionID, cubNotice("OpenCube is sleeping... zzz Run /pet first, then try /pet-size 0.7.", "☾"))
|
|
290
|
+
}
|
|
291
|
+
}
|
|
241
292
|
} else {
|
|
242
293
|
await showPet({
|
|
243
294
|
onProgress: (message) => injectNotice(client, input.sessionID, message),
|