myrlin-workbook 0.9.21 → 0.9.23

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/logs/server.pid CHANGED
@@ -1 +1 @@
1
- 54288
1
+ 68900
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
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": {
@@ -9058,6 +9058,36 @@ class CWMApp {
9058
9058
  TERMINAL GRID VIEW
9059
9059
  ═══════════════════════════════════════════════════════════ */
9060
9060
 
9061
+ /**
9062
+ * Show a disconnected placeholder in a terminal pane slot.
9063
+ * Preserves session info in terminalPanes[] for layout saves.
9064
+ * Click reconnects via openTerminalInPane().
9065
+ */
9066
+ _showDisconnectedPlaceholder(slotIdx, sessionId, sessionName, spawnOpts) {
9067
+ // Store placeholder so saveCurrentGroupPanes preserves the mapping
9068
+ this.terminalPanes[slotIdx] = { sessionId, sessionName, spawnOpts: spawnOpts || {}, _disconnected: true };
9069
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
9070
+ if (!paneEl) return;
9071
+ paneEl.hidden = false;
9072
+ paneEl.classList.remove('terminal-pane-empty');
9073
+ const titleEl = paneEl.querySelector('.terminal-pane-title');
9074
+ if (titleEl) titleEl.textContent = sessionName || sessionId;
9075
+ const closeBtn = paneEl.querySelector('.terminal-pane-close');
9076
+ if (closeBtn) closeBtn.hidden = false;
9077
+ const container = document.getElementById(`term-container-${slotIdx}`);
9078
+ if (container) {
9079
+ container.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:8px;color:var(--overlay0);font-size:13px;cursor:pointer;" class="reconnect-prompt">
9080
+ <span style="font-size:20px;">&#x1F50C;</span>
9081
+ <span>${this.escapeHtml(sessionName || sessionId)}</span>
9082
+ <span style="font-size:11px;opacity:0.7;">Click to connect</span>
9083
+ </div>`;
9084
+ container.querySelector('.reconnect-prompt').addEventListener('click', () => {
9085
+ this.openTerminalInPane(slotIdx, sessionId, sessionName, spawnOpts);
9086
+ });
9087
+ }
9088
+ this.updateTerminalGridLayout();
9089
+ }
9090
+
9061
9091
  openTerminalInPane(slotIdx, sessionId, sessionName, spawnOpts) {
9062
9092
  // Check localStorage for a previously saved name for this session
9063
9093
  const savedTitle = this.getProjectSessionTitle(sessionId);
@@ -9065,13 +9095,19 @@ class CWMApp {
9065
9095
  sessionName = savedTitle;
9066
9096
  }
9067
9097
  console.log('[DnD] openTerminalInPane slot:', slotIdx, 'session:', sessionId, 'name:', sessionName);
9098
+ // If the target slot has a disconnected placeholder, clear it so we reuse the slot.
9099
+ // Without this, "Click to connect" opens in a different pane instead of replacing.
9100
+ if (this.terminalPanes[slotIdx] && this.terminalPanes[slotIdx]._disconnected) {
9101
+ this.terminalPanes[slotIdx] = null;
9102
+ }
9103
+
9068
9104
  // If the target slot already has an active terminal, find the next empty slot
9069
9105
  if (this.terminalPanes[slotIdx]) {
9070
9106
  const emptySlot = this.terminalPanes.findIndex(p => p === null);
9071
9107
  if (emptySlot !== -1) {
9072
9108
  slotIdx = emptySlot;
9073
9109
  } else {
9074
- // All slots full - replace the target slot
9110
+ // All slots full, replace the target slot
9075
9111
  this.terminalPanes[slotIdx].dispose();
9076
9112
  this.terminalPanes[slotIdx] = null;
9077
9113
  }
@@ -12048,10 +12084,12 @@ class CWMApp {
12048
12084
  const uploadBtn = paneEl.querySelector('.terminal-pane-upload');
12049
12085
  if (uploadBtn) uploadBtn.hidden = false;
12050
12086
  }
12051
- // Reattach xterm DOM
12087
+ // Reattach xterm DOM, or re-render placeholder for disconnected panes
12052
12088
  if (cached.domFragments[i]) {
12053
12089
  const termContainer = document.getElementById(`term-container-${i}`);
12054
12090
  if (termContainer) termContainer.appendChild(cached.domFragments[i]);
12091
+ } else if (cached.panes[i]._disconnected) {
12092
+ this._showDisconnectedPlaceholder(i, cached.panes[i].sessionId, cached.panes[i].sessionName, cached.panes[i].spawnOpts);
12055
12093
  }
12056
12094
  }
12057
12095
  }
@@ -12071,12 +12109,15 @@ class CWMApp {
12071
12109
  }
12072
12110
  });
12073
12111
  } else {
12074
- // No cache, create fresh connections (first time opening this group)
12112
+ // No cache: show disconnected placeholders instead of spawning all PTYs.
12113
+ // Each PTY spawns a Claude process (~150MB), so eagerly connecting all
12114
+ // panes across all tab groups would consume gigabytes of system memory.
12115
+ // Users click a pane to connect on demand.
12075
12116
  const group = this._tabGroups.find(g => g.id === groupId);
12076
12117
  if (group && group.panes) {
12077
12118
  group.panes.forEach(p => {
12078
12119
  if (p.sessionId && !this.terminalPanes[p.slot]) {
12079
- this.openTerminalInPane(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
12120
+ this._showDisconnectedPlaceholder(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
12080
12121
  }
12081
12122
  });
12082
12123
  }