@jhizzard/termdeck 1.1.0 → 1.2.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": "1.1.0",
3
+ "version": "1.2.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"
@@ -410,7 +410,14 @@ async function checkRumen() {
410
410
  }
411
411
  const pool = new pg.Pool({ connectionString: dbUrl, max: 1, connectionTimeoutMillis: 5000 });
412
412
  try {
413
- const r = await pool.query("SELECT to_char(NOW() - MAX(created_at), 'HH24:MI:SS') AS ago FROM rumen_jobs");
413
+ // Sprint 63 T3 §3.1 `rumen_jobs` has `started_at` (migration 001), NOT
414
+ // `created_at`. Pre-Sprint-63 this probed `created_at` and threw a
415
+ // generic WARN that doctor's same-DB check did not (Sprint 35 doctor fix
416
+ // landed RUMEN_TIME_COL.rumen_jobs='started_at' but never propagated
417
+ // here). Brad reproduced on r730 2026-05-11; doctor 23/23 GREEN while
418
+ // launcher Step 3 emitted `WARN (query failed: column "created_at"
419
+ // does not exist)`. Aligned both probes to the same column.
420
+ const r = await pool.query("SELECT to_char(NOW() - MAX(started_at), 'HH24:MI:SS') AS ago FROM rumen_jobs");
414
421
  const ago = r.rows[0] && r.rows[0].ago;
415
422
  if (ago) {
416
423
  stepLine('3/4', 'Checking Rumen', 'OK', `(last job ${ago} ago)`);
@@ -419,10 +426,20 @@ async function checkRumen() {
419
426
  stepLine('3/4', 'Checking Rumen', 'WARN', '(no jobs yet — try termdeck init --rumen)');
420
427
  return { ago: null };
421
428
  } catch (err) {
422
- if (/relation .*rumen_jobs.* does not exist/i.test(String(err.message))) {
429
+ const msg = String(err && err.message ? err.message : err);
430
+ if (/relation .*rumen_jobs.* does not exist/i.test(msg)) {
423
431
  stepLine('3/4', 'Checking Rumen', 'SKIP', '(rumen_jobs table not present — run termdeck init --rumen)');
424
432
  } else {
425
- stepLine('3/4', 'Checking Rumen', 'WARN', `(query failed: ${err.message})`);
433
+ const colMatch = msg.match(/column "([^"]+)" does not exist/i);
434
+ if (colMatch) {
435
+ // Schema drift — rumen_jobs is missing the column we queried. Naming
436
+ // the column + remediation beats a bare `query failed` that operators
437
+ // learn to filter out (Brad's r730, 2026-05-11).
438
+ stepLine('3/4', 'Checking Rumen', 'WARN',
439
+ `(rumen_jobs.${colMatch[1]} column missing — re-run \`termdeck init --rumen\` to apply migration 001)`);
440
+ } else {
441
+ stepLine('3/4', 'Checking Rumen', 'WARN', `(query failed: ${err.message})`);
442
+ }
426
443
  }
427
444
  return { ago: null };
428
445
  } finally {
@@ -129,6 +129,12 @@
129
129
  // Sprint 37 T1: orchestrator Guide right-rail. Lazy — fetches the doc
130
130
  // on first expand to keep page load light.
131
131
  setupGuideRail();
132
+
133
+ // 2026-05-08 hotfix: document-level capture-phase image-paste handler.
134
+ // Intercepts Cmd+V image data before xterm-helper-textarea consumes it
135
+ // (xterm reads only text/plain, drops images silently). See comment on
136
+ // setupGlobalImagePaste() near uploadFilesAndType() for details.
137
+ setupGlobalImagePaste();
132
138
  }
133
139
 
134
140
  // ===== Drag/drop reorder of PTY panels (Sprint 42 T4) =====
@@ -275,6 +281,57 @@
275
281
  }
276
282
  }
277
283
 
284
+ // Document-level capture-phase image paste handler.
285
+ //
286
+ // The Sprint 59 per-panel `paste` listener at line 218 is bubble-phase, but
287
+ // xterm.js@5.5.0's hidden helper-textarea has its own `paste` handler that
288
+ // reads only `clipboardData.getData('text/plain')`. Image data lives in
289
+ // `clipboardData.items` with `kind: 'file'` and never reaches xterm's
290
+ // text path — and the panel-level bubble-phase handler runs after xterm's,
291
+ // by which point xterm has already returned (silently dropping the image).
292
+ // Net: pre-fix, Cmd+V'ing a screenshot into a focused TermDeck panel did
293
+ // nothing. Joshua reported this on 2026-05-08 (post-v1.1.0 upgrade).
294
+ //
295
+ // Fix: document-level listener with `{capture: true}` runs in capture
296
+ // phase BEFORE the event reaches xterm-helper-textarea. If the event
297
+ // target is inside a `.term-panel` AND the clipboard contains image
298
+ // files, we preventDefault + stopPropagation (so xterm + the bubble-phase
299
+ // panel handler don't see it) and route through `uploadFilesAndType`.
300
+ // For text paste (no image files) we let the event continue normally.
301
+ //
302
+ // Idempotent: setupGlobalImagePaste() is called once from init().
303
+ let _globalImagePasteSetup = false;
304
+ function setupGlobalImagePaste() {
305
+ if (_globalImagePasteSetup) return;
306
+ _globalImagePasteSetup = true;
307
+ document.addEventListener('paste', (e) => {
308
+ const target = e.target;
309
+ if (!(target instanceof Element)) return;
310
+ const panel = target.closest('.term-panel');
311
+ if (!panel) return;
312
+ const items = (e.clipboardData && e.clipboardData.items) || [];
313
+ const blobs = [];
314
+ for (const item of items) {
315
+ if (item.kind === 'file' && item.type && item.type.startsWith('image/')) {
316
+ const blob = item.getAsFile();
317
+ if (blob) blobs.push(blob);
318
+ }
319
+ }
320
+ if (blobs.length === 0) return;
321
+ e.preventDefault();
322
+ e.stopPropagation();
323
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
324
+ const named = blobs.map((b, i) => {
325
+ const ext = (b.type.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '');
326
+ const name = b.name && b.name.length > 0
327
+ ? b.name
328
+ : `pasted-${ts}${blobs.length > 1 ? '-' + i : ''}.${ext}`;
329
+ return new File([b], name, { type: b.type });
330
+ });
331
+ uploadFilesAndType(panel, named);
332
+ }, { capture: true });
333
+ }
334
+
278
335
  // ===== Create Terminal Panel =====
279
336
  function createTerminalPanel(sessionData) {
280
337
  const id = sessionData.id;
@@ -50,13 +50,18 @@ function statusFor(data) {
50
50
  // resolveTranscriptPath — Sprint 50 T1.
51
51
  //
52
52
  // Gemini CLI persists chats at
53
- // ~/.gemini/tmp/<basename(cwd)>/chats/session-<ISO-ts>-<short-id>.json
54
- // (single-JSON-object shape that matches parseGeminiJson, verified
55
- // 2026-05-02 substrate probe). Pick the most recently modified file whose
56
- // mtime is at-or-after `session.meta.createdAt`. Falls back to walking
57
- // every project directory under `~/.gemini/tmp/*/chats/` if the basename
58
- // heuristic produces no candidate (e.g., Gemini renormalized the project
59
- // name to deduplicate against an existing one).
53
+ // ~/.gemini/tmp/<basename(cwd)>/chats/session-<ISO-ts>-<short-id>.{json,jsonl}
54
+ // (single-JSON-object shape that matches parseGeminiJson for the .json
55
+ // flavor, verified 2026-05-02 substrate probe; .jsonl flavor introduced
56
+ // some time between 2026-05-02 and 2026-05-08, surfaced by Sprint 63 T2
57
+ // acceptance see docs/sprint-63-wave-2/EXIT-CAPTURE-VERIFICATION.md
58
+ // Finding #2. The extension filter accepts both shapes; downstream parser
59
+ // handling of JSONL deltas is a Sprint 64 candidate). Pick the most
60
+ // recently modified file whose mtime is at-or-after
61
+ // `session.meta.createdAt`. Falls back to walking every project directory
62
+ // under `~/.gemini/tmp/*/chats/` if the basename heuristic produces no
63
+ // candidate (e.g., Gemini renormalized the project name to deduplicate
64
+ // against an existing one).
60
65
  // ──────────────────────────────────────────────────────────────────────────
61
66
 
62
67
  async function resolveTranscriptPath(session) {
@@ -83,7 +88,8 @@ async function resolveTranscriptPath(session) {
83
88
  let entries;
84
89
  try { entries = fs.readdirSync(dir); } catch (_) { return; }
85
90
  for (const name of entries) {
86
- if (!name.startsWith('session-') || !name.endsWith('.json')) continue;
91
+ if (!name.startsWith('session-')) continue;
92
+ if (!name.endsWith('.json') && !name.endsWith('.jsonl')) continue;
87
93
  const full = path.join(dir, name);
88
94
  let st;
89
95
  try { st = fs.statSync(full); } catch (_) { continue; }