@saluzi/saluzi-edu 0.1.74 → 0.1.76

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.
@@ -31,7 +31,8 @@ const waitingPillEl = document.getElementById('waiting-pill')
31
31
  const sessionsEl = document.getElementById('sessions')
32
32
  const moreLineEl = document.getElementById('more-line')
33
33
  const ctxMenu = document.getElementById('ctx-menu')
34
- const ctxClose = document.getElementById('ctx-close')
34
+ const ctxMode = document.getElementById('ctx-mode')
35
+ const closeBtn = document.getElementById('close-btn')
35
36
 
36
37
  // ---------------------------------------------------------------------------
37
38
  // WebSocket connection
@@ -65,9 +66,18 @@ function connectWebSocket() {
65
66
 
66
67
  ws.onmessage = function (event) {
67
68
  try {
68
- sessions = JSON.parse(event.data)
69
- updateCounts()
70
- updateSessionRows()
69
+ const data = JSON.parse(event.data)
70
+ if (Array.isArray(data)) {
71
+ sessions = data
72
+ updateCounts()
73
+ updateSessionRows()
74
+ } else if (data && data.type === 'focus') {
75
+ // Server relayed a focus request — tell electron main process to
76
+ // activate the terminal window for this PID.
77
+ if (window.parade && typeof window.parade.focusSession === 'function') {
78
+ window.parade.focusSession(data.pid)
79
+ }
80
+ }
71
81
  } catch (_) {
72
82
  // Ignore malformed messages
73
83
  }
@@ -182,13 +192,30 @@ function buildSessionRow(s, kind) {
182
192
  if (promptText.length > MAX_PROMPT_LENGTH) {
183
193
  promptText = promptText.substring(0, MAX_PROMPT_LENGTH) + '...'
184
194
  }
195
+ var text = document.createElement('span')
196
+ text.className = 'session-text'
185
197
  if (kind === 'idle') {
186
- row.textContent = promptText || '(idle)'
198
+ text.textContent = promptText || '(idle)'
187
199
  } else if (kind === 'blocked') {
188
- row.textContent = promptText || '(needs approval)'
200
+ text.textContent = promptText || '(needs approval)'
189
201
  } else {
190
- row.textContent = promptText || '(running)'
202
+ text.textContent = promptText || '(running)'
191
203
  }
204
+ row.appendChild(text)
205
+
206
+ // Jump-to-terminal icon — appears on hover, sends focus request via WS
207
+ var jump = document.createElement('span')
208
+ jump.className = 'session-jump'
209
+ jump.textContent = '\u25B8' // ▸
210
+ jump.title = 'Jump to terminal'
211
+ jump.addEventListener('click', function (e) {
212
+ e.stopPropagation()
213
+ if (ws && ws.readyState === WebSocket.OPEN) {
214
+ ws.send(JSON.stringify({ type: 'focus', pid: s.pid }))
215
+ }
216
+ })
217
+ row.appendChild(jump)
218
+
192
219
  return row
193
220
  }
194
221
 
@@ -198,6 +225,7 @@ function buildSessionRow(s, kind) {
198
225
 
199
226
  document.addEventListener('contextmenu', function (e) {
200
227
  e.preventDefault()
228
+ updateModeLabel()
201
229
  ctxMenu.style.display = 'block'
202
230
  ctxMenu.style.left = e.clientX + 'px'
203
231
  ctxMenu.style.top = e.clientY + 'px'
@@ -215,6 +243,7 @@ if (window.parade && typeof window.parade.startDrag === 'function') {
215
243
  document.addEventListener('mousedown', function (e) {
216
244
  if (e.button !== 0) return // left button only
217
245
  if (e.target.closest('#ctx-menu')) return // don't drag from menu
246
+ if (e.target.closest('#close-btn')) return // don't drag from close btn
218
247
  var cornerEl = e.target.closest('[data-corner]')
219
248
  if (cornerEl && typeof window.parade.startResize === 'function') {
220
249
  window.parade.startResize(cornerEl.dataset.corner)
@@ -238,17 +267,19 @@ document.addEventListener('click', function () {
238
267
  ctxMenu.style.display = 'none'
239
268
  })
240
269
 
241
- ctxClose.addEventListener('click', function () {
242
- ctxMenu.style.display = 'none'
270
+ // ---------------------------------------------------------------------------
271
+ // Close button (top-right corner)
272
+ // ---------------------------------------------------------------------------
273
+
274
+ closeBtn.addEventListener('click', function () {
243
275
  // POST /shutdown to stop the parade server — this releases the port and
244
276
  // tears down all connections. The electron window process polls /health
245
- // every 1s; after 3 failures it quits, closing the overlay.
277
+ // every 1s; after 10 failures it quits, closing the overlay.
246
278
  fetch('http://localhost:' + port + '/shutdown', { method: 'POST' })
247
279
  .catch(function () {
248
280
  /* server may already be down */
249
281
  })
250
282
  .finally(function () {
251
- // Also try window.close() — harmless if it no-ops (electron ignores it).
252
283
  try {
253
284
  window.close()
254
285
  } catch (_) {
@@ -257,6 +288,37 @@ ctxClose.addEventListener('click', function () {
257
288
  })
258
289
  })
259
290
 
291
+ // ---------------------------------------------------------------------------
292
+ // Dynamic / Static cube mode toggle
293
+ // ---------------------------------------------------------------------------
294
+ // Dynamic (default): cube rotates when sessions are running, static when idle.
295
+ // Static (inverse): cube static when running, rotates when idle.
296
+ // The context menu shows the *switchable* label (opposite of current mode).
297
+
298
+ var cubeMode = 'dynamic'
299
+ try {
300
+ cubeMode = localStorage.getItem('parade-cube-mode') || 'dynamic'
301
+ } catch (_) {
302
+ /* localStorage may be unavailable */
303
+ }
304
+
305
+ function updateModeLabel() {
306
+ ctxMode.textContent = cubeMode === 'dynamic' ? 'Static' : 'Dynamic'
307
+ }
308
+
309
+ ctxMode.addEventListener('click', function () {
310
+ ctxMenu.style.display = 'none'
311
+ cubeMode = cubeMode === 'dynamic' ? 'static' : 'dynamic'
312
+ try {
313
+ localStorage.setItem('parade-cube-mode', cubeMode)
314
+ } catch (_) {
315
+ /* ignore */
316
+ }
317
+ updateModeLabel()
318
+ })
319
+
320
+ updateModeLabel()
321
+
260
322
  // ---------------------------------------------------------------------------
261
323
  // WebGL: Purple 3D Cube
262
324
  // ---------------------------------------------------------------------------
@@ -265,7 +327,6 @@ var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
265
327
  var cubeProgram = null
266
328
  var cubeBuffer = null
267
329
  var cubeIndexBuffer = null
268
- var isRotating = false
269
330
 
270
331
  // Sync the canvas drawing buffer to its CSS display size × devicePixelRatio.
271
332
  // Without this, the canvas keeps its initial 240×320 buffer regardless of the
@@ -533,11 +594,14 @@ function mat4Multiply(a, b) {
533
594
 
534
595
  /**
535
596
  * Animation loop. Rotates the cube on X and Y axes (gyroscope-style)
536
- * only when runningCount > 0. Stops and holds orientation when no
537
- * sessions are running.
597
+ * based on the current cube mode:
598
+ * Dynamic: rotate when runningCount > 0
599
+ * Static: rotate when runningCount === 0 (inverse)
538
600
  */
539
601
  function animate() {
540
- if (runningCount > 0) {
602
+ var shouldRotate =
603
+ cubeMode === 'dynamic' ? runningCount > 0 : runningCount === 0
604
+ if (shouldRotate) {
541
605
  rotY += 0.025
542
606
  rotX += 0.012
543
607
  }
@@ -127,12 +127,18 @@
127
127
  font-size: 10.5px;
128
128
  line-height: 1.35;
129
129
  color: var(--text-dim);
130
- white-space: nowrap;
131
- overflow: hidden;
132
- text-overflow: ellipsis;
133
130
  padding: 2px 0 2px 9px;
134
131
  position: relative;
135
132
  font-variant-numeric: tabular-nums;
133
+ display: flex;
134
+ align-items: center;
135
+ }
136
+ .session-text {
137
+ flex: 1;
138
+ overflow: hidden;
139
+ text-overflow: ellipsis;
140
+ white-space: nowrap;
141
+ min-width: 0;
136
142
  }
137
143
  .session-row::before {
138
144
  content: '';
@@ -216,6 +222,49 @@
216
222
  background: var(--purple);
217
223
  color: #fff;
218
224
  }
225
+
226
+ /* ---- Close button (top-right corner) ---- */
227
+ #close-btn {
228
+ position: fixed;
229
+ top: 4px;
230
+ right: 6px;
231
+ width: 16px;
232
+ height: 16px;
233
+ border-radius: 50%;
234
+ background: rgba(139, 92, 246, 0.15);
235
+ color: var(--purple-soft);
236
+ font-size: 13px;
237
+ line-height: 15px;
238
+ text-align: center;
239
+ cursor: pointer;
240
+ z-index: 300;
241
+ transition: all 0.15s ease;
242
+ opacity: 0;
243
+ border: 1px solid rgba(139, 92, 246, 0.2);
244
+ }
245
+ body:hover #close-btn { opacity: 0.5; }
246
+ body #close-btn:hover {
247
+ opacity: 1;
248
+ background: rgba(139, 92, 246, 0.4);
249
+ color: #fff;
250
+ box-shadow: 0 0 8px var(--purple-glow);
251
+ transform: scale(1.15);
252
+ }
253
+
254
+ /* ---- Session jump icon ---- */
255
+ .session-jump {
256
+ display: inline-block;
257
+ margin-left: auto;
258
+ padding-left: 4px;
259
+ cursor: pointer;
260
+ opacity: 0;
261
+ color: var(--purple-soft);
262
+ font-size: 9px;
263
+ transition: opacity 0.15s;
264
+ flex-shrink: 0;
265
+ }
266
+ .session-row:hover .session-jump { opacity: 0.7; }
267
+ .session-row:hover .session-jump:hover { opacity: 1; }
219
268
  </style>
220
269
  </head>
221
270
  <body>
@@ -235,8 +284,9 @@
235
284
  <div id="more-line"></div>
236
285
  </div>
237
286
  <div id="ctx-menu">
238
- <div class="ctx-item" id="ctx-close">Close</div>
287
+ <div class="ctx-item" id="ctx-mode">Static</div>
239
288
  </div>
289
+ <div id="close-btn" title="Close parade">×</div>
240
290
  <div id="ws-status" title="WS: connecting"></div>
241
291
  <!-- Corner resize handles: invisible hit areas at each corner -->
242
292
  <div class="corner-handle corner-tl" data-corner="tl"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saluzi/saluzi-edu",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
4
4
  "description": "Saluzi CLI - interactive AI coding assistant in the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",