myrlin-workbook 0.9.33 → 0.9.34

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.33",
3
+ "version": "0.9.34",
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": {
@@ -148,6 +148,7 @@ class CWMApp {
148
148
  // Key: groupId, Value: { panes: [TerminalPane|null x MAX_PANES], domFragments: [DocumentFragment|null x MAX_PANES] }
149
149
  this._groupPaneCache = {};
150
150
  this.PANE_SLOT_COLORS = ['mauve', 'blue', 'green', 'peach', 'red', 'pink'];
151
+ this.TAB_COLORS = (window.InstanceColors && window.InstanceColors.TAB_COLORS) || [];
151
152
  this._gridColSizes = [1, 1]; // fr ratios for column widths
152
153
  this._gridRowSizes = [1, 1]; // fr ratios for row heights
153
154
  // Voice recognition instances per slot (for mic-to-terminal input)
@@ -1421,6 +1422,14 @@ class CWMApp {
1421
1422
  return;
1422
1423
  }
1423
1424
 
1425
+ // Pip click — navigate to that instance (handled BEFORE session-item click).
1426
+ const pip = e.target.closest('.instance-indicator');
1427
+ if (pip && pip.dataset.tabId) {
1428
+ e.stopPropagation();
1429
+ this._navigateToInstance(pip.dataset.tabId, parseInt(pip.dataset.slot, 10));
1430
+ return;
1431
+ }
1432
+
1424
1433
  const wsSessionItem = e.target.closest('.ws-session-item');
1425
1434
  if (wsSessionItem) {
1426
1435
  e.stopPropagation();
@@ -4148,14 +4157,68 @@ class CWMApp {
4148
4157
  });
4149
4158
  }
4150
4159
 
4151
- /** Find which pane slot (0-3) a session is open in, or -1 if not found */
4152
- getSlotForSession(sessionId) {
4153
- for (let i = 0; i < this.terminalPanes.length; i++) {
4154
- if (this.terminalPanes[i] && this.terminalPanes[i].sessionId === sessionId) {
4155
- return i;
4156
- }
4160
+ /** Return one entry per place a session is open across all tab groups. */
4161
+ getSessionInstances(sessionId) {
4162
+ return window.InstanceColors.getSessionInstances(sessionId, this._tabGroups || []);
4163
+ }
4164
+
4165
+ /** Return the (positional) colour for a tab — global index across all tabs. */
4166
+ getTabColor(tabId) {
4167
+ return window.InstanceColors.getTabColor(tabId, this._tabGroups || []);
4168
+ }
4169
+
4170
+ /** Render one indicator: top half = tab colour, bottom half = slot colour, 1px divider. */
4171
+ renderInstanceIndicator({ tabColor, slotColor, title, tabId, slot }) {
4172
+ return `<span class="instance-indicator"
4173
+ title="${this.escapeHtml(title || '')}"
4174
+ data-tab-id="${this.escapeHtml(tabId)}"
4175
+ data-slot="${slot}"
4176
+ style="--c-outer:var(--${tabColor});
4177
+ --c-inner:var(--${slotColor})">
4178
+ <span class="instance-indicator-square">
4179
+ <span class="instance-indicator-inner"></span>
4180
+ </span>
4181
+ </span>`;
4182
+ }
4183
+
4184
+ /** Render the full row of indicators for a session, or empty string. */
4185
+ renderInstanceIndicatorRow(sessionId) {
4186
+ if (!this.state.settings.paneColorHighlights) return '';
4187
+ const instances = this.getSessionInstances(sessionId);
4188
+ if (!instances.length) return '';
4189
+ return `<span class="instance-indicator-row">${
4190
+ instances.map(inst => this.renderInstanceIndicator({
4191
+ tabColor: this.getTabColor(inst.tabId),
4192
+ slotColor: this.PANE_SLOT_COLORS[inst.slot % this.PANE_SLOT_COLORS.length],
4193
+ title: this._formatInstanceTooltip(inst),
4194
+ tabId: inst.tabId,
4195
+ slot: inst.slot,
4196
+ })).join('')
4197
+ }</span>`;
4198
+ }
4199
+
4200
+ /** Build a textual tooltip for an instance: "<tab> › slot N". */
4201
+ _formatInstanceTooltip({ tabId, slot }) {
4202
+ const tab = (this._tabGroups || []).find(g => g.id === tabId);
4203
+ const tabName = tab ? tab.name : '?';
4204
+ return `${tabName} › slot ${slot + 1}`;
4205
+ }
4206
+
4207
+ /** Navigate to a session instance: switch to its tab, then briefly pulse the pane. */
4208
+ _navigateToInstance(tabId, slot) {
4209
+ if (this._activeGroupId !== tabId) {
4210
+ this.switchTerminalGroup(tabId);
4157
4211
  }
4158
- return -1;
4212
+ // Pulse the target pane on the next frame so the switch has rendered.
4213
+ requestAnimationFrame(() => {
4214
+ const paneEls = document.querySelectorAll('.terminal-pane');
4215
+ const paneEl = paneEls[slot];
4216
+ if (!paneEl) return;
4217
+ paneEl.classList.remove('pane-nav-pulse');
4218
+ void paneEl.offsetWidth; // force reflow so the animation restarts
4219
+ paneEl.classList.add('pane-nav-pulse');
4220
+ setTimeout(() => paneEl.classList.remove('pane-nav-pulse'), 800);
4221
+ });
4159
4222
  }
4160
4223
 
4161
4224
  /** Open the settings overlay */
@@ -8862,11 +8925,8 @@ class CWMApp {
8862
8925
  }
8863
8926
  }
8864
8927
 
8865
- // Pane color pip show matching dot if session is open in a terminal slot
8866
- const slotIdx = this.getSlotForSession(s.id);
8867
- const pip = (slotIdx !== -1 && this.state.settings.paneColorHighlights)
8868
- ? `<span class="pane-color-pip" style="background:var(--${this.PANE_SLOT_COLORS[slotIdx]})"></span>`
8869
- : '';
8928
+ // Three-layer indicatorone per place this session is open across all tab groups
8929
+ const pip = this.renderInstanceIndicatorRow(s.id);
8870
8930
 
8871
8931
  // Build meta row (badges + size + time) — only if there's something to show
8872
8932
  const metaParts = [badges, sizeStr ? `<span class="ws-session-size">${sizeStr}</span>` : ''].filter(Boolean).join('');
@@ -8874,8 +8934,8 @@ class CWMApp {
8874
8934
  const timeEl = timeStr ? `<span class="ws-session-time">${timeStr}</span>` : '';
8875
8935
 
8876
8936
  return `<div class="ws-session-item${isHidden ? ' ws-session-hidden' : ''}" data-session-id="${s.id}" draggable="true" title="${this.escapeHtml(s.workingDir || '')}">
8877
- <span class="ws-session-dot${tristateAttr}" style="background: ${statusDot}"></span>${pip}
8878
- <span class="ws-session-name">${this.escapeHtml(name)}</span>${timeEl}
8937
+ <span class="ws-session-dot${tristateAttr}" style="background: ${statusDot}"></span>
8938
+ <span class="ws-session-name">${this.escapeHtml(name)}</span>${pip}${timeEl}
8879
8939
  ${metaRow}
8880
8940
  </div>`;
8881
8941
  };
@@ -10543,13 +10603,12 @@ class CWMApp {
10543
10603
  this.switchTerminalTab(slotIdx);
10544
10604
  }
10545
10605
 
10546
- // Re-render sidebar to show pane color pips
10547
- if (this.state.settings.paneColorHighlights) {
10548
- this.renderWorkspaces();
10549
- }
10550
-
10551
10606
  // Refresh pinned-notes badge for this pane now that a session is loaded
10552
10607
  this._refreshPanePin(slotIdx);
10608
+
10609
+ // Route through the centralised chokepoint so the sidebar indicator and
10610
+ // server-persisted layout reflect the new pane.
10611
+ this.saveTerminalLayout();
10553
10612
  }
10554
10613
 
10555
10614
  /**
@@ -11025,14 +11084,13 @@ class CWMApp {
11025
11084
  this.updateTerminalTabs();
11026
11085
  }
11027
11086
 
11028
- // Re-render sidebar to remove pane color pips
11029
- if (this.state.settings.paneColorHighlights) {
11030
- this.renderWorkspaces();
11031
- }
11032
-
11033
11087
  if (sessionName) {
11034
11088
  this.showToast(`"${sessionName}" moved to background - drag it back to reconnect`, 'info');
11035
11089
  }
11090
+
11091
+ // Route through the centralised chokepoint so the sidebar indicator and
11092
+ // server-persisted layout reflect the closed pane.
11093
+ this.saveTerminalLayout();
11036
11094
  }
11037
11095
 
11038
11096
  /**
@@ -11360,6 +11418,10 @@ class CWMApp {
11360
11418
  if (tp) tp.safeFit();
11361
11419
  });
11362
11420
  });
11421
+
11422
+ // Route through the centralised chokepoint so the sidebar indicator and
11423
+ // server-persisted layout reflect the swapped panes.
11424
+ this.saveTerminalLayout();
11363
11425
  }
11364
11426
 
11365
11427
  updateTerminalGridLayout() {
@@ -13413,7 +13475,10 @@ class CWMApp {
13413
13475
  const tp = this.terminalPanes.find((_, i) => p.slot === i);
13414
13476
  return tp !== null;
13415
13477
  });
13416
- return `<button class="terminal-group-tab${isActive ? ' active' : ''}" data-group-id="${g.id}">
13478
+ const tabColor = this.getTabColor(g.id);
13479
+ return `<button class="terminal-group-tab${isActive ? ' active' : ''}"
13480
+ data-group-id="${g.id}"
13481
+ style="--tab-color:var(--${tabColor})">
13417
13482
  <span class="terminal-group-tab-dot${hasActive ? '' : ' inactive'}"></span>
13418
13483
  <span class="terminal-group-tab-name">${this.escapeHtml(g.name)}</span>
13419
13484
  ${paneCount > 0 ? `<span class="terminal-group-tab-count">${paneCount}</span>` : ''}
@@ -14351,7 +14416,24 @@ class CWMApp {
14351
14416
  });
14352
14417
  }
14353
14418
 
14419
+ /**
14420
+ * Central chokepoint for tab/pane mutations. Performs three things in order:
14421
+ * 1. Synchronously flushes live pane state via `saveCurrentGroupPanes()`.
14422
+ * 2. Synchronously re-renders the sidebar via `renderWorkspaces()`.
14423
+ * 3. Schedules a debounced (500ms) PUT to /api/layout for server persistence.
14424
+ * Callers that mutate `terminalPanes` or `_tabGroups` should route through
14425
+ * this method rather than calling `renderWorkspaces()` themselves. Note:
14426
+ * `renderWorkspaces()` does not feed back into `saveTerminalLayout()`, so
14427
+ * there is no recursion.
14428
+ */
14354
14429
  saveTerminalLayout() {
14430
+ // Flush live pane state into _tabGroups[*].panes synchronously so the
14431
+ // sidebar indicator (which reads from _tabGroups) is fresh — the
14432
+ // server PUT below stays debounced.
14433
+ this.saveCurrentGroupPanes();
14434
+ if (typeof this.renderWorkspaces === 'function') {
14435
+ this.renderWorkspaces();
14436
+ }
14355
14437
  clearTimeout(this._layoutSaveTimer);
14356
14438
  this._layoutSaveTimer = setTimeout(async () => {
14357
14439
  this.saveCurrentGroupPanes();
@@ -1749,6 +1749,7 @@
1749
1749
  <script src="vendor/xterm-addon-fit/xterm-addon-fit.min.js"></script>
1750
1750
  <script src="vendor/xterm-addon-web-links/xterm-addon-web-links.min.js"></script>
1751
1751
  <script src="terminal.js"></script>
1752
+ <script src="instance-colors.js"></script>
1752
1753
  <script src="app.js"></script>
1753
1754
  <script src="schedules.js"></script>
1754
1755
  <script>
@@ -0,0 +1,49 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ Instance-color helpers for the session indicator.
3
+ Pure functions over tab data; no DOM dependencies.
4
+ Loaded as a browser <script> AND requireable from Node tests.
5
+ SPDX-License-Identifier: AGPL-3.0-only
6
+ ═══════════════════════════════════════════════════════════════ */
7
+
8
+ (function (root, factory) {
9
+ if (typeof module === 'object' && module.exports) {
10
+ module.exports = factory();
11
+ } else {
12
+ root.InstanceColors = factory();
13
+ }
14
+ })(typeof self !== 'undefined' ? self : this, function () {
15
+ 'use strict';
16
+
17
+ const TAB_COLORS = ['red', 'yellow', 'green', 'teal', 'blue', 'mauve'];
18
+
19
+ /**
20
+ * Return one entry per place sessionId is currently open across all tab groups.
21
+ * @param {string} sessionId
22
+ * @param {Array<{id:string, panes:Array<{slot:number,sessionId:string}>}>} tabGroups
23
+ * @returns {Array<{tabId:string, slot:number}>}
24
+ */
25
+ function getSessionInstances(sessionId, tabGroups) {
26
+ const out = [];
27
+ if (!sessionId || !Array.isArray(tabGroups)) return out;
28
+ for (const tab of tabGroups) {
29
+ const panes = (tab && tab.panes) || [];
30
+ for (const p of panes) {
31
+ if (p && p.sessionId === sessionId) {
32
+ out.push({ tabId: tab.id, slot: p.slot });
33
+ }
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * Return the tab's positional colour. Index is the tab's global position
41
+ * across all tab groups (regardless of folder), wrapping modulo the palette.
42
+ */
43
+ function getTabColor(tabId, tabGroups) {
44
+ const idx = (tabGroups || []).findIndex(g => g.id === tabId);
45
+ return TAB_COLORS[(idx >= 0 ? idx : 0) % TAB_COLORS.length];
46
+ }
47
+
48
+ return { TAB_COLORS, getSessionInstances, getTabColor };
49
+ });
@@ -1290,7 +1290,7 @@ textarea.input {
1290
1290
  flex-wrap: wrap;
1291
1291
  align-items: center;
1292
1292
  gap: 4px 6px;
1293
- padding: 5px 10px 5px 34px;
1293
+ padding: 5px 10px 5px 24px;
1294
1294
  border-radius: var(--radius-sm);
1295
1295
  cursor: pointer;
1296
1296
  transition: background var(--transition-fast);
@@ -5197,19 +5197,67 @@ html.pane-colors-enabled .terminal-pane:not(.terminal-pane-empty)[data-slot="5"]
5197
5197
  border-left: 3px solid var(--pink);
5198
5198
  }
5199
5199
 
5200
- /* ─── Sidebar Pane Color Pip ───────────────────────────── */
5201
- .pane-color-pip {
5200
+ /* ─── Sidebar Session Instance Indicator ───────────────── */
5201
+ .instance-indicator-row {
5202
+ display: inline-flex;
5203
+ gap: 0; /* per-pip 2px padding provides spacing */
5204
+ margin-left: 4px;
5205
+ flex-shrink: 0;
5206
+ }
5207
+ .instance-indicator {
5208
+ padding: 2px; /* invisible 2px hit-area extension; also growth room */
5209
+ cursor: pointer;
5202
5210
  display: inline-block;
5203
- width: 4px;
5204
- height: 4px;
5205
- border-radius: 50%;
5211
+ line-height: 0;
5206
5212
  flex-shrink: 0;
5207
- margin-left: 2px;
5208
5213
  }
5209
- html:not(.pane-colors-enabled) .pane-color-pip {
5214
+ .instance-indicator-square {
5215
+ width: 10px;
5216
+ height: 10px;
5217
+ border-radius: 1px;
5218
+ background: var(--c-outer);
5219
+ position: relative;
5220
+ overflow: hidden;
5221
+ display: inline-block;
5222
+ transition: transform 120ms ease, box-shadow 120ms ease;
5223
+ transform-origin: center center;
5224
+ }
5225
+ .instance-indicator:hover .instance-indicator-square {
5226
+ transform: scale(1.1);
5227
+ }
5228
+ /* Hover ring around the visible square — disabled for now.
5229
+ .instance-indicator:hover .instance-indicator-square {
5230
+ box-shadow: 0 0 0 1.5px var(--c-outer);
5231
+ }
5232
+ */
5233
+ .instance-indicator-inner {
5234
+ position: absolute;
5235
+ left: 0;
5236
+ right: 0;
5237
+ bottom: 0;
5238
+ height: 7px;
5239
+ background: var(--c-inner);
5240
+ border-top: 1px solid #000;
5241
+ box-sizing: border-box;
5242
+ }
5243
+ html:not(.pane-colors-enabled) .instance-indicator-row {
5210
5244
  display: none;
5211
5245
  }
5212
5246
 
5247
+ /* Pane pulse — short flash when navigating from a sidebar pip */
5248
+ @keyframes pane-nav-pulse {
5249
+ 0% { background-color: rgba(205, 214, 244, 0.05); box-shadow: inset 0 0 0 3px var(--text), 0 0 16px rgba(205, 214, 244, 0.4); }
5250
+ 100% { background-color: transparent; box-shadow: inset 0 0 0 0 transparent, 0 0 0 transparent; }
5251
+ }
5252
+ .pane-nav-pulse::after {
5253
+ content: "";
5254
+ position: absolute;
5255
+ inset: 0;
5256
+ pointer-events: none;
5257
+ z-index: 9999;
5258
+ animation: pane-nav-pulse 700ms ease-out forwards;
5259
+ }
5260
+
5213
5261
  /* ─── Tri-State Dots (Worktree Tasks) ─────────────────── */
5214
5262
  .ws-session-dot[data-tristate="busy"] {
5215
5263
  animation: tristate-pulse 1.5s ease-in-out infinite;
@@ -5461,11 +5509,24 @@ html.activity-indicators-disabled .terminal-pane-activity {
5461
5509
  transition: all var(--transition-fast);
5462
5510
  position: relative;
5463
5511
  /* Touch: let DragDropTouch polyfill handle long-press-to-drag without the
5464
- browser claiming the gesture for the scrollable tab strip.
5512
+ browser claiming the gesture for the scrollable tab strip.
5465
5513
  */
5466
5514
  touch-action: none;
5467
5515
  }
5468
5516
 
5517
+ .terminal-group-tab::before {
5518
+ content: '';
5519
+ position: absolute;
5520
+ left: 50%;
5521
+ bottom: 2px;
5522
+ transform: translateX(-50%);
5523
+ width: 85%;
5524
+ height: 2px;
5525
+ border-radius: 2px;
5526
+ background: var(--tab-color);
5527
+ pointer-events: none;
5528
+ }
5529
+
5469
5530
  .terminal-group-tab:hover {
5470
5531
  color: var(--text);
5471
5532
  background: var(--surface0);
@@ -5480,12 +5541,16 @@ html.activity-indicators-disabled .terminal-pane-activity {
5480
5541
  width: 6px;
5481
5542
  height: 6px;
5482
5543
  border-radius: 50%;
5483
- background: var(--green);
5544
+ background: var(--tab-color);
5484
5545
  flex-shrink: 0;
5485
5546
  }
5486
5547
 
5487
5548
  .terminal-group-tab-dot.inactive {
5488
- background: var(--surface2);
5549
+ opacity: 0.4;
5550
+ }
5551
+
5552
+ .terminal-group-tab-name {
5553
+ color: var(--tab-color);
5489
5554
  }
5490
5555
 
5491
5556
  .terminal-group-tab-count {