@jhizzard/termdeck 0.17.0 → 0.18.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": "@jhizzard/termdeck",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
5
5
  "bin": {
6
6
  "termdeck": "./packages/cli/src/index.js"
@@ -4,6 +4,13 @@
4
4
  const WS_PROTOCOL = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
5
5
  const WS_BASE = `${WS_PROTOCOL}//${window.location.host}/ws`;
6
6
 
7
+ // ===== Utilities =====
8
+ function escapeHtml(str) {
9
+ const div = document.createElement('div');
10
+ div.textContent = str;
11
+ return div.innerHTML;
12
+ }
13
+
7
14
  // State
8
15
  const state = {
9
16
  sessions: new Map(), // id → { session, terminal, ws, fitAddon, el }
@@ -2654,12 +2661,6 @@
2654
2661
  }
2655
2662
  }
2656
2663
 
2657
- function escapeHtml(str) {
2658
- const div = document.createElement('div');
2659
- div.textContent = str;
2660
- return div.innerHTML;
2661
- }
2662
-
2663
2664
  function updateGlobalStats(sessions) {
2664
2665
  let active = 0, thinking = 0, idle = 0;
2665
2666
  for (const s of sessions) {
@@ -4257,12 +4258,6 @@
4257
4258
  dropdown.innerHTML = html;
4258
4259
  }
4259
4260
 
4260
- function escapeHtml(str) {
4261
- const div = document.createElement('div');
4262
- div.textContent = str;
4263
- return div.innerHTML;
4264
- }
4265
-
4266
4261
  function toggleHealthDropdown() {
4267
4262
  if (healthState.dropdownOpen) {
4268
4263
  closeHealthDropdown();
@@ -271,6 +271,41 @@
271
271
  margin-bottom: 16px;
272
272
  font-size: 13px;
273
273
  }
274
+
275
+ /* Pagination (Sprint 49 T2) */
276
+ .fb-pagination {
277
+ display: flex;
278
+ align-items: center;
279
+ justify-content: center;
280
+ gap: 12px;
281
+ padding: 16px;
282
+ margin-top: 8px;
283
+ }
284
+ .fb-pag-info {
285
+ font-size: 12px;
286
+ color: var(--tg-text-dim);
287
+ font-family: var(--tg-mono);
288
+ }
289
+ .fb-pag-btn {
290
+ background: var(--tg-bg);
291
+ color: var(--tg-text);
292
+ border: 1px solid var(--tg-border);
293
+ border-radius: var(--tg-radius-sm);
294
+ padding: 4px 10px;
295
+ font-size: 12px;
296
+ font-family: var(--tg-mono);
297
+ cursor: pointer;
298
+ transition: all 0.15s;
299
+ }
300
+ .fb-pag-btn:hover:not(:disabled) {
301
+ background: var(--tg-surface-hover);
302
+ border-color: var(--tg-border-active);
303
+ color: var(--tg-text-bright);
304
+ }
305
+ .fb-pag-btn:disabled {
306
+ opacity: 0.3;
307
+ cursor: not-allowed;
308
+ }
274
309
  </style>
275
310
  </head>
276
311
  <body class="fb-page">
@@ -13,10 +13,13 @@
13
13
  // Vanilla JS, no framework — matches the rest of public/.
14
14
 
15
15
  (() => {
16
- const API = window.location.origin;
16
+ const API = window.location.origin;
17
+ const PAGE_SIZE = 25;
17
18
 
18
- const els = {
19
- windowSel: document.getElementById('fbWindow'),
19
+ let _allEvents = [];
20
+ let _currentPage = 1;
21
+
22
+ const els = { windowSel: document.getElementById('fbWindow'),
20
23
  refreshBtn: document.getElementById('fbRefresh'),
21
24
  errorBanner: document.getElementById('fbErrorBanner'),
22
25
  content: document.getElementById('fbContent'),
@@ -154,8 +157,12 @@
154
157
  `;
155
158
  }
156
159
 
157
- function renderTable(events) {
158
- const rows = events.map((e) => {
160
+ function renderTable(events, page = 1) {
161
+ const totalPages = Math.ceil(events.length / PAGE_SIZE) || 1;
162
+ const start = (page - 1) * PAGE_SIZE;
163
+ const slice = events.slice(start, start + PAGE_SIZE);
164
+
165
+ const rows = slice.map((e) => {
159
166
  const projectCell = e.project
160
167
  ? `<span class="fb-cell-project">${escapeHtml(e.project)}</span>`
161
168
  : `<span class="fb-cell-project" style="color:var(--tg-text-dim)">—</span>`;
@@ -183,7 +190,7 @@
183
190
  `;
184
191
  }).join('');
185
192
 
186
- els.content.innerHTML = `
193
+ let html = `
187
194
  <div class="fb-table-wrap">
188
195
  <table class="fb-table">
189
196
  <thead>
@@ -200,6 +207,36 @@
200
207
  </table>
201
208
  </div>
202
209
  `;
210
+
211
+ if (events.length > PAGE_SIZE) {
212
+ html += `
213
+ <div class="fb-pagination">
214
+ <button type="button" class="fb-pag-btn" id="fbPrev" ${page <= 1 ? 'disabled' : ''}>&larr; Prev</button>
215
+ <span class="fb-pag-info">Page ${page} of ${totalPages}</span>
216
+ <button type="button" class="fb-pag-btn" id="fbNext" ${page >= totalPages ? 'disabled' : ''}>Next &rarr;</button>
217
+ </div>
218
+ `;
219
+ }
220
+
221
+ els.content.innerHTML = html;
222
+
223
+ // Wire pagination buttons
224
+ const prevBtn = document.getElementById('fbPrev');
225
+ const nextBtn = document.getElementById('fbNext');
226
+ if (prevBtn) {
227
+ prevBtn.onclick = () => {
228
+ _currentPage--;
229
+ localStorage.setItem('fbHistoryPage', String(_currentPage));
230
+ renderTable(_allEvents, _currentPage);
231
+ };
232
+ }
233
+ if (nextBtn) {
234
+ nextBtn.onclick = () => {
235
+ _currentPage++;
236
+ localStorage.setItem('fbHistoryPage', String(_currentPage));
237
+ renderTable(_allEvents, _currentPage);
238
+ };
239
+ }
203
240
  }
204
241
 
205
242
  function showError(msg) {
@@ -211,7 +248,7 @@
211
248
  els.errorBanner.textContent = '';
212
249
  }
213
250
 
214
- async function refresh() {
251
+ async function refresh(resetPage = true) {
215
252
  clearError();
216
253
  els.content.innerHTML = `<div class="fb-loading">Loading flashback history…</div>`;
217
254
 
@@ -219,7 +256,7 @@
219
256
  const since = sinceFromWindow(winKey);
220
257
  const qs = new URLSearchParams();
221
258
  if (since) qs.set('since', since);
222
- qs.set('limit', '200');
259
+ qs.set('limit', '500'); // Sprint 49: raised from 200 for better pagination scale
223
260
 
224
261
  let data;
225
262
  try {
@@ -235,24 +272,34 @@
235
272
  return;
236
273
  }
237
274
 
275
+ _allEvents = data.events || [];
238
276
  renderFunnel(data.funnel || { fires: 0, dismissed: 0, clicked_through: 0 });
239
277
 
240
- if (!Array.isArray(data.events) || data.events.length === 0) {
278
+ if (_allEvents.length === 0) {
241
279
  renderZeroState(winKey);
242
280
  return;
243
281
  }
244
282
 
245
- renderTable(data.events);
283
+ if (resetPage) {
284
+ _currentPage = 1;
285
+ localStorage.setItem('fbHistoryPage', '1');
286
+ } else {
287
+ _currentPage = parseInt(localStorage.getItem('fbHistoryPage') || '1', 10);
288
+ const maxPage = Math.ceil(_allEvents.length / PAGE_SIZE) || 1;
289
+ if (_currentPage > maxPage) _currentPage = 1;
290
+ }
291
+
292
+ renderTable(_allEvents, _currentPage);
246
293
  }
247
294
 
248
295
  // Wire controls
249
296
  els.windowSel.addEventListener('change', () => {
250
297
  writeStateToUrl();
251
- refresh();
298
+ refresh(true);
252
299
  });
253
- els.refreshBtn.addEventListener('click', () => refresh());
300
+ els.refreshBtn.addEventListener('click', () => refresh(true));
254
301
 
255
302
  // Boot
256
303
  loadStateFromUrl();
257
- refresh();
304
+ refresh(false);
258
305
  })();
@@ -40,6 +40,11 @@
40
40
  </div>
41
41
  </header>
42
42
 
43
+ <div class="graph-presets" id="graphPresets" role="toolbar" aria-label="Filter presets">
44
+ <button type="button" class="gf-preset" id="presetAll">All</button>
45
+ <button type="button" class="gf-preset" id="presetNone">None</button>
46
+ </div>
47
+
43
48
  <div class="graph-filters" id="graphFilters" role="toolbar" aria-label="Edge type filters"></div>
44
49
 
45
50
  <div class="graph-filters graph-filters-row2" id="graphControls" role="toolbar" aria-label="Graph view controls">
@@ -410,6 +410,7 @@
410
410
  const keys = Object.keys(EDGE_COLORS).filter((k) => present.has(k));
411
411
  if (keys.length === 0) {
412
412
  wrap.style.display = 'none';
413
+ updatePresetButtons();
413
414
  return;
414
415
  }
415
416
  wrap.style.display = '';
@@ -429,10 +430,27 @@
429
430
  if (state.activeKinds.has(k)) state.activeKinds.delete(k);
430
431
  else state.activeKinds.add(k);
431
432
  chip.classList.toggle('active');
433
+ updatePresetButtons();
432
434
  applyFilter();
433
435
  });
434
436
  wrap.appendChild(chip);
435
437
  }
438
+ updatePresetButtons();
439
+ }
440
+
441
+ function isAllKindsActive() {
442
+ return state.activeKinds.size === Object.keys(EDGE_COLORS).length;
443
+ }
444
+
445
+ function isNoKindsActive() {
446
+ return state.activeKinds.size === 0;
447
+ }
448
+
449
+ function updatePresetButtons() {
450
+ const allBtn = $('presetAll');
451
+ const noneBtn = $('presetNone');
452
+ if (allBtn) allBtn.disabled = isAllKindsActive();
453
+ if (noneBtn) noneBtn.disabled = isNoKindsActive();
436
454
  }
437
455
 
438
456
  function applyFilter() {
@@ -843,6 +861,25 @@
843
861
  });
844
862
  $('graphFit').addEventListener('click', () => fitToView());
845
863
 
864
+ // Sprint 49 T3 — chip filter presets (All/None). Wire once; renderFilters()
865
+ // keeps their disabled state in sync with activeKinds boundary conditions.
866
+ const presetAll = $('presetAll');
867
+ const presetNone = $('presetNone');
868
+ if (presetAll) {
869
+ presetAll.addEventListener('click', () => {
870
+ state.activeKinds = new Set(Object.keys(EDGE_COLORS));
871
+ renderFilters();
872
+ applyFilter();
873
+ });
874
+ }
875
+ if (presetNone) {
876
+ presetNone.addEventListener('click', () => {
877
+ state.activeKinds = new Set();
878
+ renderFilters();
879
+ applyFilter();
880
+ });
881
+ }
882
+
846
883
  // Sprint 43 T1 — graph view-controls. Hydrate input values from state, then
847
884
  // wire change handlers that mutate state.controls + URL + re-render from
848
885
  // the cached raw fetch so toggling is fast (no API round-trip).
@@ -58,9 +58,8 @@
58
58
  <button class="topbar-ql-btn" onclick="quickLaunch('python3 -m http.server 8080')" title="Open a Python HTTP server on :8080">python</button>
59
59
  </div>
60
60
  <div class="topbar-row-2-spacer"></div>
61
- <button id="btn-status">status</button>
62
- <button id="btn-config">config</button>
63
- <button id="btn-sprint" title="Define and kick off a 4+1 sprint">sprint</button>
61
+ <button id="btn-status" title="Global metrics: session counts, RAG mode, and memory bridge status">status</button>
62
+ <button id="btn-config" title="Configuration: project list, theme defaults, and live RAG-mode toggle">config</button> <button id="btn-sprint" title="Define and kick off a 4+1 sprint">sprint</button>
64
63
  <button id="btn-graph" title="Open the knowledge-graph view (memory_items + memory_relationships, force-directed)" onclick="window.open('/graph.html','_blank','noopener')">graph</button>
65
64
  <button id="btn-flashback-history" title="Audit dashboard: every Flashback fire, dismiss/click-through funnel" onclick="window.open('/flashback-history.html','_blank','noopener')">flashback history</button>
66
65
  <button id="btn-how" title="Walkthrough of every TermDeck feature">how this works</button>
@@ -24,6 +24,10 @@
24
24
  root.LauncherResolver = factory();
25
25
  }
26
26
  })(typeof self !== 'undefined' ? self : this, function () {
27
+ function escapeRegex(s) {
28
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ }
30
+
27
31
  function resolve(command, project, agentAdapters, projects) {
28
32
  let resolvedCommand = command;
29
33
  let resolvedType = 'shell';
@@ -36,7 +40,7 @@
36
40
  }
37
41
 
38
42
  const adapter = (agentAdapters || []).find((a) =>
39
- a && a.binary && new RegExp(`^${a.binary}\\b`, 'i').test(canonical)
43
+ a && a.binary && new RegExp(`^${escapeRegex(a.binary)}\\b`, 'i').test(canonical)
40
44
  );
41
45
 
42
46
  if (adapter) {
@@ -3567,6 +3567,37 @@
3567
3567
  background: rgba(255, 255, 255, 0.08);
3568
3568
  }
3569
3569
 
3570
+ /* Sprint 49 T3 — graph chip filter presets (Surface 2 from Sprint 46 audit).
3571
+ Subtle variant of .gf-chip: smaller, no dot/count, secondary tint,
3572
+ disabled state for boundary (all/none already active). Placed above
3573
+ the chip row. */
3574
+ .graph-presets {
3575
+ display: flex;
3576
+ gap: 6px;
3577
+ padding: 8px 16px 4px;
3578
+ background: var(--tg-surface);
3579
+ border-bottom: 1px solid var(--tg-border);
3580
+ flex-shrink: 0;
3581
+ }
3582
+ .gf-preset {
3583
+ padding: 2px 10px;
3584
+ font-size: 10px;
3585
+ font-family: var(--tg-mono);
3586
+ color: var(--tg-text-dim);
3587
+ background: var(--tg-bg);
3588
+ border: 1px solid var(--tg-border);
3589
+ border-radius: 999px;
3590
+ cursor: pointer;
3591
+ transition: all 150ms;
3592
+ opacity: 0.7;
3593
+ }
3594
+ .gf-preset:hover { opacity: 1; background: var(--tg-surface-hover); }
3595
+ .gf-preset:disabled {
3596
+ opacity: 0.35;
3597
+ cursor: not-allowed;
3598
+ background: transparent;
3599
+ }
3600
+
3570
3601
  /* Sprint 43 T1 — second toolbar row: hide-isolated / min-degree / window
3571
3602
  / layout selectors. Mirrors .graph-filters spacing but shifts the
3572
3603
  background a notch darker so the two rows are visually distinguishable