opencube 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencube",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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
@@ -26,6 +26,7 @@ let interactionEvents = []
26
26
  let petSignals = []
27
27
  let sessionMap = new Map()
28
28
  let activeToolsBySession = new Map()
29
+ let pendingPermissionsByRequest = new Map()
29
30
  let cleanupTimer = null
30
31
  let dragState = null
31
32
 
@@ -131,6 +132,7 @@ function recordEvent(event) {
131
132
  }
132
133
  applySessionEvent(item)
133
134
  applyToolEvent(item)
135
+ applyPermissionEvent(item)
134
136
  if (item.type === "hello" || item.type === "fancy_hello") {
135
137
  petSignals.push({
136
138
  id: item.id,
@@ -148,6 +150,36 @@ function recordEvent(event) {
148
150
  return item
149
151
  }
150
152
 
153
+ function applyPermissionEvent(event) {
154
+ if (!event || typeof event.sessionID !== "string") return
155
+
156
+ if (event.type === "permission.ask") {
157
+ const requestID = typeof event.requestID === "string" ? event.requestID : event.id
158
+ if (!requestID) return
159
+ pendingPermissionsByRequest.set(requestID, {
160
+ requestID,
161
+ sessionID: event.sessionID,
162
+ permission: event.permission,
163
+ patterns: event.patterns,
164
+ metadata: event.metadata,
165
+ always: event.always,
166
+ tool: event.tool,
167
+ askedAt: event.receivedAt || Date.now(),
168
+ })
169
+ return
170
+ }
171
+
172
+ if (event.type === "permission.reply") {
173
+ if (typeof event.requestID === "string") pendingPermissionsByRequest.delete(event.requestID)
174
+ }
175
+ }
176
+
177
+ function clearPendingPermissionsForSession(sessionID) {
178
+ for (const [requestID, permission] of pendingPermissionsByRequest) {
179
+ if (permission?.sessionID === sessionID) pendingPermissionsByRequest.delete(requestID)
180
+ }
181
+ }
182
+
151
183
  function applyToolEvent(event) {
152
184
  if (!event || typeof event.sessionID !== "string" || typeof event.callID !== "string") return
153
185
  if (event.type !== "tool.start" && event.type !== "tool.finish") return
@@ -204,6 +236,7 @@ function applySessionEvent(event) {
204
236
 
205
237
  if (event.type === "session.idle") {
206
238
  activeToolsBySession.delete(event.sessionID)
239
+ clearPendingPermissionsForSession(event.sessionID)
207
240
  sessionMap.set(event.sessionID, {
208
241
  sessionID: event.sessionID,
209
242
  state: "idle",
@@ -225,6 +258,7 @@ function pruneIdleSessions(refresh = true) {
225
258
  if (session.state === "idle" && expiresFrom && now - expiresFrom > IDLE_TTL_MS) {
226
259
  sessionMap.delete(sessionID)
227
260
  activeToolsBySession.delete(sessionID)
261
+ clearPendingPermissionsForSession(sessionID)
228
262
  changed = true
229
263
  }
230
264
  }
@@ -259,6 +293,7 @@ function getPetState() {
259
293
  idleIndex: session.state === "idle" ? idleIndex++ : undefined,
260
294
  color: DEFAULT_SESSION_COLORS[index % DEFAULT_SESSION_COLORS.length],
261
295
  })),
296
+ permissions: Array.from(pendingPermissionsByRequest.values()),
262
297
  signals: petSignals,
263
298
  }
264
299
  }
@@ -678,6 +713,7 @@ function petHtml3D() {
678
713
  const colorReleaseSpeed = 90
679
714
  const faceMeshes = new Map()
680
715
  const glowMeshes = new Map()
716
+ const permissionGlowMeshes = new Map()
681
717
  let snapshot = window.__PET_STATE || { sessions: [] }
682
718
  let lastFrame = performance.now()
683
719
  let rotation = { x: -14, y: -28, z: 0 }
@@ -723,6 +759,7 @@ function petHtml3D() {
723
759
  const dragParticleTexture = createDragParticleTexture()
724
760
  const faceGeometry = new THREE.PlaneGeometry(0.60, 0.60)
725
761
  const glowGeometry = new THREE.PlaneGeometry(1.18, 1.18)
762
+ const permissionGlowGeometry = new THREE.PlaneGeometry(1.42, 1.42)
726
763
  const rad = THREE.MathUtils.degToRad
727
764
 
728
765
  function createGlowTexture() {
@@ -763,6 +800,8 @@ function petHtml3D() {
763
800
  const dragParticles = []
764
801
  const toolParticles = []
765
802
  const toolEmitAccumulators = new Map()
803
+ const toolEmissionStates = new Map()
804
+ const toolEmissionHoldMs = 2000
766
805
  const faceVectors = {
767
806
  front: { position: new THREE.Vector3(0, 0, 0.34), normal: new THREE.Vector3(0, 0, 1) },
768
807
  back: { position: new THREE.Vector3(0, 0, -0.34), normal: new THREE.Vector3(0, 0, -1) },
@@ -855,25 +894,61 @@ function petHtml3D() {
855
894
  return true
856
895
  }
857
896
 
858
- function updateToolParticles(sessions, dt) {
859
- const activeToolSessions = sessions.filter((session) => session.state === "busy" && session.activeTools?.length > 0)
860
- const activeIDs = new Set(activeToolSessions.map((session) => session.sessionID))
897
+ function updateToolParticles(sessions, dt, now) {
898
+ const activeIDs = new Set()
899
+ const emitters = []
900
+ const busyIDs = new Set(
901
+ sessions
902
+ .filter((session) => session?.state === "busy" && typeof session.sessionID === "string")
903
+ .map((session) => session.sessionID),
904
+ )
905
+
906
+ for (const session of sessions) {
907
+ if (session?.state !== "busy" || typeof session.sessionID !== "string" || !session.activeTools?.length) continue
908
+ const currentState = toolEmissionStates.get(session.sessionID)
909
+ const faceName = sessionFaceMap.get(session.sessionID) || currentState?.faceName
910
+ if (!faceName) continue
911
+ const color = sessionColorMap.get(session.sessionID) || currentState?.color || randomSessionGlowColor()
912
+
913
+ activeIDs.add(session.sessionID)
914
+ toolEmissionStates.set(session.sessionID, {
915
+ faceName,
916
+ color,
917
+ holdUntil: now + toolEmissionHoldMs,
918
+ })
919
+ emitters.push({ sessionID: session.sessionID, faceName, color, held: false, holdRemainingMs: toolEmissionHoldMs })
920
+ }
921
+
922
+ for (const [sessionID, state] of Array.from(toolEmissionStates.entries())) {
923
+ if (activeIDs.has(sessionID)) continue
924
+ if (!busyIDs.has(sessionID) || !state?.faceName || state.holdUntil <= now) {
925
+ toolEmissionStates.delete(sessionID)
926
+ toolEmitAccumulators.delete(sessionID)
927
+ continue
928
+ }
929
+ emitters.push({
930
+ sessionID,
931
+ faceName: state.faceName,
932
+ color: state.color || randomSessionGlowColor(),
933
+ held: true,
934
+ holdRemainingMs: state.holdUntil - now,
935
+ })
936
+ }
937
+
938
+ const emittingIDs = new Set(emitters.map((emitter) => emitter.sessionID))
861
939
  for (const sessionID of Array.from(toolEmitAccumulators.keys())) {
862
- if (!activeIDs.has(sessionID)) toolEmitAccumulators.delete(sessionID)
940
+ if (!emittingIDs.has(sessionID)) toolEmitAccumulators.delete(sessionID)
863
941
  }
864
942
 
865
- for (const session of activeToolSessions) {
866
- const faceName = sessionFaceMap.get(session.sessionID)
867
- if (!faceName) continue
868
- const color = sessionColorMap.get(session.sessionID) || randomSessionGlowColor()
943
+ for (const emitter of emitters) {
869
944
  const jitterRate = randomBetween(7.5, 11.5)
870
- const next = (toolEmitAccumulators.get(session.sessionID) || 0) + dt * jitterRate
945
+ const next = (toolEmitAccumulators.get(emitter.sessionID) || 0) + dt * jitterRate
871
946
  let accumulator = next
872
947
  while (accumulator >= 1) {
873
- if (!emitToolParticle(faceName, color)) break
948
+ if (!emitToolParticle(emitter.faceName, emitter.color)) break
874
949
  accumulator -= 1
875
950
  }
876
- toolEmitAccumulators.set(session.sessionID, accumulator)
951
+ toolEmitAccumulators.set(emitter.sessionID, accumulator)
877
952
  }
878
953
 
879
954
  let activeCount = 0
@@ -895,8 +970,9 @@ function petHtml3D() {
895
970
  particle.scale.setScalar(data.size * (1.02 + age * 0.42))
896
971
  particle.material.opacity = Math.min(1, 0.24 + Math.sin(age * Math.PI) * 1.18)
897
972
  }
898
- toolParticleGroup.visible = activeCount > 0 || activeToolSessions.length > 0
899
- return { activeSessions: activeToolSessions.length, activeCount }
973
+ const heldSessions = emitters.filter((emitter) => emitter.held).length
974
+ toolParticleGroup.visible = activeCount > 0 || emitters.length > 0
975
+ return { activeSessions: activeIDs.size, emittingSessions: emitters.length, heldSessions, activeCount }
900
976
  }
901
977
 
902
978
  function activeDragColors() {
@@ -1198,6 +1274,23 @@ function petHtml3D() {
1198
1274
  glow.rotation.set(...rotation)
1199
1275
  cubeGroup.add(glow)
1200
1276
  glowMeshes.set(name, glow)
1277
+
1278
+ const permissionGlow = new THREE.Mesh(
1279
+ permissionGlowGeometry,
1280
+ new THREE.MeshBasicMaterial({
1281
+ map: glowTexture,
1282
+ color: 0xff2448,
1283
+ transparent: true,
1284
+ opacity: 0,
1285
+ blending: THREE.AdditiveBlending,
1286
+ depthWrite: false,
1287
+ side: THREE.DoubleSide,
1288
+ }),
1289
+ )
1290
+ permissionGlow.position.set(...glowPosition.map((value) => value === 0 ? 0 : value * 1.055))
1291
+ permissionGlow.rotation.set(...rotation)
1292
+ cubeGroup.add(permissionGlow)
1293
+ permissionGlowMeshes.set(name, permissionGlow)
1201
1294
  }
1202
1295
 
1203
1296
  function magnitude(vector) {
@@ -1384,6 +1477,35 @@ function petHtml3D() {
1384
1477
  return active
1385
1478
  }
1386
1479
 
1480
+ function applyPermissionGlowFaces(permissions, now) {
1481
+ const pendingSessionIDs = new Set(
1482
+ (permissions || [])
1483
+ .map((permission) => permission?.sessionID)
1484
+ .filter((sessionID) => typeof sessionID === "string"),
1485
+ )
1486
+ const active = {}
1487
+ const wave = (Math.sin(now * 0.0095) + 1) / 2
1488
+ const pulse = Math.pow(wave, 1.85)
1489
+
1490
+ for (const faceName of faceOrder) {
1491
+ const permissionGlow = permissionGlowMeshes.get(faceName)
1492
+ if (!permissionGlow) continue
1493
+
1494
+ const sessionID = Array.from(pendingSessionIDs).find((id) => sessionFaceMap.get(id) === faceName)
1495
+ if (!sessionID) {
1496
+ permissionGlow.material.opacity = 0
1497
+ continue
1498
+ }
1499
+
1500
+ permissionGlow.material.color.setRGB(1, 0.08, 0.16)
1501
+ permissionGlow.material.opacity = 0.16 + pulse * 0.62
1502
+ permissionGlow.scale.setScalar(1.00 + pulse * 0.34)
1503
+ active[faceName] = { sessionID, pulse, pending: true }
1504
+ }
1505
+
1506
+ return active
1507
+ }
1508
+
1387
1509
  window.__setPetState = setSnapshot
1388
1510
  window.__getPetDebug = () => latestDebug
1389
1511
 
@@ -1440,9 +1562,10 @@ function petHtml3D() {
1440
1562
  const glowB = Math.round(232 + (210 - 232) * glow)
1441
1563
  const dragParticles = updateDragParticles(now, frictionHoldLevel, speed, dt)
1442
1564
  const busyFaces = syncBusyFaces(sessions, speed)
1443
- const toolParticles = updateToolParticles(sessions, dt)
1565
+ const toolParticles = updateToolParticles(sessions, dt, now)
1444
1566
  processSignals(snapshot.signals || [], now)
1445
1567
  const helloFlashes = applyFlashFaces(now)
1568
+ const permissionGlows = applyPermissionGlowFaces(snapshot.permissions || [], now)
1446
1569
  renderCube()
1447
1570
  latestDebug = {
1448
1571
  now: Date.now(),
@@ -1472,6 +1595,7 @@ function petHtml3D() {
1472
1595
  faceRotations,
1473
1596
  busyFaces,
1474
1597
  helloFlashes,
1598
+ permissionGlows,
1475
1599
  }
1476
1600
  requestAnimationFrame(tick)
1477
1601
  }
@@ -139,6 +139,36 @@ module.exports = {
139
139
  },
140
140
 
141
141
  event: async ({ event }) => {
142
+ if (event.type === "permission.asked") {
143
+ const permission = event.properties || {}
144
+ await sendEvent({
145
+ type: "permission.ask",
146
+ message: "opencode is waiting for permission",
147
+ sessionID: permission.sessionID,
148
+ requestID: permission.id,
149
+ permission: permission.permission,
150
+ patterns: permission.patterns,
151
+ metadata: permission.metadata,
152
+ always: permission.always,
153
+ tool: permission.tool,
154
+ source: "opencube-plugin",
155
+ })
156
+ return
157
+ }
158
+
159
+ if (event.type === "permission.replied") {
160
+ const permission = event.properties || {}
161
+ await sendEvent({
162
+ type: "permission.reply",
163
+ message: "opencode permission was answered",
164
+ sessionID: permission.sessionID,
165
+ requestID: permission.requestID,
166
+ reply: permission.reply,
167
+ source: "opencube-plugin",
168
+ })
169
+ return
170
+ }
171
+
142
172
  if (event.type !== "session.status") return
143
173
 
144
174
  const sessionID = event.properties?.sessionID