myrlin-workbook 0.9.30 → 0.9.32

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": "myrlin-workbook",
3
- "version": "0.9.30",
3
+ "version": "0.9.32",
4
4
  "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -142,6 +142,7 @@ class CWMApp {
142
142
  // ─── Terminal panes ──────────────────────────────────────────
143
143
  this.terminalPanes = new Array(CWMApp.MAX_PANES).fill(null);
144
144
  this._activeTerminalSlot = null;
145
+ this._paneRefreshTimers = {};
145
146
  // Cache of TerminalPane instances per group to avoid reconnection on tab switch.
146
147
  // Key: groupId, Value: { panes: [TerminalPane|null x MAX_PANES], domFragments: [DocumentFragment|null x MAX_PANES] }
147
148
  this._groupPaneCache = {};
@@ -239,6 +240,7 @@ class CWMApp {
239
240
  logoutBtn: document.getElementById('logout-btn'),
240
241
  themeToggleBtn: document.getElementById('theme-toggle-btn'),
241
242
  themeDropdown: document.getElementById('theme-dropdown'),
243
+ vkbToggleBtn: document.getElementById('vkb-toggle-btn'),
242
244
  scaleDownBtn: document.getElementById('scale-down-btn'),
243
245
  scaleUpBtn: document.getElementById('scale-up-btn'),
244
246
 
@@ -558,6 +560,43 @@ class CWMApp {
558
560
  });
559
561
  }
560
562
 
563
+ // Virtual keyboard toggle. inputmode="none" is a no-op on devices without
564
+ // a soft keyboard, so we apply it unconditionally instead of trying to
565
+ // detect "is mobile" (which is unreliable across browsers).
566
+ this._vkbDisabled = localStorage.getItem('cwm_vkb_disabled') === '1';
567
+ this._applyVkbState();
568
+ if (this.els.vkbToggleBtn) {
569
+ this.els.vkbToggleBtn.addEventListener('click', () => {
570
+ this._vkbDisabled = !this._vkbDisabled;
571
+ localStorage.setItem('cwm_vkb_disabled', this._vkbDisabled ? '1' : '0');
572
+ this._applyVkbState();
573
+ });
574
+ // Watch for newly-created xterm helper textareas and apply state to them.
575
+ const obs = new MutationObserver((muts) => {
576
+ if (!this._vkbDisabled) return;
577
+ for (const m of muts) {
578
+ for (const n of m.addedNodes) {
579
+ if (n.nodeType !== 1) continue;
580
+ if (n.matches && n.matches('.xterm-helper-textarea')) {
581
+ n.setAttribute('inputmode', 'none');
582
+ } else if (n.querySelectorAll) {
583
+ n.querySelectorAll('.xterm-helper-textarea').forEach(t => t.setAttribute('inputmode', 'none'));
584
+ }
585
+ }
586
+ }
587
+ });
588
+ obs.observe(document.body, { childList: true, subtree: true });
589
+ }
590
+
591
+ // While any HTML5 drag is active, mark <body> so the mobile sidebar
592
+ // backdrop becomes pointer-events: none. Without this, on mobile the
593
+ // backdrop occludes terminal panes for elementFromPoint, so the
594
+ // DragDropTouch polyfill never fires dragover/drop on the pane behind it.
595
+ document.addEventListener('dragstart', () => document.body.classList.add('cwm-dragging'), true);
596
+ const clearDragging = () => document.body.classList.remove('cwm-dragging');
597
+ document.addEventListener('dragend', clearDragging, true);
598
+ document.addEventListener('drop', clearDragging, true);
599
+
561
600
  // Sidebar toggle (mobile)
562
601
  this.els.sidebarToggle.addEventListener('click', () => this.toggleSidebar());
563
602
 
@@ -3532,6 +3571,22 @@ class CWMApp {
3532
3571
  });
3533
3572
  }
3534
3573
 
3574
+ _applyVkbState() {
3575
+ const btn = this.els && this.els.vkbToggleBtn;
3576
+ const active = !!this._vkbDisabled;
3577
+ if (btn) {
3578
+ btn.classList.toggle('active', active);
3579
+ btn.setAttribute('aria-pressed', active ? 'true' : 'false');
3580
+ btn.title = active
3581
+ ? 'On-screen keyboard disabled (click to enable)'
3582
+ : 'Disable on-screen keyboard (hardware keyboard only)';
3583
+ }
3584
+ document.querySelectorAll('.xterm-helper-textarea').forEach(t => {
3585
+ if (active) t.setAttribute('inputmode', 'none');
3586
+ else t.removeAttribute('inputmode');
3587
+ });
3588
+ }
3589
+
3535
3590
  // Legacy alias for any remaining callers
3536
3591
  toggleTheme() {
3537
3592
  const current = document.documentElement.dataset.theme || 'mocha';
@@ -5017,8 +5072,8 @@ class CWMApp {
5017
5072
  * Builds a two-pane layout: file tree sidebar on left, editor pane on right.
5018
5073
  * Skips re-initialization if already rendered for the same workspace.
5019
5074
  */
5020
- async renderTasksFilesPanel() {
5021
- const panel = document.getElementById('tasks-files-panel');
5075
+ async renderTasksFilesPanel(container = null) {
5076
+ const panel = container || document.getElementById('tasks-files-panel');
5022
5077
  if (!panel) return;
5023
5078
 
5024
5079
  const ws = this.state.activeWorkspace;
@@ -5036,7 +5091,7 @@ class CWMApp {
5036
5091
  panel._wsId = ws.id;
5037
5092
 
5038
5093
  panel.replaceChildren();
5039
- const container = document.createElement('div');
5094
+ container = document.createElement('div');
5040
5095
  container.className = 'files-container';
5041
5096
 
5042
5097
  const sidebar = document.createElement('div');
@@ -5353,8 +5408,8 @@ class CWMApp {
5353
5408
  * and commit log; right pane shows diff for the selected file.
5354
5409
  * Includes 10-second auto-refresh when the tab is active.
5355
5410
  */
5356
- async renderTasksGitPanel() {
5357
- const panel = document.getElementById('tasks-git-panel');
5411
+ async renderTasksGitPanel(container = null) {
5412
+ const panel = container || document.getElementById('tasks-git-panel');
5358
5413
  if (!panel) return;
5359
5414
 
5360
5415
  const ws = this.state.activeWorkspace;
@@ -5378,7 +5433,7 @@ class CWMApp {
5378
5433
 
5379
5434
  panel.textContent = '';
5380
5435
 
5381
- const container = document.createElement('div');
5436
+ container = document.createElement('div');
5382
5437
  container.className = 'git-panel-container';
5383
5438
 
5384
5439
  const left = document.createElement('div');
@@ -5706,7 +5761,7 @@ class CWMApp {
5706
5761
  }
5707
5762
 
5708
5763
  /** Fetch tasks and render in the active layout */
5709
- async renderTasksView() {
5764
+ async renderTasksView(container = null) {
5710
5765
  // Initialize layout from localStorage (default: board)
5711
5766
  if (!this._tasksLayout) {
5712
5767
  this._tasksLayout = localStorage.getItem('cwm_tasksLayout') || 'board';
@@ -7518,15 +7573,15 @@ class CWMApp {
7518
7573
  let startX = 0;
7519
7574
  let startWidth = 0;
7520
7575
 
7521
- const onMouseMove = (e) => {
7576
+ const onMove = (clientX) => {
7522
7577
  if (!isResizing) return;
7523
- const dx = e.clientX - startX;
7578
+ const dx = clientX - startX;
7524
7579
  const newWidth = Math.max(180, Math.min(600, startWidth + dx));
7525
7580
  sidebar.style.width = newWidth + 'px';
7526
7581
  sidebar.style.transition = 'none'; // disable transition during drag
7527
7582
  };
7528
7583
 
7529
- const onMouseUp = () => {
7584
+ const onEnd = () => {
7530
7585
  if (!isResizing) return;
7531
7586
  isResizing = false;
7532
7587
  handle.classList.remove('active');
@@ -7546,24 +7601,32 @@ class CWMApp {
7546
7601
  });
7547
7602
 
7548
7603
  document.removeEventListener('mousemove', onMouseMove);
7549
- document.removeEventListener('mouseup', onMouseUp);
7604
+ document.removeEventListener('mouseup', onEnd);
7605
+ document.removeEventListener('touchmove', onTouchMove);
7606
+ document.removeEventListener('touchend', onEnd);
7607
+ document.removeEventListener('touchcancel', onEnd);
7550
7608
  };
7551
7609
 
7552
- handle.addEventListener('mousedown', (e) => {
7553
- // Don't resize if sidebar is collapsed
7554
- if (sidebar.classList.contains('collapsed')) return;
7610
+ const onMouseMove = (e) => onMove(e.clientX);
7611
+ const onTouchMove = (e) => { e.preventDefault(); onMove(e.touches[0].clientX); };
7555
7612
 
7556
- e.preventDefault();
7613
+ const startResize = (clientX) => {
7614
+ if (sidebar.classList.contains('collapsed')) return;
7557
7615
  isResizing = true;
7558
- startX = e.clientX;
7616
+ startX = clientX;
7559
7617
  startWidth = sidebar.getBoundingClientRect().width;
7560
7618
  handle.classList.add('active');
7561
7619
  document.body.style.cursor = 'col-resize';
7562
7620
  document.body.style.userSelect = 'none';
7563
-
7564
7621
  document.addEventListener('mousemove', onMouseMove);
7565
- document.addEventListener('mouseup', onMouseUp);
7566
- });
7622
+ document.addEventListener('mouseup', onEnd);
7623
+ document.addEventListener('touchmove', onTouchMove, { passive: false });
7624
+ document.addEventListener('touchend', onEnd);
7625
+ document.addEventListener('touchcancel', onEnd);
7626
+ };
7627
+
7628
+ handle.addEventListener('mousedown', (e) => { e.preventDefault(); startResize(e.clientX); });
7629
+ handle.addEventListener('touchstart', (e) => { e.preventDefault(); startResize(e.touches[0].clientX); }, { passive: false });
7567
7630
  }
7568
7631
 
7569
7632
  initSidebarSectionResize() {
@@ -9937,9 +10000,11 @@ class CWMApp {
9937
10000
  pane.classList.remove('drag-over');
9938
10001
  console.log('[DnD] Drop on pane', slotIdx, 'types:', Array.from(e.dataTransfer.types));
9939
10002
 
9940
- // Terminal pane swap/reposition - drag a pane header onto another pane
10003
+ // Terminal pane swap/reposition - drag a pane header onto another pane.
10004
+ // Use truthy check: native DataTransfer.getData returns '' for missing
10005
+ // keys, but the touch polyfill returns undefined — both must skip.
9941
10006
  const swapSource = e.dataTransfer.getData('cwm/terminal-swap');
9942
- if (swapSource !== '') {
10007
+ if (swapSource) {
9943
10008
  const srcSlot = parseInt(swapSource, 10);
9944
10009
  if (srcSlot !== slotIdx) {
9945
10010
  this.swapTerminalPanes(srcSlot, slotIdx);
@@ -10150,7 +10215,7 @@ class CWMApp {
10150
10215
  // Right-click context menu on terminal pane
10151
10216
  pane.addEventListener('contextmenu', (e) => {
10152
10217
  const tp = this.terminalPanes[slotIdx];
10153
- if (!tp) return; // empty pane - let default menu show
10218
+ if (!tp) { e.preventDefault(); e.stopPropagation(); this._showEmptyPaneContextMenu(slotIdx, e.clientX, e.clientY); return; }
10154
10219
  e.preventDefault();
10155
10220
  e.stopPropagation();
10156
10221
  this.showTerminalContextMenu(slotIdx, e.clientX, e.clientY);
@@ -10305,6 +10370,128 @@ class CWMApp {
10305
10370
  this._refreshPanePin(slotIdx);
10306
10371
  }
10307
10372
 
10373
+ /**
10374
+ * Replace the terminal in a pane with a structured view (tasks, doc, etc.).
10375
+ * The terminal is hidden but not disposed; restoreTerminalInPane() brings it back.
10376
+ * @param {number} slotIdx - The pane slot index
10377
+ * @param {string} viewType - One of: 'tasks-git', 'tasks-td', 'tasks-worktree', 'tasks-files', 'doc'
10378
+ * @param {Object} [viewData={}] - Optional view-specific data (e.g. { docId } for doc views)
10379
+ */
10380
+ async openViewInPane(slotIdx, viewType, viewData = {}) {
10381
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
10382
+ if (!paneEl) return;
10383
+ const termContainer = document.getElementById(`term-container-${slotIdx}`);
10384
+ const viewContainer = document.getElementById(`pane-view-${slotIdx}`);
10385
+ if (!termContainer || !viewContainer) return;
10386
+
10387
+ termContainer.hidden = true;
10388
+ viewContainer.hidden = false;
10389
+ viewContainer.replaceChildren();
10390
+
10391
+ const labels = {
10392
+ 'tasks-git': 'Git',
10393
+ 'tasks-td': 'Tasks',
10394
+ 'tasks-worktree': 'Worktree',
10395
+ 'tasks-files': 'Files',
10396
+ 'doc': 'Doc'
10397
+ };
10398
+ const badge = paneEl.querySelector('.pane-view-badge');
10399
+ const backBtn = paneEl.querySelector('.pane-view-back');
10400
+ if (badge) { badge.textContent = labels[viewType] || viewType; badge.hidden = false; }
10401
+ if (backBtn) backBtn.hidden = false;
10402
+
10403
+ paneEl.dataset.viewType = viewType;
10404
+ paneEl.dataset.viewData = JSON.stringify(viewData);
10405
+
10406
+ await this._renderPaneView(slotIdx, viewType, viewData, viewContainer);
10407
+ this.saveTerminalLayout();
10408
+ }
10409
+
10410
+ /**
10411
+ * Restore a pane from a structured view back to its terminal.
10412
+ * Clears the view container, stops any refresh timers, and refits the terminal.
10413
+ * @param {number} slotIdx - The pane slot index
10414
+ */
10415
+ restoreTerminalInPane(slotIdx) {
10416
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
10417
+ if (!paneEl) return;
10418
+ const termContainer = document.getElementById(`term-container-${slotIdx}`);
10419
+ const viewContainer = document.getElementById(`pane-view-${slotIdx}`);
10420
+
10421
+ if (viewContainer) { viewContainer.hidden = true; viewContainer.replaceChildren(); }
10422
+ if (termContainer) termContainer.hidden = false;
10423
+
10424
+ const badge = paneEl.querySelector('.pane-view-badge');
10425
+ const backBtn = paneEl.querySelector('.pane-view-back');
10426
+ if (badge) badge.hidden = true;
10427
+ if (backBtn) backBtn.hidden = true;
10428
+
10429
+ delete paneEl.dataset.viewType;
10430
+ delete paneEl.dataset.viewData;
10431
+
10432
+ if (this._paneRefreshTimers[slotIdx]) {
10433
+ clearInterval(this._paneRefreshTimers[slotIdx]);
10434
+ delete this._paneRefreshTimers[slotIdx];
10435
+ }
10436
+
10437
+ const tp = this.terminalPanes[slotIdx];
10438
+ if (tp && tp.safeFit) tp.safeFit();
10439
+ this.saveTerminalLayout();
10440
+ }
10441
+
10442
+ /**
10443
+ * Render the appropriate view into the pane view container.
10444
+ * Clears any existing refresh timer for this slot before rendering.
10445
+ * Git panels auto-refresh every 10 seconds.
10446
+ * @param {number} slotIdx - The pane slot index (used for refresh timer key)
10447
+ * @param {string} viewType - The view type identifier
10448
+ * @param {Object} viewData - View-specific data
10449
+ * @param {HTMLElement} container - The container element to render into
10450
+ */
10451
+ async _renderPaneView(slotIdx, viewType, viewData, container) {
10452
+ if (this._paneRefreshTimers[slotIdx]) {
10453
+ clearInterval(this._paneRefreshTimers[slotIdx]);
10454
+ delete this._paneRefreshTimers[slotIdx];
10455
+ }
10456
+ switch (viewType) {
10457
+ case 'tasks-git':
10458
+ await this.renderTasksGitPanel(container);
10459
+ this._paneRefreshTimers[slotIdx] = setInterval(() => this.renderTasksGitPanel(container), 10000);
10460
+ break;
10461
+ case 'tasks-td':
10462
+ await this.renderTasksTdPanel(container);
10463
+ break;
10464
+ case 'tasks-worktree':
10465
+ await this.renderTasksView(container);
10466
+ break;
10467
+ case 'tasks-files':
10468
+ await this.renderTasksFilesPanel(container);
10469
+ break;
10470
+ case 'doc':
10471
+ await this._renderDocInPane(container, viewData);
10472
+ break;
10473
+ }
10474
+ }
10475
+
10476
+ /**
10477
+ * Render a workspace docs textarea into the given pane container.
10478
+ * Saves content on blur via the workspace docs API.
10479
+ * @param {HTMLElement} container - The container element to render into
10480
+ * @param {Object} viewData - Optional view data (currently unused for doc type)
10481
+ */
10482
+ async _renderDocInPane(container, viewData) {
10483
+ const ws = this.state.activeWorkspace;
10484
+ if (!ws) return;
10485
+ const docs = await this.api('GET', `/api/workspaces/${ws.id}/docs`);
10486
+ const textarea = document.createElement('textarea');
10487
+ textarea.style.cssText = 'width:100%;height:100%;resize:none;background:var(--base);color:var(--text);border:none;padding:12px;font-family:var(--mono);flex:1;min-height:0;';
10488
+ textarea.value = docs.raw || '';
10489
+ textarea.addEventListener('blur', async () => {
10490
+ await this.api('POST', `/api/workspaces/${ws.id}/docs`, { content: textarea.value });
10491
+ });
10492
+ container.appendChild(textarea);
10493
+ }
10494
+
10308
10495
  /**
10309
10496
  * Update the activity indicator on a terminal pane header.
10310
10497
  * Called when 'terminal-activity' events fire from TerminalPane.
@@ -10368,6 +10555,22 @@ class CWMApp {
10368
10555
  });
10369
10556
  }
10370
10557
 
10558
+ /**
10559
+ * Context menu shown when right-clicking an empty (no terminal) pane slot.
10560
+ * Offers quick access to all pane view types.
10561
+ */
10562
+ _showEmptyPaneContextMenu(slotIdx, x, y) {
10563
+ const items = [
10564
+ { label: 'Worktree Tasks', action: () => this.openViewInPane(slotIdx, 'tasks-worktree') },
10565
+ { label: 'td Issues', action: () => this.openViewInPane(slotIdx, 'tasks-td') },
10566
+ { label: 'Git Status', action: () => this.openViewInPane(slotIdx, 'tasks-git') },
10567
+ { label: 'Files', action: () => this.openViewInPane(slotIdx, 'tasks-files') },
10568
+ { type: 'sep' },
10569
+ { label: 'Workspace Doc', action: () => this.openViewInPane(slotIdx, 'doc') },
10570
+ ];
10571
+ this._renderContextItems('Open View', items, x, y);
10572
+ }
10573
+
10371
10574
  showTerminalContextMenu(slotIdx, x, y) {
10372
10575
  const tp = this.terminalPanes[slotIdx];
10373
10576
  if (!tp) return;
@@ -10550,10 +10753,28 @@ class CWMApp {
10550
10753
  },
10551
10754
  });
10552
10755
 
10756
+ // ── Switch to view ────────────────────────────────────────
10757
+ items.push({ type: 'sep' });
10758
+ items.push({
10759
+ label: 'Switch to view',
10760
+ submenu: [
10761
+ { label: 'Worktree Tasks', action: () => this.openViewInPane(slotIdx, 'tasks-worktree') },
10762
+ { label: 'td Issues', action: () => this.openViewInPane(slotIdx, 'tasks-td') },
10763
+ { label: 'Git Status', action: () => this.openViewInPane(slotIdx, 'tasks-git') },
10764
+ { label: 'Files', action: () => this.openViewInPane(slotIdx, 'tasks-files') },
10765
+ { label: 'Workspace Doc', action: () => this.openViewInPane(slotIdx, 'doc') },
10766
+ ],
10767
+ });
10768
+
10553
10769
  this._renderContextItems(tp.sessionName || 'Terminal', items, x, y);
10554
10770
  }
10555
10771
 
10556
10772
  closeTerminalPane(slotIdx) {
10773
+ if (this._paneRefreshTimers[slotIdx]) {
10774
+ clearInterval(this._paneRefreshTimers[slotIdx]);
10775
+ delete this._paneRefreshTimers[slotIdx];
10776
+ }
10777
+
10557
10778
  const tp = this.terminalPanes[slotIdx];
10558
10779
  const sessionName = tp ? tp.sessionName : '';
10559
10780
 
@@ -11261,9 +11482,12 @@ class CWMApp {
11261
11482
  grid.addEventListener('touchmove', (e) => {
11262
11483
  // Only intercept when terminal is the active view on mobile
11263
11484
  if (!document.body.classList.contains('terminal-active')) return;
11264
- // Allow the touch event for xterm's internal scroll handling,
11265
- // but stop it from propagating to the page/body scroll.
11266
- e.stopPropagation();
11485
+ // Scope this to the xterm viewport only otherwise we eat touchmoves on
11486
+ // pane headers / resize handles and break things like the
11487
+ // DragDropTouch polyfill (which listens on document in bubble phase).
11488
+ // We don't stop propagation here anymore to allow index.html's hack
11489
+ // preventing the polyfill from breaking xterm's native scroll on desktop touch.
11490
+ // (The polyfill needs to be suppressed, index.html does it on document level).
11267
11491
  }, { passive: true });
11268
11492
  }
11269
11493
 
@@ -11272,7 +11496,8 @@ class CWMApp {
11272
11496
  * Only active on mobile. Scoped to terminal-grid to avoid sidebar conflicts.
11273
11497
  */
11274
11498
  initTerminalPaneSwipe() {
11275
- if (window.innerWidth > 768) return;
11499
+ // Enable touch pane swipe on any touch-capable device, not just phones.
11500
+ if (!('ontouchstart' in window) && navigator.maxTouchPoints === 0) return;
11276
11501
 
11277
11502
  const grid = this.els.terminalGrid;
11278
11503
  if (!grid) return;
@@ -11330,50 +11555,73 @@ class CWMApp {
11330
11555
  }
11331
11556
 
11332
11557
  _setupResizeDrag(handle, direction) {
11333
- handle.addEventListener('mousedown', (e) => {
11334
- e.preventDefault();
11335
- e.stopPropagation();
11336
-
11558
+ const start = (clientX, clientY, isTouch) => {
11337
11559
  const grid = this.els.terminalGrid;
11338
11560
  const gridRect = grid.getBoundingClientRect();
11339
11561
 
11340
- // Create a full-screen overlay to capture mouse events during drag
11341
- const overlay = document.createElement('div');
11342
- overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;cursor:${direction === 'col' ? 'col-resize' : 'row-resize'};`;
11343
- document.body.appendChild(overlay);
11562
+ // Mouse drags use a full-screen overlay to keep the resize cursor and
11563
+ // capture stray mouse events. Touch drags don't need it (no cursor;
11564
+ // touch tracking persists outside the handle naturally).
11565
+ let overlay = null;
11566
+ if (!isTouch) {
11567
+ overlay = document.createElement('div');
11568
+ overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;cursor:${direction === 'col' ? 'col-resize' : 'row-resize'};`;
11569
+ document.body.appendChild(overlay);
11570
+ }
11344
11571
 
11345
11572
  handle.classList.add('active');
11346
11573
 
11347
- const onMove = (e) => {
11574
+ const move = (cx, cy) => {
11348
11575
  if (direction === 'col') {
11349
- const ratio = (e.clientX - gridRect.left) / gridRect.width;
11576
+ const ratio = (cx - gridRect.left) / gridRect.width;
11350
11577
  const clamped = Math.max(0.15, Math.min(0.85, ratio));
11351
11578
  this._gridColSizes = [clamped, 1 - clamped];
11352
11579
  } else {
11353
- const ratio = (e.clientY - gridRect.top) / gridRect.height;
11580
+ const ratio = (cy - gridRect.top) / gridRect.height;
11354
11581
  const clamped = Math.max(0.15, Math.min(0.85, ratio));
11355
11582
  this._gridRowSizes = [clamped, 1 - clamped];
11356
11583
  }
11357
11584
  this._applyGridSizes();
11358
11585
  };
11359
11586
 
11360
- const onUp = () => {
11587
+ const onMouseMove = (e) => move(e.clientX, e.clientY);
11588
+ const onTouchMove = (e) => {
11589
+ e.preventDefault();
11590
+ move(e.touches[0].clientX, e.touches[0].clientY);
11591
+ };
11592
+ // Capture phase, because the terminal-grid has a bubble-phase touchmove
11593
+ // listener that calls stopPropagation() — would otherwise eat our event.
11594
+ const touchOpts = { passive: false, capture: true };
11595
+ const onEnd = () => {
11361
11596
  handle.classList.remove('active');
11362
- overlay.remove();
11363
- document.removeEventListener('mousemove', onMove);
11364
- document.removeEventListener('mouseup', onUp);
11365
- // Refit all terminals after resize completes
11366
- // safeFit() handles both the fit and sending resize to the server
11367
- this.terminalPanes.forEach(tp => {
11368
- if (tp) tp.safeFit();
11369
- });
11370
- // Persist split ratios for this tab group
11597
+ if (overlay) overlay.remove();
11598
+ document.removeEventListener('mousemove', onMouseMove);
11599
+ document.removeEventListener('mouseup', onEnd);
11600
+ document.removeEventListener('touchmove', onTouchMove, touchOpts);
11601
+ document.removeEventListener('touchend', onEnd);
11602
+ document.removeEventListener('touchcancel', onEnd);
11603
+ this.terminalPanes.forEach(tp => { if (tp) tp.safeFit(); });
11371
11604
  this.saveTerminalLayout();
11372
11605
  };
11373
11606
 
11374
- document.addEventListener('mousemove', onMove);
11375
- document.addEventListener('mouseup', onUp);
11607
+ document.addEventListener('mousemove', onMouseMove);
11608
+ document.addEventListener('mouseup', onEnd);
11609
+ document.addEventListener('touchmove', onTouchMove, touchOpts);
11610
+ document.addEventListener('touchend', onEnd);
11611
+ document.addEventListener('touchcancel', onEnd);
11612
+ };
11613
+
11614
+ handle.addEventListener('mousedown', (e) => {
11615
+ e.preventDefault();
11616
+ e.stopPropagation();
11617
+ start(e.clientX, e.clientY, false);
11376
11618
  });
11619
+ handle.addEventListener('touchstart', (e) => {
11620
+ e.preventDefault();
11621
+ e.stopPropagation();
11622
+ const t = e.touches[0];
11623
+ start(t.clientX, t.clientY, true);
11624
+ }, { passive: false });
11377
11625
  }
11378
11626
 
11379
11627
  /**
@@ -12934,6 +13182,9 @@ class CWMApp {
12934
13182
  group.panes.forEach(p => {
12935
13183
  if (p.sessionId && !this.terminalPanes[p.slot]) {
12936
13184
  this.openTerminalInPane(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
13185
+ if (p.viewType) {
13186
+ setTimeout(() => this.openViewInPane(p.slot, p.viewType, p.viewData || {}), 100);
13187
+ }
12937
13188
  }
12938
13189
  });
12939
13190
  }
@@ -13072,7 +13323,7 @@ class CWMApp {
13072
13323
  e.preventDefault();
13073
13324
  hdr.classList.remove('tab-drag-over');
13074
13325
  const swapSource = e.dataTransfer.getData('cwm/terminal-swap');
13075
- if (swapSource !== '') {
13326
+ if (swapSource) {
13076
13327
  const srcSlot = parseInt(swapSource, 10);
13077
13328
  const folderTabs = this._tabGroups.filter(g => g.folderId === folderId);
13078
13329
  if (folderTabs.length > 0 && folderTabs[0].id !== this._activeGroupId) {
@@ -13162,7 +13413,7 @@ class CWMApp {
13162
13413
 
13163
13414
  // Handle terminal pane drop - move terminal to this tab group
13164
13415
  const swapSource = e.dataTransfer.getData('cwm/terminal-swap');
13165
- if (swapSource !== '') {
13416
+ if (swapSource) {
13166
13417
  const srcSlot = parseInt(swapSource, 10);
13167
13418
  const targetGroupId = tab.dataset.groupId;
13168
13419
  if (targetGroupId !== this._activeGroupId) {
@@ -13396,11 +13647,16 @@ class CWMApp {
13396
13647
  const tp = this.terminalPanes[i];
13397
13648
  // Save live TerminalPanes for layout restore.
13398
13649
  if (tp && tp.sessionId) {
13650
+ const paneEl = document.getElementById('term-pane-' + i);
13651
+ const viewType = paneEl?.dataset?.viewType || null;
13652
+ const viewData = viewType ? JSON.parse(paneEl?.dataset?.viewData || '{}') : {};
13399
13653
  group.panes.push({
13400
13654
  slot: i,
13401
13655
  sessionId: tp.sessionId,
13402
13656
  sessionName: tp.sessionName,
13403
13657
  spawnOpts: tp.spawnOpts || {},
13658
+ viewType,
13659
+ viewData,
13404
13660
  });
13405
13661
  }
13406
13662
  }
@@ -9,6 +9,7 @@
9
9
  <link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png">
10
10
  <link rel="icon" type="image/png" sizes="192x192" href="favicon-192.png">
11
11
  <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
12
+ <link rel="manifest" href="/manifest.webmanifest">
12
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
15
  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400;1,500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
@@ -182,6 +183,13 @@
182
183
  <button class="theme-option" data-theme="gruvbox-light"><span class="theme-swatch" style="background:#fbf1c7;border:1px solid #d5c4a1"></span>Gruvbox Light</button>
183
184
  </div>
184
185
  </div>
186
+ <button class="btn btn-ghost btn-icon btn-sm vkb-toggle-btn" id="vkb-toggle-btn" title="Disable on-screen keyboard (hardware keyboard only)" aria-pressed="false">
187
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
188
+ <rect x="1.5" y="4" width="13" height="8" rx="1.5"/>
189
+ <path d="M4 7h.01M6 7h.01M8 7h.01M10 7h.01M12 7h.01M5 9.5h6"/>
190
+ <line class="vkb-slash" x1="2.5" y1="13.5" x2="13.5" y2="2.5" stroke-width="1.6"/>
191
+ </svg>
192
+ </button>
185
193
  <button class="btn btn-ghost btn-icon btn-sm" id="pair-mobile-btn" title="Pair Mobile Device">
186
194
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
187
195
  <rect x="4" y="1" width="8" height="14" rx="1.5"/>
@@ -562,8 +570,11 @@
562
570
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
563
571
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
564
572
  </button>
573
+ <span class="pane-view-badge" hidden></span>
574
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
565
575
  </div>
566
576
  <div class="terminal-container" id="term-container-0"></div>
577
+ <div class="pane-view-container" id="pane-view-0" hidden></div>
567
578
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
568
579
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 10v3.5a1 1 0 01-1 1h-11a1 1 0 01-1-1V10"/><polyline points="4.5 5.5 8 2 11.5 5.5"/><line x1="8" y1="2" x2="8" y2="10.5"/></svg>
569
580
  </button>
@@ -609,8 +620,11 @@
609
620
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
610
621
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
611
622
  </button>
623
+ <span class="pane-view-badge" hidden></span>
624
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
612
625
  </div>
613
626
  <div class="terminal-container" id="term-container-1"></div>
627
+ <div class="pane-view-container" id="pane-view-1" hidden></div>
614
628
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
615
629
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 10v3.5a1 1 0 01-1 1h-11a1 1 0 01-1-1V10"/><polyline points="4.5 5.5 8 2 11.5 5.5"/><line x1="8" y1="2" x2="8" y2="10.5"/></svg>
616
630
  </button>
@@ -656,8 +670,11 @@
656
670
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
657
671
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
658
672
  </button>
673
+ <span class="pane-view-badge" hidden></span>
674
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
659
675
  </div>
660
676
  <div class="terminal-container" id="term-container-2"></div>
677
+ <div class="pane-view-container" id="pane-view-2" hidden></div>
661
678
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
662
679
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 10v3.5a1 1 0 01-1 1h-11a1 1 0 01-1-1V10"/><polyline points="4.5 5.5 8 2 11.5 5.5"/><line x1="8" y1="2" x2="8" y2="10.5"/></svg>
663
680
  </button>
@@ -703,8 +720,11 @@
703
720
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
704
721
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
705
722
  </button>
723
+ <span class="pane-view-badge" hidden></span>
724
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
706
725
  </div>
707
726
  <div class="terminal-container" id="term-container-3"></div>
727
+ <div class="pane-view-container" id="pane-view-3" hidden></div>
708
728
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
709
729
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 10v3.5a1 1 0 01-1 1h-11a1 1 0 01-1-1V10"/><polyline points="4.5 5.5 8 2 11.5 5.5"/><line x1="8" y1="2" x2="8" y2="10.5"/></svg>
710
730
  </button>
@@ -750,8 +770,11 @@
750
770
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
751
771
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
752
772
  </button>
773
+ <span class="pane-view-badge" hidden></span>
774
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
753
775
  </div>
754
776
  <div class="terminal-container" id="term-container-4"></div>
777
+ <div class="pane-view-container" id="pane-view-4" hidden></div>
755
778
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
756
779
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 10v3.5a1 1 0 01-1 1h-11a1 1 0 01-1-1V10"/><polyline points="4.5 5.5 8 2 11.5 5.5"/><line x1="8" y1="2" x2="8" y2="10.5"/></svg>
757
780
  </button>
@@ -797,8 +820,11 @@
797
820
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
798
821
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
799
822
  </button>
823
+ <span class="pane-view-badge" hidden></span>
824
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
800
825
  </div>
801
826
  <div class="terminal-container" id="term-container-5"></div>
827
+ <div class="pane-view-container" id="pane-view-5" hidden></div>
802
828
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
803
829
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 10v3.5a1 1 0 01-1 1h-11a1 1 0 01-1-1V10"/><polyline points="4.5 5.5 8 2 11.5 5.5"/><line x1="8" y1="2" x2="8" y2="10.5"/></svg>
804
830
  </button>
@@ -1664,11 +1690,37 @@
1664
1690
 
1665
1691
  <script src="vendor/lucide.bundle.js"></script>
1666
1692
  <script src="vendor/material-icons.bundle.js"></script>
1693
+ <!-- Block the DragDropTouch polyfill from observing touch events that
1694
+ happen inside the xterm.js viewport. The polyfill always dispatches a
1695
+ synthetic mousemove on every touchmove; xterm preventDefault's mousemove,
1696
+ which makes the polyfill cancel the touchmove and kill native scrolling.
1697
+ This script registers its document-bubble listeners *before* the polyfill
1698
+ loads, so stopImmediatePropagation here suppresses the polyfill's
1699
+ same-phase listener for the xterm case only. -->
1700
+ <script>
1701
+ (function () {
1702
+ var inXterm = function (el) {
1703
+ return el && el.closest && el.closest('.xterm-viewport, .xterm-screen');
1704
+ };
1705
+ var stop = function (e) { if (inXterm(e.target)) e.stopImmediatePropagation(); };
1706
+ document.addEventListener('touchstart', stop, { passive: true });
1707
+ document.addEventListener('touchmove', stop, { passive: true });
1708
+ document.addEventListener('touchend', stop, { passive: true });
1709
+ document.addEventListener('touchcancel', stop, { passive: true });
1710
+ })();
1711
+ </script>
1712
+ <!-- Polyfills HTML5 drag-and-drop on touch devices (translates touch -> drag events) -->
1713
+ <script type="module" src="vendor/drag-drop-touch.esm.min.js?autoload"></script>
1667
1714
  <script src="vendor/qrcode.min.js"></script>
1668
1715
  <script src="vendor/xterm/xterm.min.js"></script>
1669
1716
  <script src="vendor/xterm-addon-fit/xterm-addon-fit.min.js"></script>
1670
1717
  <script src="vendor/xterm-addon-web-links/xterm-addon-web-links.min.js"></script>
1671
1718
  <script src="terminal.js"></script>
1672
1719
  <script src="app.js"></script>
1720
+ <script>
1721
+ if ('serviceWorker' in navigator) {
1722
+ window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js').catch(() => {}));
1723
+ }
1724
+ </script>
1673
1725
  </body>
1674
1726
  </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "myrlin's workbook",
3
+ "short_name": "Workbook",
4
+ "description": "Manage Claude Code sessions",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "background_color": "#1e1e2e",
9
+ "theme_color": "#1e1e2e",
10
+ "icons": [
11
+ { "src": "/logo.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
12
+ { "src": "/logo.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }
13
+ ]
14
+ }
@@ -1364,6 +1364,10 @@ textarea.input {
1364
1364
  color: var(--mauve) !important;
1365
1365
  }
1366
1366
 
1367
+ .vkb-toggle-btn .vkb-slash { display: none; }
1368
+ .vkb-toggle-btn.active { color: var(--mauve) !important; }
1369
+ .vkb-toggle-btn.active .vkb-slash { display: inline; }
1370
+
1367
1371
 
1368
1372
  /* ─── Main Content ──────────────────────────────────────────── */
1369
1373
 
@@ -3228,6 +3232,12 @@ textarea.input {
3228
3232
  display: none;
3229
3233
  }
3230
3234
 
3235
+ /* During a drag, let elementFromPoint fall through to drop targets behind
3236
+ the backdrop (otherwise the touch polyfill can't hit terminal panes). */
3237
+ body.cwm-dragging .sidebar-backdrop {
3238
+ pointer-events: none;
3239
+ }
3240
+
3231
3241
 
3232
3242
  /* ─── Workspace color picker in modal ───────────────────────── */
3233
3243
 
@@ -5111,9 +5121,40 @@ textarea.input {
5111
5121
  background: rgba(203, 166, 247, 0.05);
5112
5122
  }
5113
5123
 
5124
+ /* ─── Pane View Container, Badge, Back Button ──────────── */
5125
+ .pane-view-container {
5126
+ flex: 1;
5127
+ min-height: 0;
5128
+ overflow: auto;
5129
+ display: flex;
5130
+ flex-direction: column;
5131
+ background: var(--base);
5132
+ }
5133
+
5134
+ .pane-view-badge {
5135
+ font-size: 11px;
5136
+ padding: 2px 6px;
5137
+ background: var(--surface1);
5138
+ color: var(--subtext0);
5139
+ border-radius: 4px;
5140
+ font-weight: 600;
5141
+ margin-right: 4px;
5142
+ }
5143
+
5144
+ .pane-view-back {
5145
+ opacity: 0.6;
5146
+ }
5147
+
5148
+ .pane-view-back:hover {
5149
+ opacity: 1;
5150
+ }
5151
+
5114
5152
  /* Terminal pane drag-to-reposition */
5115
5153
  .terminal-pane-header {
5116
5154
  cursor: grab;
5155
+ /* Touch: let DragDropTouch polyfill do long-press-to-drag without the
5156
+ browser claiming the gesture for scroll/zoom on the underlying terminal. */
5157
+ touch-action: none;
5117
5158
  }
5118
5159
  .terminal-pane-empty .terminal-pane-header {
5119
5160
  cursor: default;
@@ -5232,6 +5273,13 @@ html.activity-indicators-disabled .terminal-pane-activity {
5232
5273
  z-index: 10;
5233
5274
  background: transparent;
5234
5275
  transition: background 0.15s ease;
5276
+ touch-action: none;
5277
+ }
5278
+ /* Wider invisible hit area for touch — visual stays at the handle's own width. */
5279
+ .terminal-resize-handle::before {
5280
+ content: "";
5281
+ position: absolute;
5282
+ inset: -8px 0;
5235
5283
  }
5236
5284
  .terminal-resize-col {
5237
5285
  top: 0;
@@ -5239,12 +5287,14 @@ html.activity-indicators-disabled .terminal-pane-activity {
5239
5287
  height: 100%;
5240
5288
  cursor: col-resize;
5241
5289
  }
5290
+ .terminal-resize-col::before { inset: 0 -8px; }
5242
5291
  .terminal-resize-row {
5243
5292
  left: 0;
5244
5293
  width: 100%;
5245
5294
  height: 6px;
5246
5295
  cursor: row-resize;
5247
5296
  }
5297
+ .terminal-resize-row::before { inset: -8px 0; }
5248
5298
  .terminal-resize-handle:hover {
5249
5299
  background: rgba(203, 166, 247, 0.4);
5250
5300
  }
@@ -5410,6 +5460,10 @@ html.activity-indicators-disabled .terminal-pane-activity {
5410
5460
  white-space: nowrap;
5411
5461
  transition: all var(--transition-fast);
5412
5462
  position: relative;
5463
+ /* Touch: let DragDropTouch polyfill handle long-press-to-drag without the
5464
+ browser claiming the gesture for the scrollable tab strip.
5465
+ */
5466
+ touch-action: none;
5413
5467
  }
5414
5468
 
5415
5469
  .terminal-group-tab:hover {
@@ -0,0 +1,3 @@
1
+ self.addEventListener('install', () => self.skipWaiting());
2
+ self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
3
+ self.addEventListener('fetch', () => {});
@@ -720,10 +720,9 @@ class TerminalPane {
720
720
  * Type mode: textarea is writable, keyboard appears for input
721
721
  */
722
722
  _isMobile() {
723
- // Use width-based check matching the CSS media query, NOT touch detection.
724
- // Touch-enabled desktops (Windows laptops) have 'ontouchstart' but should
725
- // NOT get mobile treatment - they have keyboards and wide screens.
726
- return window.innerWidth <= 768;
723
+ // Enable touch scroll/type modes for any device with touch capability,
724
+ // including tablets (not just narrow screens).
725
+ return ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
727
726
  }
728
727
 
729
728
  /**
@@ -874,6 +873,9 @@ class TerminalPane {
874
873
  }
875
874
 
876
875
  if (isScrolling) {
876
+ // Prevent default browser scroll (e.g. pull-to-refresh on Chrome mobile/tablet)
877
+ if (e.cancelable) e.preventDefault();
878
+
877
879
  // Scroll via xterm.js API so ydisp stays in sync (prevents snap-back on output)
878
880
  scrollByPixels(deltaY);
879
881
  // Track velocity for momentum (smoothed exponential average)
@@ -908,11 +910,12 @@ class TerminalPane {
908
910
  };
909
911
 
910
912
  // Use CAPTURE phase to intercept before xterm.js gets the events.
911
- // Non-passive so we can prevent xterm from seeing the events in scroll mode.
912
- container.addEventListener('touchstart', onTouchStart, { capture: true, passive: true });
913
- container.addEventListener('touchmove', onTouchMove, { capture: true, passive: true });
914
- container.addEventListener('touchend', onTouchEnd, { capture: true, passive: true });
915
- container.addEventListener('touchcancel', onTouchEnd, { capture: true, passive: true });
913
+ // Non-passive so we can prevent xterm from seeing the events in scroll mode
914
+ // AND prevent browser pull-to-refresh (overscroll) on mobile/tablet.
915
+ container.addEventListener('touchstart', onTouchStart, { capture: true, passive: false });
916
+ container.addEventListener('touchmove', onTouchMove, { capture: true, passive: false });
917
+ container.addEventListener('touchend', onTouchEnd, { capture: true, passive: false });
918
+ container.addEventListener('touchcancel', onTouchEnd, { capture: true, passive: false });
916
919
 
917
920
  // Store cleanup function for dispose()
918
921
  this._touchScrollCleanup = () => {
@@ -0,0 +1 @@
1
+ function a(s,t=!1){let e=s.touches[0];return{x:t?e.pageX:e.clientX,y:t?e.pageY:e.clientY}}function _(s,t,e){for(let o=0;o<e.length;o++){let n=e[o];s[n]=t[n]}}function f(s,t,e){let o=["altKey","ctrlKey","metaKey","shiftKey"],n=["pageX","pageY","clientX","clientY","screenX","screenY","offsetX","offsetY"],i=new Event(s,{bubbles:!0,cancelable:!0}),h=t.touches[0];return i.button=0,i.which=i.buttons=1,_(i,t,o),_(i,h,n),E(i,e),i}function E(s,t){let e=t.getBoundingClientRect();s.offsetX===void 0&&(s.offsetX=s.clientX-e.x,s.offsetY=s.clientY-e.y),s.layerX===void 0&&(s.layerX=s.pageX-e.left,s.layerY=s.pageY-e.top)}function c(s,t){if(D(t),s instanceof HTMLCanvasElement){let e=t;e.width=s.width,e.height=s.height,e.getContext("2d").drawImage(s,0,0)}y(s,t),t.style.pointerEvents="none";for(let e=0;e<s.children.length;e++)c(s.children[e],t.children[e])}function y(s,t){let e=getComputedStyle(s);for(let o of e)o.includes("transition")||(t.style[o]=e[o]);Object.keys(t.dataset).forEach(o=>delete t.dataset[o])}function D(s){["id","class","style","draggable"].forEach(function(t){s.removeAttribute(t)})}var r=class{_dropEffect;_effectAllowed;_data;_dragDropTouch;constructor(t){this._dropEffect="move",this._effectAllowed="all",this._data={},this._dragDropTouch=t}get dropEffect(){return this._dropEffect}set dropEffect(t){this._dropEffect=t}get effectAllowed(){return this._effectAllowed}set effectAllowed(t){this._effectAllowed=t}get types(){return Object.keys(this._data)}clearData(t){t!==null?delete this._data[t.toLowerCase()]:this._data={}}getData(t){let e=t.toLowerCase(),o=this._data[e];return e==="text"&&o==null&&(o=this._data["text/plain"]),o}setData(t,e){this._data[t.toLowerCase()]=e}setDragImage(t,e,o){this._dragDropTouch.setDragImage(t,e,o)}};var{round:m}=Math,b={allowDragScroll:!0,contextMenuDelayMS:900,dragImageOpacity:.5,dragScrollPercentage:10,dragScrollSpeed:10,dragThresholdPixels:5,forceListen:!1,isPressHoldMode:!1,pressHoldDelayMS:400,pressHoldMargin:25,pressHoldThresholdPixels:0},d=class{_dragRoot;_dropRoot;_dragSource;_lastTouch;_lastTarget;_ptDown;_isDragEnabled;_isDropZone;_dataTransfer;_img;_imgCustom;_imgOffset;_pressHoldIntervalId;configuration;constructor(t=document,e=document,o){for(this.configuration={...b,...o||{}},this._dragRoot=t,this._dropRoot=e;!this._dropRoot.elementFromPoint&&this._dropRoot.parentNode;)this._dropRoot=this._dropRoot.parentNode;this._dragSource=null,this._lastTouch=null,this._lastTarget=null,this._ptDown=null,this._isDragEnabled=!1,this._isDropZone=!1,this._dataTransfer=new r(this),this._img=null,this._imgCustom=null,this._imgOffset={x:0,y:0},this.listen()}listen(){if(navigator.maxTouchPoints===0&&!this.configuration.forceListen)return;let t={passive:!1,capture:!1};this._dragRoot.addEventListener("touchstart",this._touchstart.bind(this),t),this._dragRoot.addEventListener("touchmove",this._touchmove.bind(this),t),this._dragRoot.addEventListener("touchend",this._touchend.bind(this)),this._dragRoot.addEventListener("touchcancel",this._touchend.bind(this))}setDragImage(t,e,o){this._imgCustom=t,this._imgOffset={x:e,y:o}}_touchstart(t){if(this._shouldHandle(t)){this._reset();let e=this._closestDraggable(t.target);e&&t.target&&!this._dispatchEvent(t,"mousemove",t.target)&&!this._dispatchEvent(t,"mousedown",t.target)&&(this._dragSource=e,this._ptDown=a(t),this._lastTouch=t,setTimeout(()=>{this._dragSource===e&&this._img===null&&this._dispatchEvent(t,"contextmenu",e)&&this._reset()},this.configuration.contextMenuDelayMS),this.configuration.isPressHoldMode?this._pressHoldIntervalId=setTimeout(()=>{this._isDragEnabled=!0,this._touchmove(t)},this.configuration.pressHoldDelayMS):t.isTrusted||t.target!==this._lastTarget&&(this._lastTarget=t.target))}}_touchmove(t){if(this._shouldCancelPressHoldMove(t)){this._reset();return}if(this._shouldHandleMove(t)||this._shouldHandlePressHoldMove(t)){let e=this._getTarget(t);if(this._dispatchEvent(t,"mousemove",e)){this._lastTouch=t,t.preventDefault();return}if(this._dragSource&&!this._img&&this._shouldStartDragging(t)){if(this._dispatchEvent(this._lastTouch,"dragstart",this._dragSource)){this._dragSource=null;return}this._createImage(t),this._dispatchEvent(t,"dragenter",e)}if(this._img&&this._dragSource&&(this._lastTouch=t,t.preventDefault(),this._dispatchEvent(t,"drag",this._dragSource),e!==this._lastTarget&&(this._lastTarget&&this._dispatchEvent(this._lastTouch,"dragleave",this._lastTarget),this._dispatchEvent(t,"dragenter",e),this._lastTarget=e),this._moveImage(t),this._isDropZone=this._dispatchEvent(t,"dragover",e),this.configuration.allowDragScroll)){let o=this._getHotRegionDelta(t);globalThis.scrollBy(o.x,o.y)}}}_touchend(t){if(!(this._lastTouch&&t.target&&this._lastTarget)){this._reset();return}if(this._shouldHandle(t)){if(this._dispatchEvent(this._lastTouch,"mouseup",t.target)){t.preventDefault();return}this._img||(this._dragSource=null,this._dispatchEvent(this._lastTouch,"click",t.target)),this._destroyImage(),this._dragSource&&(t.type.indexOf("cancel")<0&&this._isDropZone&&this._dispatchEvent(this._lastTouch,"drop",this._lastTarget),this._dispatchEvent(this._lastTouch,"dragend",this._dragSource),this._reset())}}_shouldHandle(t){return t&&!t.defaultPrevented&&t.touches&&t.touches.length<2}_shouldHandleMove(t){return!this.configuration.isPressHoldMode&&this._shouldHandle(t)}_shouldHandlePressHoldMove(t){return this.configuration.isPressHoldMode&&this._isDragEnabled&&t&&t.touches&&t.touches.length}_shouldCancelPressHoldMove(t){return this.configuration.isPressHoldMode&&!this._isDragEnabled&&this._getDelta(t)>this.configuration.pressHoldMargin}_shouldStartDragging(t){let e=this._getDelta(t);return this.configuration.isPressHoldMode?e>=this.configuration.pressHoldThresholdPixels:e>this.configuration.dragThresholdPixels}_reset(){this._destroyImage(),this._dragSource=null,this._lastTouch=null,this._lastTarget=null,this._ptDown=null,this._isDragEnabled=!1,this._isDropZone=!1,this._dataTransfer=new r(this),clearTimeout(this._pressHoldIntervalId)}_getDelta(t){if(!this._ptDown)return 0;let{x:e,y:o}=this._ptDown,n=a(t);return((n.x-e)**2+(n.y-o)**2)**.5}_getHotRegionDelta(t){let{clientX:e,clientY:o}=t.touches[0],{innerWidth:n,innerHeight:i}=globalThis,{dragScrollPercentage:h,dragScrollSpeed:l}=this.configuration,u=h/100,g=1-u,v=e<n*u?-l:e>n*g?+l:0,T=o<i*u?-l:o>i*g?+l:0;return{x:v,y:T}}_getTarget(t){let e=a(t),o=this._dropRoot.elementFromPoint(e.x,e.y);for(;o&&getComputedStyle(o).pointerEvents=="none";)o=o.parentElement;return o}_createImage(t){this._img&&this._destroyImage();let e=this._imgCustom||this._dragSource;if(this._img=e.cloneNode(!0),c(e,this._img),this._img.style.top=this._img.style.left="-9999px",!this._imgCustom){let o=e.getBoundingClientRect(),n=a(t);this._imgOffset={x:n.x-o.left,y:n.y-o.top},this._img.style.opacity=`${this.configuration.dragImageOpacity}`}this._moveImage(t),document.body.appendChild(this._img)}_destroyImage(){this._img&&this._img.parentElement&&this._img.parentElement.removeChild(this._img),this._img=null,this._imgCustom=null}_moveImage(t){requestAnimationFrame(()=>{if(this._img){let e=a(t,!0),o=this._img.style;o.position="absolute",o.pointerEvents="none",o.zIndex="999999",o.left=`${m(e.x-this._imgOffset.x)}px`,o.top=`${m(e.y-this._imgOffset.y)}px`}})}_dispatchEvent(t,e,o){if(!(t&&o))return!1;let n=f(e,t,o);return n.dataTransfer=this._dataTransfer,o.dispatchEvent(n),n.defaultPrevented}_closestDraggable(t){for(let e=t;e!==null;e=e.parentElement)if(e.draggable)return e;return null}};function p(s=document,t=document,e){new d(s,t,e)}import.meta.url.includes("?autoload")?p(document,document,{forceListen:!0}):globalThis.DragDropTouch={enable:function(s=document,t=document,e){p(s,t,e)}};export{p as enableDragDropTouch};