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 CHANGED
@@ -4,152 +4,64 @@
4
4
 
5
5
  # OpenCube
6
6
 
7
- OpenCube is a tiny desktop pet for [opencode](https://opencode.ai/).
7
+ A tiny desktop cube pet for [opencode](https://opencode.ai/).
8
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
- - 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
- Then restart opencode and run:
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.3.1 --global --force
26
+ opencode plugin opencube@0.4.0 --global --force
50
27
  ```
51
28
 
52
- Using an explicit version is recommended for upgrades because it avoids stale `latest` cache behavior. You can still install the npm latest tag if desired:
29
+ Then restart opencode and run `/pet` again.
53
30
 
54
- ```sh
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
- opencode plugin -> http://127.0.0.1:47832 -> Electron OpenCube
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
- - The first install may take a while because Electron is downloaded as a runtime dependency.
137
- - If Electron's platform binary is missing after installation, OpenCube attempts a first-run self-repair download.
138
- - If commands do not appear after installation, restart opencode.
139
- - OpenCube uses a local-only HTTP server on `127.0.0.1:47832`.
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
- ## Local development
144
-
145
- From this repository:
56
+ ## Development
146
57
 
147
58
  ```sh
148
59
  npm install
149
- npm run start
60
+ npm start
61
+ npm pack --dry-run
150
62
  ```
151
63
 
152
- For local opencode plugin testing, point your opencode config at the package directory:
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
- After changing plugin files or opencode config, restart opencode.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencube",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A tiny Three.js desktop pet for opencode session activity.",
5
5
  "main": "src/main.js",
6
6
  "exports": {
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: point.screenX, screenY: point.screenY }
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 nextX = Math.round(dragState.windowX + point.screenX - dragState.screenX)
43
- const nextY = Math.round(dragState.windowY + point.screenY - dragState.screenY)
44
- petWindow.setPosition(nextX, nextY, false)
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: 100%;
708
- height: 100%;
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((window.devicePixelRatio || 1) * 1.5, 3))
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
- const width = Math.max(1, window.innerWidth)
1097
- const height = Math.max(1, window.innerHeight)
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 (typeof state.x === "number" && typeof state.y === "number") {
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: 120,
1680
- height: 120,
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()
@@ -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),