myrlin-workbook 0.9.29 → 0.9.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.29",
3
+ "version": "0.9.31",
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": {
@@ -1,37 +1,77 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Postinstall script: fix node-pty spawn-helper permissions on macOS/Linux.
4
- * The prebuilt spawn-helper binary ships without execute permission (644),
5
- * causing posix_spawnp failures. This sets it to 755.
6
- * See: https://github.com/therealarthur/myrlin-workbook/issues/4
7
- */
8
- 'use strict';
9
-
10
- if (process.platform === 'win32') {
11
- process.exit(0);
12
- }
13
-
14
- const { execSync } = require('child_process');
15
- const path = require('path');
16
- const fs = require('fs');
17
-
18
- const prebuildsDir = path.join(__dirname, '..', 'node_modules', 'node-pty', 'prebuilds');
19
-
20
- if (!fs.existsSync(prebuildsDir)) {
21
- process.exit(0);
22
- }
23
-
24
- try {
25
- // Find all spawn-helper binaries across platform dirs and chmod +x them
26
- const dirs = fs.readdirSync(prebuildsDir, { withFileTypes: true })
27
- .filter(d => d.isDirectory());
28
-
29
- for (const dir of dirs) {
30
- const helper = path.join(prebuildsDir, dir.name, 'spawn-helper');
31
- if (fs.existsSync(helper)) {
32
- execSync(`chmod +x "${helper}"`, { stdio: 'ignore' });
33
- }
34
- }
35
- } catch (_) {
36
- // Non-fatal: if chmod fails, user can still manually fix
37
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall script: fix node-pty spawn-helper permissions on macOS/Linux.
4
+ *
5
+ * The prebuilt spawn-helper binary in node-pty's package ships without
6
+ * execute permission (mode 644 instead of 755), causing posix_spawnp to
7
+ * fail with "posix_spawnp failed" on first PTY spawn.
8
+ *
9
+ * This is a known node-pty packaging issue. We work around it here by
10
+ * locating node-pty (wherever npm/npx hoisted it), finding all prebuilt
11
+ * spawn-helper binaries across platform dirs, and setting them to 755.
12
+ *
13
+ * See: https://github.com/therealarthur/myrlin-workbook/issues/4
14
+ */
15
+ 'use strict';
16
+
17
+ if (process.platform === 'win32') {
18
+ process.exit(0);
19
+ }
20
+
21
+ const path = require('path');
22
+ const fs = require('fs');
23
+
24
+ /**
25
+ * Locate node-pty's package directory using require.resolve.
26
+ * Works regardless of where npm/npx/yarn placed it (hoisted, nested,
27
+ * pnp-virtual, etc.). Returns null if node-pty isn't installed.
28
+ */
29
+ function findNodePtyDir() {
30
+ try {
31
+ const ptyMain = require.resolve('node-pty', { paths: [path.join(__dirname, '..')] });
32
+ let dir = path.dirname(ptyMain);
33
+ // Walk up until we find package.json with "name": "node-pty"
34
+ for (let i = 0; i < 8; i++) {
35
+ const pkg = path.join(dir, 'package.json');
36
+ if (fs.existsSync(pkg)) {
37
+ try {
38
+ const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
39
+ if (json && json.name === 'node-pty') return dir;
40
+ } catch (_) {}
41
+ }
42
+ const parent = path.dirname(dir);
43
+ if (parent === dir) break;
44
+ dir = parent;
45
+ }
46
+ } catch (_) {}
47
+ return null;
48
+ }
49
+
50
+ const ptyDir = findNodePtyDir();
51
+ if (!ptyDir) {
52
+ // node-pty not installed yet (rare during postinstall); skip silently.
53
+ process.exit(0);
54
+ }
55
+
56
+ const prebuildsDir = path.join(ptyDir, 'prebuilds');
57
+ if (!fs.existsSync(prebuildsDir)) {
58
+ process.exit(0);
59
+ }
60
+
61
+ try {
62
+ const platforms = fs.readdirSync(prebuildsDir, { withFileTypes: true })
63
+ .filter(d => d.isDirectory());
64
+
65
+ for (const platform of platforms) {
66
+ const helper = path.join(prebuildsDir, platform.name, 'spawn-helper');
67
+ if (fs.existsSync(helper)) {
68
+ try {
69
+ fs.chmodSync(helper, 0o755);
70
+ } catch (_) {
71
+ // Non-fatal: runtime fix will catch this on first PTY spawn
72
+ }
73
+ }
74
+ }
75
+ } catch (_) {
76
+ // Non-fatal
77
+ }
@@ -11,10 +11,52 @@
11
11
  * - Scrollback is capped at ~100KB total characters
12
12
  */
13
13
 
14
- const pty = require('node-pty');
15
14
  const fs = require('fs');
16
15
  const os = require('os');
17
16
  const path = require('path');
17
+
18
+ // Ensure node-pty's prebuilt spawn-helper is executable BEFORE requiring node-pty.
19
+ // node-pty's prebuild ships with mode 644 instead of 755, causing posix_spawnp
20
+ // to fail on macOS/Linux. The postinstall script handles this in normal installs
21
+ // but doesn't run with --ignore-scripts or in some npx caches. This runtime
22
+ // fallback covers those cases. See: https://github.com/therealarthur/myrlin-workbook/issues/4
23
+ if (process.platform !== 'win32') {
24
+ try {
25
+ const ptyMain = require.resolve('node-pty');
26
+ let dir = path.dirname(ptyMain);
27
+ for (let i = 0; i < 8; i++) {
28
+ const pkg = path.join(dir, 'package.json');
29
+ if (fs.existsSync(pkg)) {
30
+ try {
31
+ const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
32
+ if (json && json.name === 'node-pty') break;
33
+ } catch (_) {}
34
+ }
35
+ const parent = path.dirname(dir);
36
+ if (parent === dir) { dir = null; break; }
37
+ dir = parent;
38
+ }
39
+ if (dir) {
40
+ const prebuildsDir = path.join(dir, 'prebuilds');
41
+ if (fs.existsSync(prebuildsDir)) {
42
+ for (const p of fs.readdirSync(prebuildsDir)) {
43
+ const helper = path.join(prebuildsDir, p, 'spawn-helper');
44
+ if (fs.existsSync(helper)) {
45
+ try {
46
+ const stat = fs.statSync(helper);
47
+ // Only chmod if not already executable, avoids unnecessary syscalls
48
+ if ((stat.mode & 0o111) === 0) fs.chmodSync(helper, 0o755);
49
+ } catch (_) {}
50
+ }
51
+ }
52
+ }
53
+ }
54
+ } catch (_) {
55
+ // node-pty not yet resolvable; require() below will throw with a clearer error
56
+ }
57
+ }
58
+
59
+ const pty = require('node-pty');
18
60
  const { getStore } = require('../state/store');
19
61
 
20
62
  /**
@@ -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 = {};
@@ -5017,8 +5018,8 @@ class CWMApp {
5017
5018
  * Builds a two-pane layout: file tree sidebar on left, editor pane on right.
5018
5019
  * Skips re-initialization if already rendered for the same workspace.
5019
5020
  */
5020
- async renderTasksFilesPanel() {
5021
- const panel = document.getElementById('tasks-files-panel');
5021
+ async renderTasksFilesPanel(container = null) {
5022
+ const panel = container || document.getElementById('tasks-files-panel');
5022
5023
  if (!panel) return;
5023
5024
 
5024
5025
  const ws = this.state.activeWorkspace;
@@ -5353,8 +5354,8 @@ class CWMApp {
5353
5354
  * and commit log; right pane shows diff for the selected file.
5354
5355
  * Includes 10-second auto-refresh when the tab is active.
5355
5356
  */
5356
- async renderTasksGitPanel() {
5357
- const panel = document.getElementById('tasks-git-panel');
5357
+ async renderTasksGitPanel(container = null) {
5358
+ const panel = container || document.getElementById('tasks-git-panel');
5358
5359
  if (!panel) return;
5359
5360
 
5360
5361
  const ws = this.state.activeWorkspace;
@@ -5706,7 +5707,7 @@ class CWMApp {
5706
5707
  }
5707
5708
 
5708
5709
  /** Fetch tasks and render in the active layout */
5709
- async renderTasksView() {
5710
+ async renderTasksView(container = null) {
5710
5711
  // Initialize layout from localStorage (default: board)
5711
5712
  if (!this._tasksLayout) {
5712
5713
  this._tasksLayout = localStorage.getItem('cwm_tasksLayout') || 'board';
@@ -10150,7 +10151,7 @@ class CWMApp {
10150
10151
  // Right-click context menu on terminal pane
10151
10152
  pane.addEventListener('contextmenu', (e) => {
10152
10153
  const tp = this.terminalPanes[slotIdx];
10153
- if (!tp) return; // empty pane - let default menu show
10154
+ if (!tp) { e.preventDefault(); e.stopPropagation(); this._showEmptyPaneContextMenu(slotIdx, e.clientX, e.clientY); return; }
10154
10155
  e.preventDefault();
10155
10156
  e.stopPropagation();
10156
10157
  this.showTerminalContextMenu(slotIdx, e.clientX, e.clientY);
@@ -10305,6 +10306,128 @@ class CWMApp {
10305
10306
  this._refreshPanePin(slotIdx);
10306
10307
  }
10307
10308
 
10309
+ /**
10310
+ * Replace the terminal in a pane with a structured view (tasks, doc, etc.).
10311
+ * The terminal is hidden but not disposed; restoreTerminalInPane() brings it back.
10312
+ * @param {number} slotIdx - The pane slot index
10313
+ * @param {string} viewType - One of: 'tasks-git', 'tasks-td', 'tasks-worktree', 'tasks-files', 'doc'
10314
+ * @param {Object} [viewData={}] - Optional view-specific data (e.g. { docId } for doc views)
10315
+ */
10316
+ async openViewInPane(slotIdx, viewType, viewData = {}) {
10317
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
10318
+ if (!paneEl) return;
10319
+ const termContainer = document.getElementById(`term-container-${slotIdx}`);
10320
+ const viewContainer = document.getElementById(`pane-view-${slotIdx}`);
10321
+ if (!termContainer || !viewContainer) return;
10322
+
10323
+ termContainer.hidden = true;
10324
+ viewContainer.hidden = false;
10325
+ viewContainer.replaceChildren();
10326
+
10327
+ const labels = {
10328
+ 'tasks-git': 'Git',
10329
+ 'tasks-td': 'Tasks',
10330
+ 'tasks-worktree': 'Worktree',
10331
+ 'tasks-files': 'Files',
10332
+ 'doc': 'Doc'
10333
+ };
10334
+ const badge = paneEl.querySelector('.pane-view-badge');
10335
+ const backBtn = paneEl.querySelector('.pane-view-back');
10336
+ if (badge) { badge.textContent = labels[viewType] || viewType; badge.hidden = false; }
10337
+ if (backBtn) backBtn.hidden = false;
10338
+
10339
+ paneEl.dataset.viewType = viewType;
10340
+ paneEl.dataset.viewData = JSON.stringify(viewData);
10341
+
10342
+ await this._renderPaneView(slotIdx, viewType, viewData, viewContainer);
10343
+ this.saveTerminalLayout();
10344
+ }
10345
+
10346
+ /**
10347
+ * Restore a pane from a structured view back to its terminal.
10348
+ * Clears the view container, stops any refresh timers, and refits the terminal.
10349
+ * @param {number} slotIdx - The pane slot index
10350
+ */
10351
+ restoreTerminalInPane(slotIdx) {
10352
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
10353
+ if (!paneEl) return;
10354
+ const termContainer = document.getElementById(`term-container-${slotIdx}`);
10355
+ const viewContainer = document.getElementById(`pane-view-${slotIdx}`);
10356
+
10357
+ if (viewContainer) { viewContainer.hidden = true; viewContainer.replaceChildren(); }
10358
+ if (termContainer) termContainer.hidden = false;
10359
+
10360
+ const badge = paneEl.querySelector('.pane-view-badge');
10361
+ const backBtn = paneEl.querySelector('.pane-view-back');
10362
+ if (badge) badge.hidden = true;
10363
+ if (backBtn) backBtn.hidden = true;
10364
+
10365
+ delete paneEl.dataset.viewType;
10366
+ delete paneEl.dataset.viewData;
10367
+
10368
+ if (this._paneRefreshTimers[slotIdx]) {
10369
+ clearInterval(this._paneRefreshTimers[slotIdx]);
10370
+ delete this._paneRefreshTimers[slotIdx];
10371
+ }
10372
+
10373
+ const tp = this.terminalPanes[slotIdx];
10374
+ if (tp && tp.safeFit) tp.safeFit();
10375
+ this.saveTerminalLayout();
10376
+ }
10377
+
10378
+ /**
10379
+ * Render the appropriate view into the pane view container.
10380
+ * Clears any existing refresh timer for this slot before rendering.
10381
+ * Git panels auto-refresh every 10 seconds.
10382
+ * @param {number} slotIdx - The pane slot index (used for refresh timer key)
10383
+ * @param {string} viewType - The view type identifier
10384
+ * @param {Object} viewData - View-specific data
10385
+ * @param {HTMLElement} container - The container element to render into
10386
+ */
10387
+ async _renderPaneView(slotIdx, viewType, viewData, container) {
10388
+ if (this._paneRefreshTimers[slotIdx]) {
10389
+ clearInterval(this._paneRefreshTimers[slotIdx]);
10390
+ delete this._paneRefreshTimers[slotIdx];
10391
+ }
10392
+ switch (viewType) {
10393
+ case 'tasks-git':
10394
+ await this.renderTasksGitPanel(container);
10395
+ this._paneRefreshTimers[slotIdx] = setInterval(() => this.renderTasksGitPanel(container), 10000);
10396
+ break;
10397
+ case 'tasks-td':
10398
+ await this.renderTasksTdPanel(container);
10399
+ break;
10400
+ case 'tasks-worktree':
10401
+ await this.renderTasksView(container);
10402
+ break;
10403
+ case 'tasks-files':
10404
+ await this.renderTasksFilesPanel(container);
10405
+ break;
10406
+ case 'doc':
10407
+ await this._renderDocInPane(container, viewData);
10408
+ break;
10409
+ }
10410
+ }
10411
+
10412
+ /**
10413
+ * Render a workspace docs textarea into the given pane container.
10414
+ * Saves content on blur via the workspace docs API.
10415
+ * @param {HTMLElement} container - The container element to render into
10416
+ * @param {Object} viewData - Optional view data (currently unused for doc type)
10417
+ */
10418
+ async _renderDocInPane(container, viewData) {
10419
+ const ws = this.state.activeWorkspace;
10420
+ if (!ws) return;
10421
+ const docs = await this.api('GET', `/api/workspaces/${ws.id}/docs`);
10422
+ const textarea = document.createElement('textarea');
10423
+ 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;';
10424
+ textarea.value = docs.raw || '';
10425
+ textarea.addEventListener('blur', async () => {
10426
+ await this.api('POST', `/api/workspaces/${ws.id}/docs`, { content: textarea.value });
10427
+ });
10428
+ container.appendChild(textarea);
10429
+ }
10430
+
10308
10431
  /**
10309
10432
  * Update the activity indicator on a terminal pane header.
10310
10433
  * Called when 'terminal-activity' events fire from TerminalPane.
@@ -10368,6 +10491,22 @@ class CWMApp {
10368
10491
  });
10369
10492
  }
10370
10493
 
10494
+ /**
10495
+ * Context menu shown when right-clicking an empty (no terminal) pane slot.
10496
+ * Offers quick access to all pane view types.
10497
+ */
10498
+ _showEmptyPaneContextMenu(slotIdx, x, y) {
10499
+ const items = [
10500
+ { label: 'Worktree Tasks', action: () => this.openViewInPane(slotIdx, 'tasks-worktree') },
10501
+ { label: 'td Issues', action: () => this.openViewInPane(slotIdx, 'tasks-td') },
10502
+ { label: 'Git Status', action: () => this.openViewInPane(slotIdx, 'tasks-git') },
10503
+ { label: 'Files', action: () => this.openViewInPane(slotIdx, 'tasks-files') },
10504
+ { type: 'sep' },
10505
+ { label: 'Workspace Doc', action: () => this.openViewInPane(slotIdx, 'doc') },
10506
+ ];
10507
+ this._renderContextItems('Open View', items, x, y);
10508
+ }
10509
+
10371
10510
  showTerminalContextMenu(slotIdx, x, y) {
10372
10511
  const tp = this.terminalPanes[slotIdx];
10373
10512
  if (!tp) return;
@@ -10550,10 +10689,28 @@ class CWMApp {
10550
10689
  },
10551
10690
  });
10552
10691
 
10692
+ // ── Switch to view ────────────────────────────────────────
10693
+ items.push({ type: 'sep' });
10694
+ items.push({
10695
+ label: 'Switch to view',
10696
+ submenu: [
10697
+ { label: 'Worktree Tasks', action: () => this.openViewInPane(slotIdx, 'tasks-worktree') },
10698
+ { label: 'td Issues', action: () => this.openViewInPane(slotIdx, 'tasks-td') },
10699
+ { label: 'Git Status', action: () => this.openViewInPane(slotIdx, 'tasks-git') },
10700
+ { label: 'Files', action: () => this.openViewInPane(slotIdx, 'tasks-files') },
10701
+ { label: 'Workspace Doc', action: () => this.openViewInPane(slotIdx, 'doc') },
10702
+ ],
10703
+ });
10704
+
10553
10705
  this._renderContextItems(tp.sessionName || 'Terminal', items, x, y);
10554
10706
  }
10555
10707
 
10556
10708
  closeTerminalPane(slotIdx) {
10709
+ if (this._paneRefreshTimers[slotIdx]) {
10710
+ clearInterval(this._paneRefreshTimers[slotIdx]);
10711
+ delete this._paneRefreshTimers[slotIdx];
10712
+ }
10713
+
10557
10714
  const tp = this.terminalPanes[slotIdx];
10558
10715
  const sessionName = tp ? tp.sessionName : '';
10559
10716
 
@@ -12934,6 +13091,9 @@ class CWMApp {
12934
13091
  group.panes.forEach(p => {
12935
13092
  if (p.sessionId && !this.terminalPanes[p.slot]) {
12936
13093
  this.openTerminalInPane(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
13094
+ if (p.viewType) {
13095
+ setTimeout(() => this.openViewInPane(p.slot, p.viewType, p.viewData || {}), 100);
13096
+ }
12937
13097
  }
12938
13098
  });
12939
13099
  }
@@ -13396,11 +13556,16 @@ class CWMApp {
13396
13556
  const tp = this.terminalPanes[i];
13397
13557
  // Save live TerminalPanes for layout restore.
13398
13558
  if (tp && tp.sessionId) {
13559
+ const paneEl = document.getElementById('term-pane-' + i);
13560
+ const viewType = paneEl?.dataset?.viewType || null;
13561
+ const viewData = viewType ? JSON.parse(paneEl?.dataset?.viewData || '{}') : {};
13399
13562
  group.panes.push({
13400
13563
  slot: i,
13401
13564
  sessionId: tp.sessionId,
13402
13565
  sessionName: tp.sessionName,
13403
13566
  spawnOpts: tp.spawnOpts || {},
13567
+ viewType,
13568
+ viewData,
13404
13569
  });
13405
13570
  }
13406
13571
  }
@@ -562,8 +562,11 @@
562
562
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
563
563
  <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
564
  </button>
565
+ <span class="pane-view-badge" hidden></span>
566
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
565
567
  </div>
566
568
  <div class="terminal-container" id="term-container-0"></div>
569
+ <div class="pane-view-container" id="pane-view-0" hidden></div>
567
570
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
568
571
  <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
572
  </button>
@@ -609,8 +612,11 @@
609
612
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
610
613
  <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
614
  </button>
615
+ <span class="pane-view-badge" hidden></span>
616
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
612
617
  </div>
613
618
  <div class="terminal-container" id="term-container-1"></div>
619
+ <div class="pane-view-container" id="pane-view-1" hidden></div>
614
620
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
615
621
  <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
622
  </button>
@@ -656,8 +662,11 @@
656
662
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
657
663
  <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
664
  </button>
665
+ <span class="pane-view-badge" hidden></span>
666
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
659
667
  </div>
660
668
  <div class="terminal-container" id="term-container-2"></div>
669
+ <div class="pane-view-container" id="pane-view-2" hidden></div>
661
670
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
662
671
  <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
672
  </button>
@@ -703,8 +712,11 @@
703
712
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
704
713
  <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
714
  </button>
715
+ <span class="pane-view-badge" hidden></span>
716
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
706
717
  </div>
707
718
  <div class="terminal-container" id="term-container-3"></div>
719
+ <div class="pane-view-container" id="pane-view-3" hidden></div>
708
720
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
709
721
  <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
722
  </button>
@@ -750,8 +762,11 @@
750
762
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
751
763
  <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
764
  </button>
765
+ <span class="pane-view-badge" hidden></span>
766
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
753
767
  </div>
754
768
  <div class="terminal-container" id="term-container-4"></div>
769
+ <div class="pane-view-container" id="pane-view-4" hidden></div>
755
770
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
756
771
  <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
772
  </button>
@@ -797,8 +812,11 @@
797
812
  <button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
798
813
  <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
814
  </button>
815
+ <span class="pane-view-badge" hidden></span>
816
+ <button class="pane-view-back btn btn-ghost btn-icon btn-sm" hidden title="Back to terminal">←</button>
800
817
  </div>
801
818
  <div class="terminal-container" id="term-container-5"></div>
819
+ <div class="pane-view-container" id="pane-view-5" hidden></div>
802
820
  <button class="terminal-pane-upload" hidden title="Upload image to Claude">
803
821
  <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
822
  </button>
@@ -5111,6 +5111,34 @@ textarea.input {
5111
5111
  background: rgba(203, 166, 247, 0.05);
5112
5112
  }
5113
5113
 
5114
+ /* ─── Pane View Container, Badge, Back Button ──────────── */
5115
+ .pane-view-container {
5116
+ flex: 1;
5117
+ min-height: 0;
5118
+ overflow: auto;
5119
+ display: flex;
5120
+ flex-direction: column;
5121
+ background: var(--base);
5122
+ }
5123
+
5124
+ .pane-view-badge {
5125
+ font-size: 11px;
5126
+ padding: 2px 6px;
5127
+ background: var(--surface1);
5128
+ color: var(--subtext0);
5129
+ border-radius: 4px;
5130
+ font-weight: 600;
5131
+ margin-right: 4px;
5132
+ }
5133
+
5134
+ .pane-view-back {
5135
+ opacity: 0.6;
5136
+ }
5137
+
5138
+ .pane-view-back:hover {
5139
+ opacity: 1;
5140
+ }
5141
+
5114
5142
  /* Terminal pane drag-to-reposition */
5115
5143
  .terminal-pane-header {
5116
5144
  cursor: grab;