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 +1 -1
- package/src/web/public/app.js +307 -51
- package/src/web/public/index.html +52 -0
- package/src/web/public/manifest.webmanifest +14 -0
- package/src/web/public/styles.css +54 -0
- package/src/web/public/sw.js +3 -0
- package/src/web/public/terminal.js +12 -9
- package/src/web/public/vendor/drag-drop-touch.esm.min.js +1 -0
package/package.json
CHANGED
package/src/web/public/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
7576
|
+
const onMove = (clientX) => {
|
|
7522
7577
|
if (!isResizing) return;
|
|
7523
|
-
const dx =
|
|
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
|
|
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',
|
|
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
|
-
|
|
7553
|
-
|
|
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
|
-
|
|
7613
|
+
const startResize = (clientX) => {
|
|
7614
|
+
if (sidebar.classList.contains('collapsed')) return;
|
|
7557
7615
|
isResizing = true;
|
|
7558
|
-
startX =
|
|
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',
|
|
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)
|
|
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
|
-
//
|
|
11265
|
-
//
|
|
11266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
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
|
|
11574
|
+
const move = (cx, cy) => {
|
|
11348
11575
|
if (direction === 'col') {
|
|
11349
|
-
const ratio = (
|
|
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 = (
|
|
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
|
|
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',
|
|
11364
|
-
document.removeEventListener('mouseup',
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
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',
|
|
11375
|
-
document.addEventListener('mouseup',
|
|
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 {
|
|
@@ -720,10 +720,9 @@ class TerminalPane {
|
|
|
720
720
|
* Type mode: textarea is writable, keyboard appears for input
|
|
721
721
|
*/
|
|
722
722
|
_isMobile() {
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
|
|
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
|
-
|
|
913
|
-
container.addEventListener('
|
|
914
|
-
container.addEventListener('
|
|
915
|
-
container.addEventListener('
|
|
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};
|