opencube 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencube",
3
- "version": "0.2.0",
3
+ "version": "0.3.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
@@ -26,6 +26,8 @@ let interactionEvents = []
26
26
  let petSignals = []
27
27
  let sessionMap = new Map()
28
28
  let activeToolsBySession = new Map()
29
+ let pendingPermissionsByRequest = new Map()
30
+ let pendingQuestionsByRequest = new Map()
29
31
  let cleanupTimer = null
30
32
  let dragState = null
31
33
 
@@ -131,6 +133,8 @@ function recordEvent(event) {
131
133
  }
132
134
  applySessionEvent(item)
133
135
  applyToolEvent(item)
136
+ applyPermissionEvent(item)
137
+ applyQuestionEvent(item)
134
138
  if (item.type === "hello" || item.type === "fancy_hello") {
135
139
  petSignals.push({
136
140
  id: item.id,
@@ -148,6 +152,63 @@ function recordEvent(event) {
148
152
  return item
149
153
  }
150
154
 
155
+ function applyPermissionEvent(event) {
156
+ if (!event || typeof event.sessionID !== "string") return
157
+
158
+ if (event.type === "permission.ask") {
159
+ const requestID = typeof event.requestID === "string" ? event.requestID : event.id
160
+ if (!requestID) return
161
+ pendingPermissionsByRequest.set(requestID, {
162
+ requestID,
163
+ sessionID: event.sessionID,
164
+ permission: event.permission,
165
+ patterns: event.patterns,
166
+ metadata: event.metadata,
167
+ always: event.always,
168
+ tool: event.tool,
169
+ askedAt: event.receivedAt || Date.now(),
170
+ })
171
+ return
172
+ }
173
+
174
+ if (event.type === "permission.reply") {
175
+ if (typeof event.requestID === "string") pendingPermissionsByRequest.delete(event.requestID)
176
+ }
177
+ }
178
+
179
+ function clearPendingPermissionsForSession(sessionID) {
180
+ for (const [requestID, permission] of pendingPermissionsByRequest) {
181
+ if (permission?.sessionID === sessionID) pendingPermissionsByRequest.delete(requestID)
182
+ }
183
+ }
184
+
185
+ function applyQuestionEvent(event) {
186
+ if (!event || typeof event.sessionID !== "string") return
187
+
188
+ if (event.type === "question.ask") {
189
+ const requestID = typeof event.requestID === "string" ? event.requestID : event.id
190
+ if (!requestID) return
191
+ pendingQuestionsByRequest.set(requestID, {
192
+ requestID,
193
+ sessionID: event.sessionID,
194
+ questions: event.questions,
195
+ tool: event.tool,
196
+ askedAt: event.receivedAt || Date.now(),
197
+ })
198
+ return
199
+ }
200
+
201
+ if (event.type === "question.reply" || event.type === "question.reject") {
202
+ if (typeof event.requestID === "string") pendingQuestionsByRequest.delete(event.requestID)
203
+ }
204
+ }
205
+
206
+ function clearPendingQuestionsForSession(sessionID) {
207
+ for (const [requestID, question] of pendingQuestionsByRequest) {
208
+ if (question?.sessionID === sessionID) pendingQuestionsByRequest.delete(requestID)
209
+ }
210
+ }
211
+
151
212
  function applyToolEvent(event) {
152
213
  if (!event || typeof event.sessionID !== "string" || typeof event.callID !== "string") return
153
214
  if (event.type !== "tool.start" && event.type !== "tool.finish") return
@@ -204,6 +265,8 @@ function applySessionEvent(event) {
204
265
 
205
266
  if (event.type === "session.idle") {
206
267
  activeToolsBySession.delete(event.sessionID)
268
+ clearPendingPermissionsForSession(event.sessionID)
269
+ clearPendingQuestionsForSession(event.sessionID)
207
270
  sessionMap.set(event.sessionID, {
208
271
  sessionID: event.sessionID,
209
272
  state: "idle",
@@ -225,6 +288,8 @@ function pruneIdleSessions(refresh = true) {
225
288
  if (session.state === "idle" && expiresFrom && now - expiresFrom > IDLE_TTL_MS) {
226
289
  sessionMap.delete(sessionID)
227
290
  activeToolsBySession.delete(sessionID)
291
+ clearPendingPermissionsForSession(sessionID)
292
+ clearPendingQuestionsForSession(sessionID)
228
293
  changed = true
229
294
  }
230
295
  }
@@ -259,6 +324,8 @@ function getPetState() {
259
324
  idleIndex: session.state === "idle" ? idleIndex++ : undefined,
260
325
  color: DEFAULT_SESSION_COLORS[index % DEFAULT_SESSION_COLORS.length],
261
326
  })),
327
+ permissions: Array.from(pendingPermissionsByRequest.values()),
328
+ questions: Array.from(pendingQuestionsByRequest.values()),
262
329
  signals: petSignals,
263
330
  }
264
331
  }
@@ -678,6 +745,7 @@ function petHtml3D() {
678
745
  const colorReleaseSpeed = 90
679
746
  const faceMeshes = new Map()
680
747
  const glowMeshes = new Map()
748
+ const permissionGlowMeshes = new Map()
681
749
  let snapshot = window.__PET_STATE || { sessions: [] }
682
750
  let lastFrame = performance.now()
683
751
  let rotation = { x: -14, y: -28, z: 0 }
@@ -723,6 +791,7 @@ function petHtml3D() {
723
791
  const dragParticleTexture = createDragParticleTexture()
724
792
  const faceGeometry = new THREE.PlaneGeometry(0.60, 0.60)
725
793
  const glowGeometry = new THREE.PlaneGeometry(1.18, 1.18)
794
+ const permissionGlowGeometry = new THREE.PlaneGeometry(1.62, 1.62)
726
795
  const rad = THREE.MathUtils.degToRad
727
796
 
728
797
  function createGlowTexture() {
@@ -763,6 +832,8 @@ function petHtml3D() {
763
832
  const dragParticles = []
764
833
  const toolParticles = []
765
834
  const toolEmitAccumulators = new Map()
835
+ const toolEmissionStates = new Map()
836
+ const toolEmissionHoldMs = 2000
766
837
  const faceVectors = {
767
838
  front: { position: new THREE.Vector3(0, 0, 0.34), normal: new THREE.Vector3(0, 0, 1) },
768
839
  back: { position: new THREE.Vector3(0, 0, -0.34), normal: new THREE.Vector3(0, 0, -1) },
@@ -855,25 +926,61 @@ function petHtml3D() {
855
926
  return true
856
927
  }
857
928
 
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))
929
+ function updateToolParticles(sessions, dt, now) {
930
+ const activeIDs = new Set()
931
+ const emitters = []
932
+ const busyIDs = new Set(
933
+ sessions
934
+ .filter((session) => session?.state === "busy" && typeof session.sessionID === "string")
935
+ .map((session) => session.sessionID),
936
+ )
937
+
938
+ for (const session of sessions) {
939
+ if (session?.state !== "busy" || typeof session.sessionID !== "string" || !session.activeTools?.length) continue
940
+ const currentState = toolEmissionStates.get(session.sessionID)
941
+ const faceName = sessionFaceMap.get(session.sessionID) || currentState?.faceName
942
+ if (!faceName) continue
943
+ const color = sessionColorMap.get(session.sessionID) || currentState?.color || randomSessionGlowColor()
944
+
945
+ activeIDs.add(session.sessionID)
946
+ toolEmissionStates.set(session.sessionID, {
947
+ faceName,
948
+ color,
949
+ holdUntil: now + toolEmissionHoldMs,
950
+ })
951
+ emitters.push({ sessionID: session.sessionID, faceName, color, held: false, holdRemainingMs: toolEmissionHoldMs })
952
+ }
953
+
954
+ for (const [sessionID, state] of Array.from(toolEmissionStates.entries())) {
955
+ if (activeIDs.has(sessionID)) continue
956
+ if (!busyIDs.has(sessionID) || !state?.faceName || state.holdUntil <= now) {
957
+ toolEmissionStates.delete(sessionID)
958
+ toolEmitAccumulators.delete(sessionID)
959
+ continue
960
+ }
961
+ emitters.push({
962
+ sessionID,
963
+ faceName: state.faceName,
964
+ color: state.color || randomSessionGlowColor(),
965
+ held: true,
966
+ holdRemainingMs: state.holdUntil - now,
967
+ })
968
+ }
969
+
970
+ const emittingIDs = new Set(emitters.map((emitter) => emitter.sessionID))
861
971
  for (const sessionID of Array.from(toolEmitAccumulators.keys())) {
862
- if (!activeIDs.has(sessionID)) toolEmitAccumulators.delete(sessionID)
972
+ if (!emittingIDs.has(sessionID)) toolEmitAccumulators.delete(sessionID)
863
973
  }
864
974
 
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()
975
+ for (const emitter of emitters) {
869
976
  const jitterRate = randomBetween(7.5, 11.5)
870
- const next = (toolEmitAccumulators.get(session.sessionID) || 0) + dt * jitterRate
977
+ const next = (toolEmitAccumulators.get(emitter.sessionID) || 0) + dt * jitterRate
871
978
  let accumulator = next
872
979
  while (accumulator >= 1) {
873
- if (!emitToolParticle(faceName, color)) break
980
+ if (!emitToolParticle(emitter.faceName, emitter.color)) break
874
981
  accumulator -= 1
875
982
  }
876
- toolEmitAccumulators.set(session.sessionID, accumulator)
983
+ toolEmitAccumulators.set(emitter.sessionID, accumulator)
877
984
  }
878
985
 
879
986
  let activeCount = 0
@@ -895,8 +1002,9 @@ function petHtml3D() {
895
1002
  particle.scale.setScalar(data.size * (1.02 + age * 0.42))
896
1003
  particle.material.opacity = Math.min(1, 0.24 + Math.sin(age * Math.PI) * 1.18)
897
1004
  }
898
- toolParticleGroup.visible = activeCount > 0 || activeToolSessions.length > 0
899
- return { activeSessions: activeToolSessions.length, activeCount }
1005
+ const heldSessions = emitters.filter((emitter) => emitter.held).length
1006
+ toolParticleGroup.visible = activeCount > 0 || emitters.length > 0
1007
+ return { activeSessions: activeIDs.size, emittingSessions: emitters.length, heldSessions, activeCount }
900
1008
  }
901
1009
 
902
1010
  function activeDragColors() {
@@ -1198,6 +1306,23 @@ function petHtml3D() {
1198
1306
  glow.rotation.set(...rotation)
1199
1307
  cubeGroup.add(glow)
1200
1308
  glowMeshes.set(name, glow)
1309
+
1310
+ const permissionGlow = new THREE.Mesh(
1311
+ permissionGlowGeometry,
1312
+ new THREE.MeshBasicMaterial({
1313
+ map: glowTexture,
1314
+ color: 0xffffff,
1315
+ transparent: true,
1316
+ opacity: 0,
1317
+ blending: THREE.AdditiveBlending,
1318
+ depthWrite: false,
1319
+ side: THREE.DoubleSide,
1320
+ }),
1321
+ )
1322
+ permissionGlow.position.set(...glowPosition.map((value) => value === 0 ? 0 : value * 1.072))
1323
+ permissionGlow.rotation.set(...rotation)
1324
+ cubeGroup.add(permissionGlow)
1325
+ permissionGlowMeshes.set(name, permissionGlow)
1201
1326
  }
1202
1327
 
1203
1328
  function magnitude(vector) {
@@ -1384,6 +1509,37 @@ function petHtml3D() {
1384
1509
  return active
1385
1510
  }
1386
1511
 
1512
+ function applyPermissionGlowFaces(permissions, now) {
1513
+ const pendingSessionIDs = new Set(
1514
+ (permissions || [])
1515
+ .map((permission) => permission?.sessionID)
1516
+ .filter((sessionID) => typeof sessionID === "string"),
1517
+ )
1518
+ const active = {}
1519
+ const wave = (Math.sin(now * 0.0095) + 1) / 2
1520
+ const pulse = Math.pow(wave, 1.85)
1521
+
1522
+ for (const faceName of faceOrder) {
1523
+ const permissionGlow = permissionGlowMeshes.get(faceName)
1524
+ if (!permissionGlow) continue
1525
+
1526
+ const sessionID = Array.from(pendingSessionIDs).find((id) => sessionFaceMap.get(id) === faceName)
1527
+ if (!sessionID) {
1528
+ permissionGlow.material.opacity = 0
1529
+ permissionGlow.scale.setScalar(1)
1530
+ continue
1531
+ }
1532
+
1533
+ const color = sessionColorMap.get(sessionID) || randomSessionGlowColor()
1534
+ permissionGlow.material.color.setRGB(color.r / 255, color.g / 255, color.b / 255)
1535
+ permissionGlow.material.opacity = 0.14 + pulse * 0.60
1536
+ permissionGlow.scale.setScalar(1.06 + pulse * 0.42)
1537
+ active[faceName] = { sessionID, pulse, pending: true, color: color.name, rgb: [color.r, color.g, color.b] }
1538
+ }
1539
+
1540
+ return active
1541
+ }
1542
+
1387
1543
  window.__setPetState = setSnapshot
1388
1544
  window.__getPetDebug = () => latestDebug
1389
1545
 
@@ -1440,9 +1596,11 @@ function petHtml3D() {
1440
1596
  const glowB = Math.round(232 + (210 - 232) * glow)
1441
1597
  const dragParticles = updateDragParticles(now, frictionHoldLevel, speed, dt)
1442
1598
  const busyFaces = syncBusyFaces(sessions, speed)
1443
- const toolParticles = updateToolParticles(sessions, dt)
1599
+ const toolParticles = updateToolParticles(sessions, dt, now)
1444
1600
  processSignals(snapshot.signals || [], now)
1445
1601
  const helloFlashes = applyFlashFaces(now)
1602
+ const pendingAttention = [...(snapshot.permissions || []), ...(snapshot.questions || [])]
1603
+ const permissionGlows = applyPermissionGlowFaces(pendingAttention, now)
1446
1604
  renderCube()
1447
1605
  latestDebug = {
1448
1606
  now: Date.now(),
@@ -1472,6 +1630,7 @@ function petHtml3D() {
1472
1630
  faceRotations,
1473
1631
  busyFaces,
1474
1632
  helloFlashes,
1633
+ permissionGlows,
1475
1634
  }
1476
1635
  requestAnimationFrame(tick)
1477
1636
  }
@@ -139,6 +139,75 @@ 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
+
172
+ if (event.type === "question.asked") {
173
+ const question = event.properties || {}
174
+ await sendEvent({
175
+ type: "question.ask",
176
+ message: "opencode is waiting for a question answer",
177
+ sessionID: question.sessionID,
178
+ requestID: question.id,
179
+ questions: question.questions,
180
+ tool: question.tool,
181
+ source: "opencube-plugin",
182
+ })
183
+ return
184
+ }
185
+
186
+ if (event.type === "question.replied") {
187
+ const question = event.properties || {}
188
+ await sendEvent({
189
+ type: "question.reply",
190
+ message: "opencode question was answered",
191
+ sessionID: question.sessionID,
192
+ requestID: question.requestID,
193
+ answers: question.answers,
194
+ source: "opencube-plugin",
195
+ })
196
+ return
197
+ }
198
+
199
+ if (event.type === "question.rejected") {
200
+ const question = event.properties || {}
201
+ await sendEvent({
202
+ type: "question.reject",
203
+ message: "opencode question was rejected",
204
+ sessionID: question.sessionID,
205
+ requestID: question.requestID,
206
+ source: "opencube-plugin",
207
+ })
208
+ return
209
+ }
210
+
142
211
  if (event.type !== "session.status") return
143
212
 
144
213
  const sessionID = event.properties?.sessionID