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 +1 -1
- package/src/web/public/app.js +107 -25
- package/src/web/public/index.html +1 -0
- package/src/web/public/instance-colors.js +49 -0
- package/src/web/public/styles.css +76 -11
package/package.json
CHANGED
package/src/web/public/app.js
CHANGED
|
@@ -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
|
-
/**
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
8866
|
-
const
|
|
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 indicator — one 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
|
|
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
|
-
|
|
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
|
|
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
|
|
5201
|
-
.
|
|
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
|
-
|
|
5204
|
-
height: 4px;
|
|
5205
|
-
border-radius: 50%;
|
|
5211
|
+
line-height: 0;
|
|
5206
5212
|
flex-shrink: 0;
|
|
5207
|
-
margin-left: 2px;
|
|
5208
5213
|
}
|
|
5209
|
-
|
|
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(--
|
|
5544
|
+
background: var(--tab-color);
|
|
5484
5545
|
flex-shrink: 0;
|
|
5485
5546
|
}
|
|
5486
5547
|
|
|
5487
5548
|
.terminal-group-tab-dot.inactive {
|
|
5488
|
-
|
|
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 {
|