@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 +1 -1
- package/packages/client/public/app.js +7 -12
- package/packages/client/public/flashback-history.html +35 -0
- package/packages/client/public/flashback-history.js +60 -13
- package/packages/client/public/graph.html +5 -0
- package/packages/client/public/graph.js +37 -0
- package/packages/client/public/index.html +2 -3
- package/packages/client/public/launcher-resolver.js +5 -1
- package/packages/client/public/style.css +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "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
|
-
|
|
16
|
+
const API = window.location.origin;
|
|
17
|
+
const PAGE_SIZE = 25;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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' : ''}>← 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 →</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', '
|
|
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 (
|
|
278
|
+
if (_allEvents.length === 0) {
|
|
241
279
|
renderZeroState(winKey);
|
|
242
280
|
return;
|
|
243
281
|
}
|
|
244
282
|
|
|
245
|
-
|
|
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
|