myrlin-workbook 0.8.6 → 0.8.10

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.8.6",
3
+ "version": "0.8.10",
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": {
@@ -373,14 +373,17 @@ class PtySessionManager {
373
373
  }
374
374
  }
375
375
 
376
- // Throttled lastActive update - fires immediately then at most once per 30s
376
+ // Throttled lastActive update - deferred via setImmediate to avoid
377
+ // blocking the PTY data path with synchronous JSON file I/O.
377
378
  if (!session._lastActiveTimer) {
378
- try {
379
- const store = getStore();
380
- if (store.getSession(sessionId)) {
381
- store.updateSession(sessionId, {});
382
- }
383
- } catch (_) {}
379
+ setImmediate(() => {
380
+ try {
381
+ const store = getStore();
382
+ if (store.getSession(sessionId)) {
383
+ store.updateSession(sessionId, {});
384
+ }
385
+ } catch (_) {}
386
+ });
384
387
  session._lastActiveTimer = setTimeout(() => {
385
388
  session._lastActiveTimer = null;
386
389
  }, 30000);
@@ -114,6 +114,8 @@ class CWMApp {
114
114
  hiddenSessions: new Set(JSON.parse(localStorage.getItem('cwm_hiddenSessions') || '[]')),
115
115
  hiddenProjectSessions: new Set(JSON.parse(localStorage.getItem('cwm_hiddenProjectSessions') || '[]')),
116
116
  hiddenProjects: new Set(JSON.parse(localStorage.getItem('cwm_hiddenProjects') || '[]')),
117
+ hiddenWorkspaces: new Set(JSON.parse(localStorage.getItem('cwm_hiddenWorkspaces') || '[]')),
118
+ hiddenGroups: new Set(JSON.parse(localStorage.getItem('cwm_hiddenGroups') || '[]')),
117
119
  projectSearchQuery: '',
118
120
  showHidden: false,
119
121
  resourceData: null,
@@ -1048,6 +1050,9 @@ class CWMApp {
1048
1050
  this.closeQuickSwitcher();
1049
1051
  } else if (!this.els.modalOverlay.hidden) {
1050
1052
  this.closeModal(null);
1053
+ } else {
1054
+ // Lowest priority: collapse any expanded terminal pane
1055
+ this._collapseAllExpandedPanes();
1051
1056
  }
1052
1057
  }
1053
1058
  });
@@ -2572,6 +2577,52 @@ class CWMApp {
2572
2577
  }
2573
2578
  }
2574
2579
 
2580
+ /**
2581
+ * Hide a workspace from the sidebar. Persisted in localStorage.
2582
+ * @param {string} workspaceId - The workspace ID to hide
2583
+ */
2584
+ hideWorkspace(workspaceId) {
2585
+ const ws = this.state.workspaces.find(w => w.id === workspaceId);
2586
+ this.state.hiddenWorkspaces.add(workspaceId);
2587
+ localStorage.setItem('cwm_hiddenWorkspaces', JSON.stringify([...this.state.hiddenWorkspaces]));
2588
+ this.renderWorkspaces();
2589
+ this.renderSessions();
2590
+ this.showToast(`Hidden "${ws ? ws.name : 'project'}" - manage in Settings > Hidden Items`, 'info');
2591
+ }
2592
+
2593
+ /**
2594
+ * Unhide a workspace, making it visible in the sidebar again.
2595
+ * @param {string} workspaceId - The workspace ID to unhide
2596
+ */
2597
+ unhideWorkspace(workspaceId) {
2598
+ this.state.hiddenWorkspaces.delete(workspaceId);
2599
+ localStorage.setItem('cwm_hiddenWorkspaces', JSON.stringify([...this.state.hiddenWorkspaces]));
2600
+ this.renderWorkspaces();
2601
+ this.renderSessions();
2602
+ }
2603
+
2604
+ /**
2605
+ * Hide a category (group) from the sidebar. Persisted in localStorage.
2606
+ * @param {string} groupId - The group ID to hide
2607
+ */
2608
+ hideGroup(groupId) {
2609
+ const group = (this.state.groups || []).find(g => g.id === groupId);
2610
+ this.state.hiddenGroups.add(groupId);
2611
+ localStorage.setItem('cwm_hiddenGroups', JSON.stringify([...this.state.hiddenGroups]));
2612
+ this.renderWorkspaces();
2613
+ this.showToast(`Hidden category "${group ? group.name : ''}" - manage in Settings > Hidden Items`, 'info');
2614
+ }
2615
+
2616
+ /**
2617
+ * Unhide a category (group), making it visible in the sidebar again.
2618
+ * @param {string} groupId - The group ID to unhide
2619
+ */
2620
+ unhideGroup(groupId) {
2621
+ this.state.hiddenGroups.delete(groupId);
2622
+ localStorage.setItem('cwm_hiddenGroups', JSON.stringify([...this.state.hiddenGroups]));
2623
+ this.renderWorkspaces();
2624
+ }
2625
+
2575
2626
  toggleShowHidden() {
2576
2627
  this.state.showHidden = !this.state.showHidden;
2577
2628
  if (this.els.toggleHiddenBtn) this.els.toggleHiddenBtn.classList.toggle('active', this.state.showHidden);
@@ -2581,6 +2632,97 @@ class CWMApp {
2581
2632
  this.renderProjects();
2582
2633
  }
2583
2634
 
2635
+ /**
2636
+ * Build a flat list of all currently hidden items for the settings panel.
2637
+ * Returns array of { type, name, id } objects.
2638
+ * @returns {Array<{type: string, name: string, id: string}>}
2639
+ */
2640
+ _getHiddenItemsList() {
2641
+ const items = [];
2642
+
2643
+ // Hidden categories (groups)
2644
+ for (const groupId of this.state.hiddenGroups) {
2645
+ const group = (this.state.groups || []).find(g => g.id === groupId);
2646
+ items.push({ type: 'category', name: group ? group.name : groupId, id: groupId });
2647
+ }
2648
+
2649
+ // Hidden workspaces (projects)
2650
+ for (const wsId of this.state.hiddenWorkspaces) {
2651
+ const ws = this.state.workspaces.find(w => w.id === wsId);
2652
+ items.push({ type: 'project', name: ws ? ws.name : wsId, id: wsId });
2653
+ }
2654
+
2655
+ // Hidden sessions
2656
+ for (const sessionId of this.state.hiddenSessions) {
2657
+ const session = (this.state.allSessions || this.state.sessions).find(s => s.id === sessionId);
2658
+ items.push({ type: 'session', name: session ? session.name : sessionId, id: sessionId });
2659
+ }
2660
+
2661
+ // Hidden project folders (discovered projects)
2662
+ for (const encoded of this.state.hiddenProjects) {
2663
+ // Decode the encoded project name for display
2664
+ const decoded = decodeURIComponent(encoded).replace(/\+/g, '/');
2665
+ items.push({ type: 'folder', name: decoded, id: encoded });
2666
+ }
2667
+
2668
+ // Hidden project sessions (by name)
2669
+ for (const name of this.state.hiddenProjectSessions) {
2670
+ items.push({ type: 'file', name: name, id: name });
2671
+ }
2672
+
2673
+ return items;
2674
+ }
2675
+
2676
+ /**
2677
+ * Unhide a single item by type and ID. Called from the settings panel.
2678
+ * @param {string} type - Item type: category, project, session, folder, file
2679
+ * @param {string} id - The item identifier
2680
+ */
2681
+ _unhideItem(type, id) {
2682
+ switch (type) {
2683
+ case 'category':
2684
+ this.unhideGroup(id);
2685
+ break;
2686
+ case 'project':
2687
+ this.unhideWorkspace(id);
2688
+ break;
2689
+ case 'session':
2690
+ this.unhideSession(id);
2691
+ break;
2692
+ case 'folder':
2693
+ this.state.hiddenProjects.delete(id);
2694
+ localStorage.setItem('cwm_hiddenProjects', JSON.stringify([...this.state.hiddenProjects]));
2695
+ this.renderProjects();
2696
+ break;
2697
+ case 'file':
2698
+ this.state.hiddenProjectSessions.delete(id);
2699
+ localStorage.setItem('cwm_hiddenProjectSessions', JSON.stringify([...this.state.hiddenProjectSessions]));
2700
+ this.renderProjects();
2701
+ break;
2702
+ }
2703
+ this.showToast('Item unhidden', 'success');
2704
+ }
2705
+
2706
+ /**
2707
+ * Unhide all hidden items at once. Clears all hidden sets.
2708
+ */
2709
+ _unhideAllItems() {
2710
+ this.state.hiddenGroups.clear();
2711
+ this.state.hiddenWorkspaces.clear();
2712
+ this.state.hiddenSessions.clear();
2713
+ this.state.hiddenProjects.clear();
2714
+ this.state.hiddenProjectSessions.clear();
2715
+ localStorage.setItem('cwm_hiddenGroups', '[]');
2716
+ localStorage.setItem('cwm_hiddenWorkspaces', '[]');
2717
+ localStorage.setItem('cwm_hiddenSessions', '[]');
2718
+ localStorage.setItem('cwm_hiddenProjects', '[]');
2719
+ localStorage.setItem('cwm_hiddenProjectSessions', '[]');
2720
+ this.renderWorkspaces();
2721
+ this.renderSessions();
2722
+ this.renderProjects();
2723
+ this.showToast('All items unhidden', 'success');
2724
+ }
2725
+
2584
2726
  async startSession(id) {
2585
2727
  try {
2586
2728
  await this.api('POST', `/api/sessions/${id}/start`);
@@ -4039,8 +4181,57 @@ class CWMApp {
4039
4181
  html += `</div>`;
4040
4182
  }
4041
4183
 
4184
+ // ── Hidden Items section ──────────────────────────────
4185
+ // Build a list of all currently hidden items across all categories
4186
+ const hiddenItems = this._getHiddenItemsList();
4187
+ const hiddenMatchesFilter = !lowerFilter || 'hidden items'.includes(lowerFilter) || 'visibility'.includes(lowerFilter) || 'unhide'.includes(lowerFilter) || 'show'.includes(lowerFilter);
4188
+ if (hiddenMatchesFilter) {
4189
+ html += `<div class="settings-category">`;
4190
+ html += `<div class="settings-category-label">Hidden Items</div>`;
4191
+ if (hiddenItems.length === 0) {
4192
+ html += `<div class="settings-row"><div class="settings-row-info"><div class="settings-row-desc" style="opacity:0.5;">No hidden items. Right-click projects or categories in the sidebar to hide them.</div></div></div>`;
4193
+ } else {
4194
+ html += `<div class="settings-row" style="flex-direction:column;align-items:stretch;gap:4px;">`;
4195
+ html += `<div class="settings-row-info" style="margin-bottom:4px;"><div class="settings-row-desc">${hiddenItems.length} hidden item${hiddenItems.length !== 1 ? 's' : ''}. Click the eye icon to unhide.</div></div>`;
4196
+ for (const item of hiddenItems) {
4197
+ html += `
4198
+ <div class="settings-hidden-item" style="display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--surface0);border-radius:6px;">
4199
+ <span class="settings-hidden-item-type" style="font-size:10px;text-transform:uppercase;opacity:0.5;min-width:60px;">${this.escapeHtml(item.type)}</span>
4200
+ <span class="settings-hidden-item-name" style="flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${this.escapeHtml(item.name)}</span>
4201
+ <button class="btn btn-ghost btn-icon btn-sm settings-unhide-btn" data-unhide-type="${item.type}" data-unhide-id="${this.escapeHtml(item.id)}" title="Unhide" style="opacity:0.5;flex-shrink:0;">
4202
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 3C4.5 3 1.7 5.1 1 8c.7 2.9 3.5 5 7 5s6.3-2.1 7-5c-.7-2.9-3.5-5-7-5zm0 8a3 3 0 110-6 3 3 0 010 6zm0-5a2 2 0 100 4 2 2 0 000-4z"/></svg>
4203
+ </button>
4204
+ </div>`;
4205
+ }
4206
+ // Unhide All button
4207
+ html += `<button class="btn btn-ghost btn-sm settings-unhide-all-btn" style="margin-top:4px;font-size:11px;opacity:0.7;">Unhide All</button>`;
4208
+ html += `</div>`;
4209
+ }
4210
+ html += `</div>`;
4211
+ }
4212
+
4042
4213
  this.els.settingsBody.innerHTML = html;
4043
4214
 
4215
+ // ── Hidden items event bindings ────────────────────────
4216
+ this.els.settingsBody.querySelectorAll('.settings-unhide-btn').forEach(btn => {
4217
+ btn.addEventListener('click', () => {
4218
+ const type = btn.dataset.unhideType;
4219
+ const id = btn.dataset.unhideId;
4220
+ this._unhideItem(type, id);
4221
+ // Re-render settings to update the hidden items list
4222
+ const filter = this.els.settingsSearchInput ? this.els.settingsSearchInput.value : '';
4223
+ this.renderSettingsBody(filter);
4224
+ });
4225
+ });
4226
+ const unhideAllBtn = this.els.settingsBody.querySelector('.settings-unhide-all-btn');
4227
+ if (unhideAllBtn) {
4228
+ unhideAllBtn.addEventListener('click', () => {
4229
+ this._unhideAllItems();
4230
+ const filter = this.els.settingsSearchInput ? this.els.settingsSearchInput.value : '';
4231
+ this.renderSettingsBody(filter);
4232
+ });
4233
+ }
4234
+
4044
4235
  // ── Named tunnel controls ──────────────────────────────
4045
4236
  const ntStatus = document.getElementById('named-tunnel-status');
4046
4237
  const ntTokenInput = document.getElementById('named-tunnel-token-input');
@@ -7444,9 +7635,10 @@ class CWMApp {
7444
7635
  // Respect persisted collapse state: active workspace stays open unless user manually collapsed it
7445
7636
  const isManuallyCollapsed = this._wsCollapseState && this._wsCollapseState[ws.id] === true;
7446
7637
  const showBody = isActive && !isManuallyCollapsed;
7638
+ const isWsHidden = this.state.hiddenWorkspaces.has(ws.id);
7447
7639
 
7448
7640
  return `
7449
- <div class="workspace-accordion" data-id="${ws.id}">
7641
+ <div class="workspace-accordion${isWsHidden ? ' hidden-item' : ''}" data-id="${ws.id}">
7450
7642
  <div class="workspace-item${isActive ? ' active' : ''}" data-id="${ws.id}" draggable="true">
7451
7643
  <span class="ws-chevron${showBody ? ' open' : ''}">&#9654;</span>
7452
7644
  <div class="workspace-color-dot" style="background: ${color}"></div>
@@ -7475,10 +7667,14 @@ class CWMApp {
7475
7667
  };
7476
7668
 
7477
7669
  // Split workspaces into grouped and ungrouped
7478
- const groups = this.state.groups || [];
7670
+ const allGroups = this.state.groups || [];
7671
+ // Filter hidden groups (unless showHidden is on)
7672
+ const groups = allGroups.filter(g => this.state.showHidden || !this.state.hiddenGroups.has(g.id));
7479
7673
  const groupedIds = new Set();
7480
- groups.forEach(g => (g.workspaceIds || []).forEach(id => groupedIds.add(id)));
7481
- const ungrouped = workspaces.filter(ws => !groupedIds.has(ws.id));
7674
+ allGroups.forEach(g => (g.workspaceIds || []).forEach(id => groupedIds.add(id)));
7675
+ const ungrouped = workspaces
7676
+ .filter(ws => !groupedIds.has(ws.id))
7677
+ .filter(ws => this.state.showHidden || !this.state.hiddenWorkspaces.has(ws.id));
7482
7678
 
7483
7679
  let html = '';
7484
7680
 
@@ -7487,17 +7683,19 @@ class CWMApp {
7487
7683
  const groupColor = colorMap[group.color] || colorMap.mauve;
7488
7684
  const groupWorkspaces = (group.workspaceIds || [])
7489
7685
  .map(id => workspaces.find(ws => ws.id === id))
7490
- .filter(Boolean);
7686
+ .filter(Boolean)
7687
+ .filter(ws => this.state.showHidden || !this.state.hiddenWorkspaces.has(ws.id));
7491
7688
 
7492
7689
  // Show empty groups too so user can drag workspaces into them
7493
7690
  const groupCount = groupWorkspaces.length;
7494
7691
  const isCollapsed = this._groupCollapseState && this._groupCollapseState[group.id] === true;
7692
+ const isGroupHidden = this.state.hiddenGroups.has(group.id);
7495
7693
  const groupItemsHtml = groupCount > 0
7496
7694
  ? groupWorkspaces.map(ws => renderWorkspaceItem(ws)).join('')
7497
7695
  : '<div class="workspace-group-empty">Drag projects here</div>';
7498
7696
 
7499
7697
  html += `
7500
- <div class="workspace-group" data-group-id="${group.id}">
7698
+ <div class="workspace-group${isGroupHidden ? ' hidden-item' : ''}" data-group-id="${group.id}">
7501
7699
  <div class="workspace-group-header" data-group-id="${group.id}" style="--group-color: ${groupColor}">
7502
7700
  <span class="group-chevron${isCollapsed ? '' : ' open'}">&#9662;</span>
7503
7701
  <span class="group-color-dot" style="background: ${groupColor}"></span>
@@ -7615,6 +7813,7 @@ class CWMApp {
7615
7813
  }
7616
7814
 
7617
7815
  items.push({ type: 'sep' });
7816
+ items.push({ label: 'Hide Project', icon: '&#128065;', action: () => this.hideWorkspace(workspaceId) });
7618
7817
  items.push({ label: 'Delete Project', icon: '&#10005;', action: () => this.deleteWorkspace(workspaceId), danger: true });
7619
7818
 
7620
7819
  this._renderContextItems(ws.name, items, x, y);
@@ -7750,6 +7949,7 @@ class CWMApp {
7750
7949
  const items = [
7751
7950
  { label: 'Edit Category', icon: '&#9998;', action: () => this.renameGroup(groupId) },
7752
7951
  { type: 'sep' },
7952
+ { label: 'Hide Category', icon: '&#128065;', action: () => this.hideGroup(groupId) },
7753
7953
  { label: 'Delete Category', icon: '&#10005;', danger: true, action: () => this.deleteGroup(groupId) },
7754
7954
  ];
7755
7955
 
@@ -8585,6 +8785,26 @@ class CWMApp {
8585
8785
  closeBtn.addEventListener('click', () => this.closeTerminalPane(slotIdx));
8586
8786
  }
8587
8787
 
8788
+ // Expand button: normal → stage1 (fills grid), stage1 → stage2 (fills viewport)
8789
+ const expandBtn = pane.querySelector('.terminal-pane-expand');
8790
+ if (expandBtn) {
8791
+ expandBtn.addEventListener('click', (e) => {
8792
+ e.stopPropagation();
8793
+ this._cycleExpandPane(slotIdx);
8794
+ });
8795
+ }
8796
+
8797
+ // Collapse button: always collapses back to normal from any expanded state
8798
+ const collapseBtn = pane.querySelector('.terminal-pane-collapse');
8799
+ if (collapseBtn) {
8800
+ collapseBtn.addEventListener('click', (e) => {
8801
+ e.stopPropagation();
8802
+ this._collapseExpandPane(slotIdx);
8803
+ const tp = this.terminalPanes[slotIdx];
8804
+ if (tp) requestAnimationFrame(() => tp.safeFit());
8805
+ });
8806
+ }
8807
+
8588
8808
  // Mic (voice input) button - only show if SpeechRecognition API is available
8589
8809
  const micBtn = pane.querySelector('.terminal-pane-mic');
8590
8810
  if (micBtn && this._speechRecognitionAvailable) {
@@ -8722,6 +8942,8 @@ class CWMApp {
8722
8942
  // Show mic button if SpeechRecognition is supported
8723
8943
  const micBtn2 = paneEl.querySelector('.terminal-pane-mic');
8724
8944
  if (micBtn2 && this._speechRecognitionAvailable) micBtn2.hidden = false;
8945
+ const expandBtn2 = paneEl.querySelector('.terminal-pane-expand');
8946
+ if (expandBtn2) expandBtn2.hidden = false;
8725
8947
 
8726
8948
  // Create and mount TerminalPane
8727
8949
  const tp = new TerminalPane(containerId, sessionId, sessionName, spawnOpts);
@@ -8996,6 +9218,12 @@ class CWMApp {
8996
9218
  if (closeBtn) closeBtn.hidden = true;
8997
9219
  const uploadBtn3 = paneEl.querySelector('.terminal-pane-upload');
8998
9220
  if (uploadBtn3) uploadBtn3.hidden = true;
9221
+ // Collapse any active expansion before closing
9222
+ this._collapseExpandPane(slotIdx);
9223
+ const expandBtn3 = paneEl.querySelector('.terminal-pane-expand');
9224
+ if (expandBtn3) expandBtn3.hidden = true;
9225
+ const collapseBtn3 = paneEl.querySelector('.terminal-pane-collapse');
9226
+ if (collapseBtn3) collapseBtn3.hidden = true;
8999
9227
  // Stop any active voice recognition and hide mic button on pane close
9000
9228
  this._stopVoiceRecognition(slotIdx);
9001
9229
  const micBtn3 = paneEl.querySelector('.terminal-pane-mic');
@@ -9034,6 +9262,73 @@ class CWMApp {
9034
9262
  }
9035
9263
  }
9036
9264
 
9265
+ /**
9266
+ * Cycle expand state: normal → stage1 (fills grid) → stage2 (fills full viewport).
9267
+ * Expand button (outward arrows): visible at normal + stage1 (green at stage1), hidden at stage2.
9268
+ * Collapse button (inward arrows, red): hidden at normal, visible at stage1 + stage2.
9269
+ * @param {number} slotIdx - The terminal pane slot index
9270
+ */
9271
+ _cycleExpandPane(slotIdx) {
9272
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
9273
+ if (!paneEl) return;
9274
+ const expandBtn = paneEl.querySelector('.terminal-pane-expand');
9275
+ const collapseBtn = paneEl.querySelector('.terminal-pane-collapse');
9276
+
9277
+ if (paneEl.classList.contains('pane-expanded-stage1')) {
9278
+ // Stage1 → stage2: hide expand, keep collapse visible
9279
+ paneEl.classList.remove('pane-expanded-stage1');
9280
+ paneEl.classList.add('pane-expanded-stage2');
9281
+ if (expandBtn) {
9282
+ expandBtn.classList.remove('terminal-pane-expand-stage1');
9283
+ expandBtn.classList.add('terminal-pane-expand-stage2');
9284
+ expandBtn.title = 'Expand pane';
9285
+ }
9286
+ } else {
9287
+ // Normal → stage1: expand turns green, collapse appears
9288
+ paneEl.classList.add('pane-expanded-stage1');
9289
+ if (expandBtn) {
9290
+ expandBtn.classList.add('terminal-pane-expand-stage1');
9291
+ expandBtn.title = 'Expand to full screen';
9292
+ }
9293
+ if (collapseBtn) collapseBtn.hidden = false;
9294
+ }
9295
+
9296
+ const tp = this.terminalPanes[slotIdx];
9297
+ if (tp) requestAnimationFrame(() => tp.safeFit());
9298
+ }
9299
+
9300
+ /**
9301
+ * Collapse an expanded pane back to normal state.
9302
+ * @param {number} slotIdx - The terminal pane slot index
9303
+ */
9304
+ _collapseExpandPane(slotIdx) {
9305
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
9306
+ if (!paneEl) return;
9307
+ paneEl.classList.remove('pane-expanded-stage1', 'pane-expanded-stage2');
9308
+ const expandBtn = paneEl.querySelector('.terminal-pane-expand');
9309
+ if (expandBtn) {
9310
+ expandBtn.classList.remove('terminal-pane-expand-stage1', 'terminal-pane-expand-stage2');
9311
+ expandBtn.title = 'Expand pane';
9312
+ }
9313
+ const collapseBtn = paneEl.querySelector('.terminal-pane-collapse');
9314
+ if (collapseBtn) collapseBtn.hidden = true;
9315
+ }
9316
+
9317
+ /**
9318
+ * Collapse all expanded terminal panes back to normal state.
9319
+ * Used by the Escape key cascade as lowest-priority action.
9320
+ */
9321
+ _collapseAllExpandedPanes() {
9322
+ for (let i = 0; i < CWMApp.MAX_PANES; i++) {
9323
+ const paneEl = document.getElementById(`term-pane-${i}`);
9324
+ if (paneEl && (paneEl.classList.contains('pane-expanded-stage1') || paneEl.classList.contains('pane-expanded-stage2'))) {
9325
+ this._collapseExpandPane(i);
9326
+ const tp = this.terminalPanes[i];
9327
+ if (tp) requestAnimationFrame(() => tp.safeFit());
9328
+ }
9329
+ }
9330
+ }
9331
+
9037
9332
  /**
9038
9333
  * Toggle voice input (speech-to-text) for a terminal pane.
9039
9334
  * Uses the Web Speech API (SpeechRecognition) to capture a single utterance,
@@ -14260,7 +14555,7 @@ class CWMApp {
14260
14555
  const resp = await fetch(`/api/pty/${tp.sessionId}/upload-image`, {
14261
14556
  method: 'POST',
14262
14557
  headers: {
14263
- 'Authorization': `Bearer ${this.authToken}`,
14558
+ 'Authorization': `Bearer ${this.state.token}`,
14264
14559
  'Content-Type': file.type,
14265
14560
  'X-Filename': encodeURIComponent(file.name),
14266
14561
  },
@@ -15561,12 +15856,28 @@ class CWMApp {
15561
15856
  }
15562
15857
  }
15563
15858
 
15859
+ // If no workspace matched by directory, try matching by project folder name
15860
+ if (!workspaceId) {
15861
+ const matchByName = (this.state.workspaces || []).find(
15862
+ ws => ws.name.toLowerCase() === projectName.toLowerCase()
15863
+ );
15864
+ if (matchByName) workspaceId = matchByName.id;
15865
+ }
15866
+
15867
+ // If still no workspace found, auto-create one for this project
15868
+ if (!workspaceId) {
15869
+ const wsData = await this.api('POST', '/api/workspaces', { name: projectName });
15870
+ const newWs = wsData.workspace || wsData;
15871
+ workspaceId = newWs.id;
15872
+ await this.loadWorkspaces();
15873
+ }
15874
+
15564
15875
  const payload = {
15565
15876
  name,
15566
15877
  workingDir: dir,
15878
+ workspaceId,
15567
15879
  command: 'claude',
15568
15880
  };
15569
- if (workspaceId) payload.workspaceId = workspaceId;
15570
15881
  if (model) payload.model = model;
15571
15882
 
15572
15883
  const data = await this.api('POST', '/api/sessions', payload);
@@ -540,6 +540,16 @@
540
540
  <path d="M4 6.5a.5.5 0 011 0V7a3 3 0 006 0v-.5a.5.5 0 011 0V7a4 4 0 01-3.5 3.97V13H10a.5.5 0 010 1H6a.5.5 0 010-1h1.5v-2.03A4 4 0 014 7v-.5z"/>
541
541
  </svg>
542
542
  </button>
543
+ <button class="terminal-pane-expand btn btn-ghost btn-icon btn-sm" title="Expand pane" hidden>
544
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
545
+ <path d="M1.5 1h4a.5.5 0 010 1H2.707l3.147 3.146a.5.5 0 01-.708.708L2 2.707V5.5a.5.5 0 01-1 0v-4A.5.5 0 011.5 1zM14.5 1a.5.5 0 01.5.5v4a.5.5 0 01-1 0V2.707l-3.146 3.147a.5.5 0 01-.708-.708L13.293 2H10.5a.5.5 0 010-1h4zM1.5 15a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v2.793l3.146-3.147a.5.5 0 01.708.708L2.707 14H5.5a.5.5 0 010 1h-4zM10.354 10.146a.5.5 0 01.708 0L14 13.293V10.5a.5.5 0 011 0v4a.5.5 0 01-.5.5h-4a.5.5 0 010-1h2.793l-3.147-3.146a.5.5 0 010-.708z"/>
546
+ </svg>
547
+ </button>
548
+ <button class="terminal-pane-collapse btn btn-ghost btn-icon btn-sm" title="Collapse pane" hidden>
549
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
550
+ <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
551
+ </svg>
552
+ </button>
543
553
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
544
554
  <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>
545
555
  </button>
@@ -576,6 +586,16 @@
576
586
  <path d="M4 6.5a.5.5 0 011 0V7a3 3 0 006 0v-.5a.5.5 0 011 0V7a4 4 0 01-3.5 3.97V13H10a.5.5 0 010 1H6a.5.5 0 010-1h1.5v-2.03A4 4 0 014 7v-.5z"/>
577
587
  </svg>
578
588
  </button>
589
+ <button class="terminal-pane-expand btn btn-ghost btn-icon btn-sm" title="Expand pane" hidden>
590
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
591
+ <path d="M1.5 1h4a.5.5 0 010 1H2.707l3.147 3.146a.5.5 0 01-.708.708L2 2.707V5.5a.5.5 0 01-1 0v-4A.5.5 0 011.5 1zM14.5 1a.5.5 0 01.5.5v4a.5.5 0 01-1 0V2.707l-3.146 3.147a.5.5 0 01-.708-.708L13.293 2H10.5a.5.5 0 010-1h4zM1.5 15a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v2.793l3.146-3.147a.5.5 0 01.708.708L2.707 14H5.5a.5.5 0 010 1h-4zM10.354 10.146a.5.5 0 01.708 0L14 13.293V10.5a.5.5 0 011 0v4a.5.5 0 01-.5.5h-4a.5.5 0 010-1h2.793l-3.147-3.146a.5.5 0 010-.708z"/>
592
+ </svg>
593
+ </button>
594
+ <button class="terminal-pane-collapse btn btn-ghost btn-icon btn-sm" title="Collapse pane" hidden>
595
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
596
+ <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
597
+ </svg>
598
+ </button>
579
599
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
580
600
  <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>
581
601
  </button>
@@ -612,6 +632,16 @@
612
632
  <path d="M4 6.5a.5.5 0 011 0V7a3 3 0 006 0v-.5a.5.5 0 011 0V7a4 4 0 01-3.5 3.97V13H10a.5.5 0 010 1H6a.5.5 0 010-1h1.5v-2.03A4 4 0 014 7v-.5z"/>
613
633
  </svg>
614
634
  </button>
635
+ <button class="terminal-pane-expand btn btn-ghost btn-icon btn-sm" title="Expand pane" hidden>
636
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
637
+ <path d="M1.5 1h4a.5.5 0 010 1H2.707l3.147 3.146a.5.5 0 01-.708.708L2 2.707V5.5a.5.5 0 01-1 0v-4A.5.5 0 011.5 1zM14.5 1a.5.5 0 01.5.5v4a.5.5 0 01-1 0V2.707l-3.146 3.147a.5.5 0 01-.708-.708L13.293 2H10.5a.5.5 0 010-1h4zM1.5 15a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v2.793l3.146-3.147a.5.5 0 01.708.708L2.707 14H5.5a.5.5 0 010 1h-4zM10.354 10.146a.5.5 0 01.708 0L14 13.293V10.5a.5.5 0 011 0v4a.5.5 0 01-.5.5h-4a.5.5 0 010-1h2.793l-3.147-3.146a.5.5 0 010-.708z"/>
638
+ </svg>
639
+ </button>
640
+ <button class="terminal-pane-collapse btn btn-ghost btn-icon btn-sm" title="Collapse pane" hidden>
641
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
642
+ <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
643
+ </svg>
644
+ </button>
615
645
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
616
646
  <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>
617
647
  </button>
@@ -648,6 +678,16 @@
648
678
  <path d="M4 6.5a.5.5 0 011 0V7a3 3 0 006 0v-.5a.5.5 0 011 0V7a4 4 0 01-3.5 3.97V13H10a.5.5 0 010 1H6a.5.5 0 010-1h1.5v-2.03A4 4 0 014 7v-.5z"/>
649
679
  </svg>
650
680
  </button>
681
+ <button class="terminal-pane-expand btn btn-ghost btn-icon btn-sm" title="Expand pane" hidden>
682
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
683
+ <path d="M1.5 1h4a.5.5 0 010 1H2.707l3.147 3.146a.5.5 0 01-.708.708L2 2.707V5.5a.5.5 0 01-1 0v-4A.5.5 0 011.5 1zM14.5 1a.5.5 0 01.5.5v4a.5.5 0 01-1 0V2.707l-3.146 3.147a.5.5 0 01-.708-.708L13.293 2H10.5a.5.5 0 010-1h4zM1.5 15a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v2.793l3.146-3.147a.5.5 0 01.708.708L2.707 14H5.5a.5.5 0 010 1h-4zM10.354 10.146a.5.5 0 01.708 0L14 13.293V10.5a.5.5 0 011 0v4a.5.5 0 01-.5.5h-4a.5.5 0 010-1h2.793l-3.147-3.146a.5.5 0 010-.708z"/>
684
+ </svg>
685
+ </button>
686
+ <button class="terminal-pane-collapse btn btn-ghost btn-icon btn-sm" title="Collapse pane" hidden>
687
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
688
+ <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
689
+ </svg>
690
+ </button>
651
691
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
652
692
  <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>
653
693
  </button>
@@ -684,6 +724,16 @@
684
724
  <path d="M4 6.5a.5.5 0 011 0V7a3 3 0 006 0v-.5a.5.5 0 011 0V7a4 4 0 01-3.5 3.97V13H10a.5.5 0 010 1H6a.5.5 0 010-1h1.5v-2.03A4 4 0 014 7v-.5z"/>
685
725
  </svg>
686
726
  </button>
727
+ <button class="terminal-pane-expand btn btn-ghost btn-icon btn-sm" title="Expand pane" hidden>
728
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
729
+ <path d="M1.5 1h4a.5.5 0 010 1H2.707l3.147 3.146a.5.5 0 01-.708.708L2 2.707V5.5a.5.5 0 01-1 0v-4A.5.5 0 011.5 1zM14.5 1a.5.5 0 01.5.5v4a.5.5 0 01-1 0V2.707l-3.146 3.147a.5.5 0 01-.708-.708L13.293 2H10.5a.5.5 0 010-1h4zM1.5 15a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v2.793l3.146-3.147a.5.5 0 01.708.708L2.707 14H5.5a.5.5 0 010 1h-4zM10.354 10.146a.5.5 0 01.708 0L14 13.293V10.5a.5.5 0 011 0v4a.5.5 0 01-.5.5h-4a.5.5 0 010-1h2.793l-3.147-3.146a.5.5 0 010-.708z"/>
730
+ </svg>
731
+ </button>
732
+ <button class="terminal-pane-collapse btn btn-ghost btn-icon btn-sm" title="Collapse pane" hidden>
733
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
734
+ <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
735
+ </svg>
736
+ </button>
687
737
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
688
738
  <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>
689
739
  </button>
@@ -720,6 +770,16 @@
720
770
  <path d="M4 6.5a.5.5 0 011 0V7a3 3 0 006 0v-.5a.5.5 0 011 0V7a4 4 0 01-3.5 3.97V13H10a.5.5 0 010 1H6a.5.5 0 010-1h1.5v-2.03A4 4 0 014 7v-.5z"/>
721
771
  </svg>
722
772
  </button>
773
+ <button class="terminal-pane-expand btn btn-ghost btn-icon btn-sm" title="Expand pane" hidden>
774
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
775
+ <path d="M1.5 1h4a.5.5 0 010 1H2.707l3.147 3.146a.5.5 0 01-.708.708L2 2.707V5.5a.5.5 0 01-1 0v-4A.5.5 0 011.5 1zM14.5 1a.5.5 0 01.5.5v4a.5.5 0 01-1 0V2.707l-3.146 3.147a.5.5 0 01-.708-.708L13.293 2H10.5a.5.5 0 010-1h4zM1.5 15a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v2.793l3.146-3.147a.5.5 0 01.708.708L2.707 14H5.5a.5.5 0 010 1h-4zM10.354 10.146a.5.5 0 01.708 0L14 13.293V10.5a.5.5 0 011 0v4a.5.5 0 01-.5.5h-4a.5.5 0 010-1h2.793l-3.147-3.146a.5.5 0 010-.708z"/>
776
+ </svg>
777
+ </button>
778
+ <button class="terminal-pane-collapse btn btn-ghost btn-icon btn-sm" title="Collapse pane" hidden>
779
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
780
+ <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
781
+ </svg>
782
+ </button>
723
783
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
724
784
  <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>
725
785
  </button>
@@ -2167,6 +2167,23 @@ textarea.input {
2167
2167
  font-size: 13px;
2168
2168
  }
2169
2169
 
2170
+ /* Hidden items in settings panel */
2171
+ .settings-hidden-item:hover {
2172
+ background: var(--surface1) !important;
2173
+ }
2174
+ .settings-hidden-item .settings-unhide-btn:hover {
2175
+ opacity: 1 !important;
2176
+ color: var(--green);
2177
+ }
2178
+
2179
+ /* Sidebar: hidden items shown via "Show hidden" toggle get a dim visual */
2180
+ .workspace-group.hidden-item {
2181
+ opacity: 0.45;
2182
+ }
2183
+ .workspace-accordion.hidden-item {
2184
+ opacity: 0.45;
2185
+ }
2186
+
2170
2187
  .modal {
2171
2188
  width: 100%;
2172
2189
  max-width: 440px;
@@ -4693,6 +4710,60 @@ textarea.input {
4693
4710
  }
4694
4711
 
4695
4712
  .terminal-pane-close { width: 20px; height: 20px; padding: 0; }
4713
+
4714
+ /* Expand button - between mic and close, hidden until pane is active */
4715
+ .terminal-pane-expand {
4716
+ opacity: 0.5;
4717
+ transition: opacity 0.15s, color 0.15s;
4718
+ }
4719
+ .terminal-pane-expand:hover {
4720
+ opacity: 1;
4721
+ }
4722
+ /* Stage 1: expand button turns green (advance to stage 2) */
4723
+ .terminal-pane-expand-stage1 {
4724
+ color: var(--green);
4725
+ opacity: 1;
4726
+ }
4727
+ /* Stage 2: expand button hidden — already at max, no further expansion */
4728
+ .terminal-pane-expand-stage2 {
4729
+ display: none !important;
4730
+ }
4731
+
4732
+ /* Collapse button — inward arrows, always red, shown at stage1 + stage2 */
4733
+ .terminal-pane-collapse {
4734
+ color: var(--red);
4735
+ opacity: 0.8;
4736
+ transition: opacity 0.15s;
4737
+ }
4738
+ .terminal-pane-collapse:hover {
4739
+ opacity: 1;
4740
+ }
4741
+
4742
+ /* Stage 1: pane overlays entire terminal grid */
4743
+ .terminal-pane.pane-expanded-stage1 {
4744
+ position: absolute;
4745
+ inset: 0;
4746
+ z-index: 10;
4747
+ width: 100% !important;
4748
+ height: 100% !important;
4749
+ }
4750
+
4751
+ /* Stage 2: pane overlays entire browser viewport (covers sidebar too) */
4752
+ /* z-index 900: below all overlay panels (1000+) so modals/overlays remain accessible */
4753
+ .terminal-pane.pane-expanded-stage2 {
4754
+ position: fixed;
4755
+ inset: 0;
4756
+ z-index: 900;
4757
+ width: 100vw !important;
4758
+ height: 100vh !important;
4759
+ }
4760
+
4761
+ /* Smooth expand/collapse transitions */
4762
+ .terminal-pane.pane-expanded-stage1,
4763
+ .terminal-pane.pane-expanded-stage2 {
4764
+ transition: all 150ms ease;
4765
+ }
4766
+
4696
4767
  /* Voice input mic button - in terminal pane header, between activity and close */
4697
4768
  .terminal-pane-mic {
4698
4769
  opacity: 0.5;
@@ -4749,13 +4820,17 @@ textarea.input {
4749
4820
  .terminal-pane.image-drag-over { position: relative; }
4750
4821
  .terminal-pane.image-drag-over .terminal-container {
4751
4822
  outline: 2px dashed var(--blue); outline-offset: -2px;
4823
+ filter: blur(4px) brightness(0.7);
4824
+ transition: filter 150ms ease;
4752
4825
  }
4753
4826
  .terminal-pane.image-drag-over::after {
4754
- content: 'Drop image here'; position: absolute; top: 50%; left: 50%;
4827
+ content: 'Drop image to send to this session'; position: absolute; top: 50%; left: 50%;
4755
4828
  transform: translate(-50%, -50%); color: var(--blue);
4756
4829
  font-size: 14px; font-weight: 600; pointer-events: none; z-index: 10;
4830
+ background: var(--base); padding: 8px 16px; border-radius: 8px;
4831
+ border: 1px solid var(--blue); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
4757
4832
  }
4758
- .terminal-container { flex: 1; overflow: hidden; }
4833
+ .terminal-container { flex: 1; overflow: hidden; transition: filter 150ms ease; }
4759
4834
  .terminal-pane-empty .terminal-container {
4760
4835
  display: flex; align-items: center; justify-content: center;
4761
4836
  border: 2px dashed var(--surface1); margin: 8px;
@@ -1009,14 +1009,18 @@ class TerminalPane {
1009
1009
  // Track completion (debounced internally)
1010
1010
  this._trackActivityForCompletion();
1011
1011
 
1012
- // Debounce activity detection to at most once per 200ms
1012
+ // Debounce activity detection to at most once per 200ms.
1013
+ // Skip for unfocused terminals to avoid wasting CPU on regex
1014
+ // parsing when the result won't be visible anyway.
1013
1015
  if (!this._activityDebounceTimer) {
1014
1016
  this._activityDebounceTimer = setTimeout(() => {
1015
1017
  this._activityDebounceTimer = null;
1016
1018
  const sample = this._activitySample;
1017
1019
  this._activitySample = '';
1018
- if (sample) this._detectActivity(sample);
1019
- this._analyzeForAutoTrust(sample);
1020
+ if (sample && this._isFocused) {
1021
+ this._detectActivity(sample);
1022
+ this._analyzeForAutoTrust(sample);
1023
+ }
1020
1024
  }, 200);
1021
1025
  }
1022
1026
  }