@jhizzard/termdeck 0.14.0 → 0.15.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.14.0",
3
+ "version": "0.15.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"
@@ -2479,56 +2479,20 @@
2479
2479
  return;
2480
2480
  }
2481
2481
 
2482
- // Sprint 45 T4: registry-driven shorthand resolution. Pre-Sprint-45
2483
- // had hardcoded claude/cc/gemini/python branches here; now the type
2484
- // detection consults state.agentAdapters (loaded from
2485
- // /api/agent-adapters at init), and only the Claude `cc` alias and
2486
- // the python-server detection (no adapter exists) stay as
2487
- // special-cases below. Adapter matching uses an anchored prefix on
2488
- // the adapter's binary name (`^binary\b`, case-insensitive) which
2489
- // fits all four Sprint-45 adapters (claude / codex / gemini / grok)
2490
- // since each binary is uniquely named.
2491
- let resolvedCommand = command;
2492
- let resolvedType = 'shell';
2493
- let resolvedCwd = undefined;
2494
- let resolvedProject = project || undefined;
2495
-
2496
- // Claude `cc` alias normalization. Documented Claude shorthand —
2497
- // does not generalize to other adapters, so it stays in client UX,
2498
- // not in the server-side adapter contract.
2499
- let canonical = command;
2500
- if (/^cc\b/i.test(canonical)) {
2501
- canonical = canonical.replace(/^cc\b/i, 'claude');
2502
- }
2503
-
2504
- const adapter = (state.agentAdapters || []).find((a) =>
2505
- a && a.binary && new RegExp(`^${a.binary}\\b`, 'i').test(canonical)
2506
- );
2507
-
2508
- if (adapter) {
2509
- resolvedType = adapter.sessionType;
2510
- // Claude shorthand: `claude <project-or-cwd>` rewrites to `claude`
2511
- // and routes the trailing arg into either the project dropdown
2512
- // (if it's a known project name) or the cwd parameter. Other
2513
- // adapters' arg-parsing — codex sub-commands, gemini -p flag,
2514
- // grok --model — pass through unchanged via resolvedCommand.
2515
- if (adapter.name === 'claude') {
2516
- const argMatch = canonical.match(/^claude\s+(?:code\s+)?(.+)/i);
2517
- if (argMatch) {
2518
- const arg = argMatch[1].trim();
2519
- if (state.config.projects && state.config.projects[arg]) {
2520
- resolvedProject = arg;
2521
- } else {
2522
- resolvedCwd = arg;
2523
- }
2524
- }
2525
- resolvedCommand = adapter.binary;
2526
- }
2527
- } else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn)/i.test(canonical)) {
2528
- // python-server is a server SUBTYPE for status badges, not an
2529
- // agent adapter. No registry entry for it; detection stays here.
2530
- resolvedType = 'python-server';
2531
- }
2482
+ // Sprint 45 T4 + Sprint 46 T4: resolver extracted to
2483
+ // packages/client/public/launcher-resolver.js so the same routing
2484
+ // logic runs in the browser AND under `node --test` (see
2485
+ // tests/launcher-resolver.test.js for the contract pin). Sprint 46
2486
+ // T4 also extended the python-server preemptive regex to recognize
2487
+ // `http.server` so the python topbar quick-launch button is typed
2488
+ // correctly from the first frame.
2489
+ const { resolvedCommand, resolvedType, resolvedCwd, resolvedProject } =
2490
+ LauncherResolver.resolve(
2491
+ command,
2492
+ project,
2493
+ state.agentAdapters,
2494
+ state.config.projects
2495
+ );
2532
2496
 
2533
2497
  const session = await api('POST', '/api/sessions', {
2534
2498
  command: resolvedCommand,
@@ -4493,18 +4457,28 @@
4493
4457
  for (const sess of data.sessions) {
4494
4458
  const id = sess.sessionId || sess.session_id || 'unknown';
4495
4459
  const shortId = id.slice(0, 8);
4496
- const type = sess.type || 'shell';
4460
+ // Server (/api/transcripts/recent) returns { sessions: [{ session_id, chunks: [...] }] }
4461
+ // with chunks already grouped per session in DESC created_at order. Type/project
4462
+ // metadata isn't on the transcripts table — fall back to optional fields if any
4463
+ // future server enrichment ships them.
4464
+ const chunks = Array.isArray(sess.chunks) ? sess.chunks : [];
4465
+ const type = sess.type || (chunks.length ? 'session' : 'shell');
4497
4466
  const project = sess.project || '';
4498
- const lines = sess.lines || sess.preview || [];
4499
- const lineCount = sess.totalLines || lines.length;
4467
+ const totalChunks = sess.totalLines || chunks.length;
4468
+ // Build preview from the most-recent chunks. Server returns DESC order, so
4469
+ // the first 6 entries are the newest — reverse for natural top-down reading.
4470
+ const previewChunks = chunks.slice(0, 6).reverse();
4471
+ const previewText = sess.preview
4472
+ ? (Array.isArray(sess.preview) ? sess.preview.join('\n') : String(sess.preview))
4473
+ : previewChunks.map(c => (c && typeof c.content === 'string') ? c.content : '').join('');
4500
4474
  html += `<div class="transcript-session" data-session-id="${escapeHtml(id)}">
4501
4475
  <div class="ts-header">
4502
4476
  <span class="ts-id">${escapeHtml(shortId)}</span>
4503
4477
  <span class="ts-type">${escapeHtml(type)}</span>
4504
4478
  ${project ? `<span class="ts-project">${escapeHtml(project)}</span>` : ''}
4505
- <span class="ts-lines">${lineCount} lines</span>
4479
+ <span class="ts-lines">${totalChunks} chunks</span>
4506
4480
  </div>
4507
- <pre class="ts-preview">${escapeHtml(lines.slice(-6).join('\n'))}</pre>
4481
+ <pre class="ts-preview">${escapeHtml(previewText)}</pre>
4508
4482
  </div>`;
4509
4483
  }
4510
4484
  body.innerHTML = html;
@@ -4544,7 +4518,11 @@
4544
4518
  const id = result.sessionId || result.session_id || 'unknown';
4545
4519
  const shortId = id.slice(0, 8);
4546
4520
  const line = result.line || result.content || '';
4547
- const ts = result.timestamp ? new Date(result.timestamp).toLocaleTimeString() : '';
4521
+ // Server (/api/transcripts/search) sends `created_at`; legacy `timestamp` kept
4522
+ // as a fallback in case a future enrichment swaps the field name.
4523
+ const tsSource = result.timestamp || result.created_at || '';
4524
+ const tsDate = tsSource ? new Date(tsSource) : null;
4525
+ const ts = (tsDate && !isNaN(tsDate.getTime())) ? tsDate.toLocaleTimeString() : '';
4548
4526
  html += `<div class="transcript-result" data-session-id="${escapeHtml(id)}">
4549
4527
  <div class="tr-meta">
4550
4528
  <span class="tr-session">${escapeHtml(shortId)}</span>
@@ -552,7 +552,7 @@
552
552
  .attr('stroke-width', 1.2)
553
553
  .attr('filter', 'url(#nodeGlow)')
554
554
  .style('cursor', 'pointer')
555
- .on('mouseenter', (event, d) => onNodeHover(d.id))
555
+ .on('mouseenter', (event, d) => { onNodeHover(d.id); showNodeTooltip(event, d); })
556
556
  .on('mouseleave', () => onNodeHover(null))
557
557
  .on('click', (event, d) => onNodeClick(d))
558
558
  .call(window.d3.drag()
@@ -738,6 +738,28 @@
738
738
  tip.hidden = false;
739
739
  moveTooltip(event);
740
740
  }
741
+
742
+ // Sprint 46 T1 — node hover tooltip. Shows project (color-coded) + a short
743
+ // content snippet so the user can scan the graph without having to open the
744
+ // drawer for every node. Click still opens the full detail drawer.
745
+ function showNodeTooltip(event, node) {
746
+ const tip = $('graphTooltip');
747
+ if (!tip || !node) return;
748
+ const proj = node.project || 'global';
749
+ const text = escapeHtml(truncate(node.label || node.snippet || '(no content)', 80));
750
+ const meta = node.source_type ? `<span style="opacity:0.7">${escapeHtml(node.source_type)}</span>` : '';
751
+ tip.innerHTML = `<strong style="color:${hashHue(proj)}">${escapeHtml(proj)}</strong> ${meta} · ${text}`;
752
+ tip.hidden = false;
753
+ moveTooltip(event);
754
+ }
755
+
756
+ function escapeHtml(s) {
757
+ return String(s == null ? '' : s)
758
+ .replace(/&/g, '&amp;')
759
+ .replace(/</g, '&lt;')
760
+ .replace(/>/g, '&gt;')
761
+ .replace(/"/g, '&quot;');
762
+ }
741
763
  function moveTooltip(event) {
742
764
  const tip = $('graphTooltip');
743
765
  if (tip.hidden) return;
@@ -372,6 +372,7 @@
372
372
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
373
373
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
374
374
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
375
+ <script src="launcher-resolver.js" defer></script>
375
376
  <script src="app.js" defer></script>
376
377
  </body>
377
378
  </html>
@@ -0,0 +1,68 @@
1
+ // TermDeck launcher resolver — extracted Sprint 46 T4
2
+ //
3
+ // Pure function: given (command, project, agentAdapters, projects),
4
+ // returns the resolved spawn parameters the launcher POSTs to /api/sessions.
5
+ // Lives in its own file so the same code runs in the browser (via
6
+ // <script src="launcher-resolver.js">) AND under `node --test` (via
7
+ // `require('.../launcher-resolver')`). Sprint 46 T4 added this extraction
8
+ // to close a zero-coverage gap on the client-side routing logic — see
9
+ // tests/launcher-resolver.test.js for the contract pin.
10
+ //
11
+ // Sprint 45 T4 refactor lives here too: registry-driven shorthand
12
+ // resolution. Pre-Sprint-45 had hardcoded claude/cc/gemini/python branches;
13
+ // now the type detection consults `agentAdapters` (loaded from
14
+ // /api/agent-adapters at init), and only the Claude `cc` alias and the
15
+ // python-server detection (no adapter exists) stay as special-cases.
16
+ // Adapter matching uses an anchored prefix on the adapter's binary name
17
+ // (`^binary\b`, case-insensitive) which fits all four Sprint-45 adapters
18
+ // (claude / codex / gemini / grok) since each binary is uniquely named.
19
+
20
+ (function (root, factory) {
21
+ if (typeof module === 'object' && module.exports) {
22
+ module.exports = factory();
23
+ } else {
24
+ root.LauncherResolver = factory();
25
+ }
26
+ })(typeof self !== 'undefined' ? self : this, function () {
27
+ function resolve(command, project, agentAdapters, projects) {
28
+ let resolvedCommand = command;
29
+ let resolvedType = 'shell';
30
+ let resolvedCwd;
31
+ let resolvedProject = project || undefined;
32
+
33
+ let canonical = command;
34
+ if (/^cc\b/i.test(canonical)) {
35
+ canonical = canonical.replace(/^cc\b/i, 'claude');
36
+ }
37
+
38
+ const adapter = (agentAdapters || []).find((a) =>
39
+ a && a.binary && new RegExp(`^${a.binary}\\b`, 'i').test(canonical)
40
+ );
41
+
42
+ if (adapter) {
43
+ resolvedType = adapter.sessionType;
44
+ if (adapter.name === 'claude') {
45
+ const argMatch = canonical.match(/^claude\s+(?:code\s+)?(.+)/i);
46
+ if (argMatch) {
47
+ const arg = argMatch[1].trim();
48
+ if (projects && projects[arg]) {
49
+ resolvedProject = arg;
50
+ } else {
51
+ resolvedCwd = arg;
52
+ }
53
+ }
54
+ resolvedCommand = adapter.binary;
55
+ }
56
+ } else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn|http\.server)/i.test(canonical)) {
57
+ // Sprint 46 T4: extended `http\.server` so the python topbar
58
+ // quick-launch button is preemptively typed correctly. Without
59
+ // this, the badge flickers through `shell` for ~1s before
60
+ // session.js's runtime detection (`/Serving HTTP on/`) catches up.
61
+ resolvedType = 'python-server';
62
+ }
63
+
64
+ return { resolvedCommand, resolvedType, resolvedCwd, resolvedProject };
65
+ }
66
+
67
+ return { resolve };
68
+ });