tabminal 3.0.3 → 3.0.5
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 +2 -2
- package/public/app.js +510 -111
- package/public/index.html +61 -11
- package/public/styles.css +1 -1
- package/src/acp-manager.mjs +133 -0
- package/src/persistence.mjs +1 -0
- package/src/server.mjs +6 -2
- package/src/terminal-manager.mjs +67 -5
package/public/app.js
CHANGED
|
@@ -99,6 +99,7 @@ const HEARTBEAT_INTERVAL_MS = 1000;
|
|
|
99
99
|
const RECONNECT_RETRY_MS = 5000;
|
|
100
100
|
const MAIN_SERVER_ID = 'main';
|
|
101
101
|
const RUNTIME_BOOT_ID_STORAGE_KEY = 'tabminal_runtime_boot_id';
|
|
102
|
+
const WORKSPACE_DEVICE_ID_STORAGE_KEY = 'tabminal_workspace_device_id';
|
|
102
103
|
const RECENT_AGENT_USAGE_STORAGE_KEY = 'tabminal_recent_agent_usage';
|
|
103
104
|
const FILE_WORKSPACE_TAB_PREFIX = 'file:';
|
|
104
105
|
const AGENT_WORKSPACE_TAB_PREFIX = 'agent:';
|
|
@@ -184,6 +185,96 @@ function workspaceKeyToFilePath(key) {
|
|
|
184
185
|
return key.slice(FILE_WORKSPACE_TAB_PREFIX.length);
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
function getWorkspaceDeviceId() {
|
|
189
|
+
try {
|
|
190
|
+
let value = localStorage.getItem(WORKSPACE_DEVICE_ID_STORAGE_KEY) || '';
|
|
191
|
+
if (!value) {
|
|
192
|
+
value = crypto.randomUUID();
|
|
193
|
+
localStorage.setItem(WORKSPACE_DEVICE_ID_STORAGE_KEY, value);
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
} catch {
|
|
197
|
+
return 'ephemeral-device';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function uniqueStringList(values) {
|
|
202
|
+
if (!Array.isArray(values)) return [];
|
|
203
|
+
return Array.from(new Set(
|
|
204
|
+
values.filter(
|
|
205
|
+
(value) => typeof value === 'string' && value.length > 0
|
|
206
|
+
)
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeWorkspaceSnapshot(input = {}, fallback = {}) {
|
|
211
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
212
|
+
const base = fallback && typeof fallback === 'object' ? fallback : {};
|
|
213
|
+
const updatedAt = Number.isFinite(source.updatedAt)
|
|
214
|
+
? source.updatedAt
|
|
215
|
+
: (
|
|
216
|
+
Number.isFinite(base.updatedAt)
|
|
217
|
+
? base.updatedAt
|
|
218
|
+
: 0
|
|
219
|
+
);
|
|
220
|
+
const updatedBy = typeof source.updatedBy === 'string'
|
|
221
|
+
? source.updatedBy
|
|
222
|
+
: (
|
|
223
|
+
typeof base.updatedBy === 'string'
|
|
224
|
+
? base.updatedBy
|
|
225
|
+
: ''
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
updatedAt,
|
|
229
|
+
updatedBy,
|
|
230
|
+
isVisible: !!source.isVisible,
|
|
231
|
+
openFiles: uniqueStringList(source.openFiles),
|
|
232
|
+
terminalDisplayMode: source.terminalDisplayMode === 'tab'
|
|
233
|
+
? 'tab'
|
|
234
|
+
: 'auto',
|
|
235
|
+
expandedPaths: uniqueStringList(source.expandedPaths)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function compareWorkspaceSnapshots(left, right) {
|
|
240
|
+
const leftUpdatedAt = Number.isFinite(left?.updatedAt) ? left.updatedAt : 0;
|
|
241
|
+
const rightUpdatedAt = Number.isFinite(right?.updatedAt)
|
|
242
|
+
? right.updatedAt
|
|
243
|
+
: 0;
|
|
244
|
+
if (leftUpdatedAt !== rightUpdatedAt) {
|
|
245
|
+
return leftUpdatedAt - rightUpdatedAt;
|
|
246
|
+
}
|
|
247
|
+
const leftUpdatedBy = typeof left?.updatedBy === 'string'
|
|
248
|
+
? left.updatedBy
|
|
249
|
+
: '';
|
|
250
|
+
const rightUpdatedBy = typeof right?.updatedBy === 'string'
|
|
251
|
+
? right.updatedBy
|
|
252
|
+
: '';
|
|
253
|
+
return leftUpdatedBy.localeCompare(rightUpdatedBy);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildWorkspaceSnapshotForSession(session, overrides = {}) {
|
|
257
|
+
return normalizeWorkspaceSnapshot({
|
|
258
|
+
...session.sharedWorkspaceState,
|
|
259
|
+
isVisible: session.editorState.isVisible,
|
|
260
|
+
openFiles: session.editorState.openFiles,
|
|
261
|
+
terminalDisplayMode: session.sharedWorkspaceState.terminalDisplayMode,
|
|
262
|
+
expandedPaths: session.sharedWorkspaceState.expandedPaths,
|
|
263
|
+
...overrides
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function touchSharedWorkspace(session, overrides = {}) {
|
|
268
|
+
if (!session) return null;
|
|
269
|
+
const snapshot = buildWorkspaceSnapshotForSession(session, {
|
|
270
|
+
...overrides,
|
|
271
|
+
updatedAt: Date.now(),
|
|
272
|
+
updatedBy: getWorkspaceDeviceId()
|
|
273
|
+
});
|
|
274
|
+
session.sharedWorkspaceState = snapshot;
|
|
275
|
+
return snapshot;
|
|
276
|
+
}
|
|
277
|
+
|
|
187
278
|
// #region Sidebar Toggle (Mobile)
|
|
188
279
|
const sidebarToggle = document.getElementById('sidebar-toggle');
|
|
189
280
|
const sidebar = document.getElementById('sidebar');
|
|
@@ -616,7 +707,7 @@ class EditorManager {
|
|
|
616
707
|
}
|
|
617
708
|
|
|
618
709
|
isTerminalTabPinned(session = this.currentSession) {
|
|
619
|
-
return session?.
|
|
710
|
+
return session?.sharedWorkspaceState?.terminalDisplayMode === 'tab';
|
|
620
711
|
}
|
|
621
712
|
|
|
622
713
|
canToggleTerminalWorkspaceMode(session = this.currentSession) {
|
|
@@ -694,17 +785,20 @@ class EditorManager {
|
|
|
694
785
|
);
|
|
695
786
|
|
|
696
787
|
if (isCompactWorkspaceMode()) {
|
|
697
|
-
session.
|
|
788
|
+
session.sharedWorkspaceState.terminalDisplayMode = 'auto';
|
|
698
789
|
this.updateTerminalLayoutButton();
|
|
699
790
|
return;
|
|
700
791
|
}
|
|
701
792
|
|
|
702
|
-
if (
|
|
793
|
+
if (
|
|
794
|
+
(session.sharedWorkspaceState.terminalDisplayMode || 'auto')
|
|
795
|
+
=== nextMode
|
|
796
|
+
) {
|
|
703
797
|
this.updateTerminalLayoutButton();
|
|
704
798
|
return;
|
|
705
799
|
}
|
|
706
800
|
|
|
707
|
-
session.
|
|
801
|
+
session.sharedWorkspaceState.terminalDisplayMode = nextMode;
|
|
708
802
|
if (nextMode === 'tab') {
|
|
709
803
|
session.workspaceState.activeTabKey = TERMINAL_WORKSPACE_TAB_KEY;
|
|
710
804
|
} else if (
|
|
@@ -714,7 +808,7 @@ class EditorManager {
|
|
|
714
808
|
this.getPreferredNonTerminalWorkspaceTabKey(session);
|
|
715
809
|
}
|
|
716
810
|
|
|
717
|
-
session.saveState();
|
|
811
|
+
session.saveState({ touchWorkspace: true });
|
|
718
812
|
this.switchTo(session);
|
|
719
813
|
this.updateEditorPaneVisibility();
|
|
720
814
|
renderTabs();
|
|
@@ -1469,12 +1563,15 @@ class EditorManager {
|
|
|
1469
1563
|
this.updateTerminalLayoutButton();
|
|
1470
1564
|
}
|
|
1471
1565
|
|
|
1472
|
-
toggle() {
|
|
1473
|
-
if (!
|
|
1474
|
-
const
|
|
1566
|
+
toggle(session = this.currentSession) {
|
|
1567
|
+
if (!session) return;
|
|
1568
|
+
const isCurrentSession = this.currentSession?.key === session.key;
|
|
1569
|
+
const state = session.editorState;
|
|
1475
1570
|
state.isVisible = !state.isVisible;
|
|
1476
1571
|
|
|
1477
|
-
const tab = document.querySelector(
|
|
1572
|
+
const tab = document.querySelector(
|
|
1573
|
+
`.tab-item[data-session-key="${session.key}"]`
|
|
1574
|
+
);
|
|
1478
1575
|
if (tab) {
|
|
1479
1576
|
if (state.isVisible) tab.classList.add('editor-open');
|
|
1480
1577
|
else tab.classList.remove('editor-open');
|
|
@@ -1482,26 +1579,34 @@ class EditorManager {
|
|
|
1482
1579
|
|
|
1483
1580
|
if (state.isVisible) {
|
|
1484
1581
|
// Only render if empty (first open)
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
if (activeKey) {
|
|
1491
|
-
this.activateWorkspaceTab(activeKey, true);
|
|
1582
|
+
if (
|
|
1583
|
+
session.fileTreeElement
|
|
1584
|
+
&& session.fileTreeElement.children.length === 0
|
|
1585
|
+
) {
|
|
1586
|
+
this.refreshSessionTree(session);
|
|
1492
1587
|
}
|
|
1588
|
+
} else if (session.fileTreeElement) {
|
|
1589
|
+
session.fileTreeElement.innerHTML = '';
|
|
1493
1590
|
}
|
|
1494
1591
|
|
|
1495
|
-
if (
|
|
1592
|
+
if (isCurrentSession) {
|
|
1496
1593
|
this.renderEditorTabs();
|
|
1497
|
-
const activeKey = this.getActiveWorkspaceTabKey(
|
|
1594
|
+
const activeKey = this.getActiveWorkspaceTabKey(session);
|
|
1498
1595
|
if (activeKey) {
|
|
1499
1596
|
this.activateWorkspaceTab(activeKey, true);
|
|
1500
1597
|
}
|
|
1598
|
+
if (this.hasCompactWorkspaceTabs(session)) {
|
|
1599
|
+
this.renderEditorTabs();
|
|
1600
|
+
const compactActiveKey = this.getActiveWorkspaceTabKey(session);
|
|
1601
|
+
if (compactActiveKey) {
|
|
1602
|
+
this.activateWorkspaceTab(compactActiveKey, true);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
this.updateEditorPaneVisibility();
|
|
1501
1606
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1607
|
+
|
|
1608
|
+
session.updateTabUI();
|
|
1609
|
+
session.saveState({ touchWorkspace: true });
|
|
1505
1610
|
}
|
|
1506
1611
|
|
|
1507
1612
|
switchTo(session) {
|
|
@@ -1527,6 +1632,9 @@ class EditorManager {
|
|
|
1527
1632
|
const shouldShowWorkspace = state.isVisible
|
|
1528
1633
|
|| this.hasVisibleWorkspaceTabs(session);
|
|
1529
1634
|
if (shouldShowWorkspace) {
|
|
1635
|
+
if (state.isVisible) {
|
|
1636
|
+
this.refreshSessionTree(session);
|
|
1637
|
+
}
|
|
1530
1638
|
this.renderEditorTabs();
|
|
1531
1639
|
const activeKey = this.getActiveWorkspaceTabKey(session);
|
|
1532
1640
|
if (activeKey) {
|
|
@@ -1578,7 +1686,12 @@ class EditorManager {
|
|
|
1578
1686
|
if (file.isDirectory) div.classList.add('is-dir');
|
|
1579
1687
|
|
|
1580
1688
|
let isExpanded = false;
|
|
1581
|
-
if (
|
|
1689
|
+
if (
|
|
1690
|
+
file.isDirectory
|
|
1691
|
+
&& session.sharedWorkspaceState.expandedPaths.includes(
|
|
1692
|
+
file.path
|
|
1693
|
+
)
|
|
1694
|
+
) {
|
|
1582
1695
|
isExpanded = true;
|
|
1583
1696
|
li.classList.add('expanded');
|
|
1584
1697
|
}
|
|
@@ -1598,11 +1711,17 @@ class EditorManager {
|
|
|
1598
1711
|
if (file.isDirectory) {
|
|
1599
1712
|
if (li.classList.contains('expanded')) {
|
|
1600
1713
|
li.classList.remove('expanded');
|
|
1601
|
-
session.
|
|
1602
|
-
|
|
1714
|
+
session.sharedWorkspaceState.expandedPaths =
|
|
1715
|
+
session.sharedWorkspaceState.expandedPaths
|
|
1716
|
+
.filter((path) => path !== file.path);
|
|
1717
|
+
session.saveState({ touchWorkspace: true });
|
|
1718
|
+
void session.server.fetch('/api/memory/expand', {
|
|
1603
1719
|
method: 'POST',
|
|
1604
1720
|
headers: { 'Content-Type': 'application/json' },
|
|
1605
|
-
body: JSON.stringify({
|
|
1721
|
+
body: JSON.stringify({
|
|
1722
|
+
path: file.path,
|
|
1723
|
+
expanded: false
|
|
1724
|
+
})
|
|
1606
1725
|
});
|
|
1607
1726
|
|
|
1608
1727
|
icon.innerHTML = this.getIcon(file.name, true, false);
|
|
@@ -1610,18 +1729,26 @@ class EditorManager {
|
|
|
1610
1729
|
if (childUl) childUl.remove();
|
|
1611
1730
|
} else {
|
|
1612
1731
|
li.classList.add('expanded');
|
|
1613
|
-
session.
|
|
1614
|
-
|
|
1732
|
+
session.sharedWorkspaceState.expandedPaths =
|
|
1733
|
+
uniqueStringList([
|
|
1734
|
+
...session.sharedWorkspaceState.expandedPaths,
|
|
1735
|
+
file.path
|
|
1736
|
+
]);
|
|
1737
|
+
session.saveState({ touchWorkspace: true });
|
|
1738
|
+
void session.server.fetch('/api/memory/expand', {
|
|
1615
1739
|
method: 'POST',
|
|
1616
1740
|
headers: { 'Content-Type': 'application/json' },
|
|
1617
|
-
body: JSON.stringify({
|
|
1741
|
+
body: JSON.stringify({
|
|
1742
|
+
path: file.path,
|
|
1743
|
+
expanded: true
|
|
1744
|
+
})
|
|
1618
1745
|
});
|
|
1619
1746
|
|
|
1620
1747
|
icon.innerHTML = this.getIcon(file.name, true, true);
|
|
1621
1748
|
await this.renderTree(file.path, li, session);
|
|
1622
1749
|
}
|
|
1623
1750
|
} else {
|
|
1624
|
-
this.openFile(file.path);
|
|
1751
|
+
await this.openFile(file.path, session);
|
|
1625
1752
|
}
|
|
1626
1753
|
});
|
|
1627
1754
|
|
|
@@ -1639,13 +1766,25 @@ class EditorManager {
|
|
|
1639
1766
|
}
|
|
1640
1767
|
}
|
|
1641
1768
|
|
|
1642
|
-
async openFile(filePath) {
|
|
1643
|
-
|
|
1644
|
-
|
|
1769
|
+
async openFile(filePath, sessionOrRestore = this.currentSession) {
|
|
1770
|
+
const session = typeof sessionOrRestore === 'boolean'
|
|
1771
|
+
? this.currentSession
|
|
1772
|
+
: sessionOrRestore;
|
|
1773
|
+
if (!session) return;
|
|
1774
|
+
if (this.currentSession?.key !== session.key) {
|
|
1775
|
+
await switchToSession(session.key);
|
|
1776
|
+
}
|
|
1777
|
+
const targetSession = this.currentSession?.key === session.key
|
|
1778
|
+
? this.currentSession
|
|
1779
|
+
: session;
|
|
1780
|
+
if (!targetSession) return;
|
|
1781
|
+
const state = targetSession.editorState;
|
|
1645
1782
|
|
|
1783
|
+
let touchedWorkspace = false;
|
|
1646
1784
|
if (!state.openFiles.includes(filePath)) {
|
|
1647
1785
|
state.openFiles.push(filePath);
|
|
1648
1786
|
this.renderEditorTabs();
|
|
1787
|
+
touchedWorkspace = true;
|
|
1649
1788
|
}
|
|
1650
1789
|
|
|
1651
1790
|
this.updateEditorPaneVisibility();
|
|
@@ -1660,7 +1799,9 @@ class EditorManager {
|
|
|
1660
1799
|
|
|
1661
1800
|
if (!isImage) {
|
|
1662
1801
|
try {
|
|
1663
|
-
const res = await
|
|
1802
|
+
const res = await targetSession.server.fetch(
|
|
1803
|
+
`/api/fs/read?path=${encodeURIComponent(filePath)}`
|
|
1804
|
+
);
|
|
1664
1805
|
if (!res.ok) throw new Error('Failed to read file');
|
|
1665
1806
|
const data = await res.json();
|
|
1666
1807
|
content = data.content;
|
|
@@ -1692,7 +1833,9 @@ class EditorManager {
|
|
|
1692
1833
|
}
|
|
1693
1834
|
|
|
1694
1835
|
this.activateFileTab(filePath);
|
|
1695
|
-
|
|
1836
|
+
if (touchedWorkspace) {
|
|
1837
|
+
targetSession.saveState({ touchWorkspace: true });
|
|
1838
|
+
}
|
|
1696
1839
|
}
|
|
1697
1840
|
|
|
1698
1841
|
closeFile(filePath) {
|
|
@@ -1700,8 +1843,10 @@ class EditorManager {
|
|
|
1700
1843
|
const state = this.currentSession.editorState;
|
|
1701
1844
|
|
|
1702
1845
|
const index = state.openFiles.indexOf(filePath);
|
|
1846
|
+
let touchedWorkspace = false;
|
|
1703
1847
|
if (index > -1) {
|
|
1704
1848
|
state.openFiles.splice(index, 1);
|
|
1849
|
+
touchedWorkspace = true;
|
|
1705
1850
|
}
|
|
1706
1851
|
|
|
1707
1852
|
this.renderEditorTabs();
|
|
@@ -1727,7 +1872,9 @@ class EditorManager {
|
|
|
1727
1872
|
}
|
|
1728
1873
|
|
|
1729
1874
|
// Save state AFTER updating activeFilePath
|
|
1730
|
-
|
|
1875
|
+
if (touchedWorkspace) {
|
|
1876
|
+
this.currentSession.saveState({ touchWorkspace: true });
|
|
1877
|
+
}
|
|
1731
1878
|
}
|
|
1732
1879
|
|
|
1733
1880
|
renderEditorTabs() {
|
|
@@ -4241,33 +4388,58 @@ class Session {
|
|
|
4241
4388
|
this.lastExecutionEntry = null;
|
|
4242
4389
|
this.needsAttention = false;
|
|
4243
4390
|
this.lastNotifiedExecutionId = '';
|
|
4391
|
+
const legacyEditorState = data.editorState
|
|
4392
|
+
&& typeof data.editorState === 'object'
|
|
4393
|
+
? data.editorState
|
|
4394
|
+
: {};
|
|
4395
|
+
const sharedWorkspaceInput = data.workspaceState
|
|
4396
|
+
&& typeof data.workspaceState === 'object'
|
|
4397
|
+
? data.workspaceState
|
|
4398
|
+
: legacyEditorState;
|
|
4399
|
+
const hasExplicitExpandedPaths = Array.isArray(
|
|
4400
|
+
sharedWorkspaceInput?.expandedPaths
|
|
4401
|
+
);
|
|
4402
|
+
this.sharedWorkspaceState = normalizeWorkspaceSnapshot(
|
|
4403
|
+
{
|
|
4404
|
+
...sharedWorkspaceInput,
|
|
4405
|
+
expandedPaths: hasExplicitExpandedPaths
|
|
4406
|
+
? sharedWorkspaceInput.expandedPaths
|
|
4407
|
+
: Array.from(this.server.expandedPaths)
|
|
4408
|
+
}
|
|
4409
|
+
);
|
|
4410
|
+
const initialActiveFilePath = (
|
|
4411
|
+
typeof legacyEditorState.activeFilePath === 'string'
|
|
4412
|
+
&& this.sharedWorkspaceState.openFiles.includes(
|
|
4413
|
+
legacyEditorState.activeFilePath
|
|
4414
|
+
)
|
|
4415
|
+
)
|
|
4416
|
+
? legacyEditorState.activeFilePath
|
|
4417
|
+
: (this.sharedWorkspaceState.openFiles[0] || null);
|
|
4244
4418
|
|
|
4245
4419
|
this.editorState = {
|
|
4246
|
-
isVisible:
|
|
4420
|
+
isVisible: this.sharedWorkspaceState.isVisible,
|
|
4247
4421
|
root: this.cwd,
|
|
4248
|
-
openFiles:
|
|
4249
|
-
activeFilePath:
|
|
4422
|
+
openFiles: [...this.sharedWorkspaceState.openFiles],
|
|
4423
|
+
activeFilePath: initialActiveFilePath,
|
|
4250
4424
|
viewStates: new Map() // Path -> ViewState
|
|
4251
4425
|
};
|
|
4252
4426
|
this.workspaceState = {
|
|
4253
|
-
activeTabKey:
|
|
4254
|
-
|| (
|
|
4255
|
-
? makeFileWorkspaceTabKey(
|
|
4427
|
+
activeTabKey: legacyEditorState.activeWorkspaceTabKey
|
|
4428
|
+
|| (initialActiveFilePath
|
|
4429
|
+
? makeFileWorkspaceTabKey(initialActiveFilePath)
|
|
4256
4430
|
: ''),
|
|
4257
|
-
lastNonTerminalTabKey:
|
|
4431
|
+
lastNonTerminalTabKey: legacyEditorState.activeWorkspaceTabKey
|
|
4258
4432
|
&& !isTerminalWorkspaceTabKey(
|
|
4259
|
-
|
|
4433
|
+
legacyEditorState.activeWorkspaceTabKey
|
|
4260
4434
|
)
|
|
4261
|
-
?
|
|
4262
|
-
: (
|
|
4263
|
-
? makeFileWorkspaceTabKey(
|
|
4435
|
+
? legacyEditorState.activeWorkspaceTabKey
|
|
4436
|
+
: (initialActiveFilePath
|
|
4437
|
+
? makeFileWorkspaceTabKey(initialActiveFilePath)
|
|
4264
4438
|
: ''),
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
recentAgentTabKeys: Array.isArray(data.editorState?.recentAgentTabKeys)
|
|
4270
|
-
? data.editorState.recentAgentTabKeys.filter(
|
|
4439
|
+
recentAgentTabKeys: Array.isArray(
|
|
4440
|
+
legacyEditorState?.recentAgentTabKeys
|
|
4441
|
+
)
|
|
4442
|
+
? legacyEditorState.recentAgentTabKeys.filter(
|
|
4271
4443
|
(key) => typeof key === 'string' && key.length > 0
|
|
4272
4444
|
)
|
|
4273
4445
|
: []
|
|
@@ -4375,8 +4547,63 @@ class Session {
|
|
|
4375
4547
|
}
|
|
4376
4548
|
}
|
|
4377
4549
|
|
|
4550
|
+
applySharedWorkspaceSnapshot(nextWorkspaceState) {
|
|
4551
|
+
const normalized = normalizeWorkspaceSnapshot(
|
|
4552
|
+
nextWorkspaceState,
|
|
4553
|
+
this.sharedWorkspaceState
|
|
4554
|
+
);
|
|
4555
|
+
const resolveFallbackActiveKey = () => {
|
|
4556
|
+
if (this.editorState.activeFilePath) {
|
|
4557
|
+
return makeFileWorkspaceTabKey(this.editorState.activeFilePath);
|
|
4558
|
+
}
|
|
4559
|
+
const agentTab = getAgentTabsForSession(this)[0];
|
|
4560
|
+
if (agentTab) {
|
|
4561
|
+
return agentTab.key;
|
|
4562
|
+
}
|
|
4563
|
+
return normalized.terminalDisplayMode === 'tab'
|
|
4564
|
+
? TERMINAL_WORKSPACE_TAB_KEY
|
|
4565
|
+
: '';
|
|
4566
|
+
};
|
|
4567
|
+
this.sharedWorkspaceState = normalized;
|
|
4568
|
+
this.editorState.isVisible = normalized.isVisible;
|
|
4569
|
+
this.editorState.openFiles = [...normalized.openFiles];
|
|
4570
|
+
|
|
4571
|
+
if (
|
|
4572
|
+
this.editorState.activeFilePath
|
|
4573
|
+
&& !this.editorState.openFiles.includes(
|
|
4574
|
+
this.editorState.activeFilePath
|
|
4575
|
+
)
|
|
4576
|
+
) {
|
|
4577
|
+
this.editorState.activeFilePath = this.editorState.openFiles[0]
|
|
4578
|
+
|| null;
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
const activeKey = this.workspaceState.activeTabKey || '';
|
|
4582
|
+
if (isFileWorkspaceTabKey(activeKey)) {
|
|
4583
|
+
const filePath = workspaceKeyToFilePath(activeKey);
|
|
4584
|
+
if (!this.editorState.openFiles.includes(filePath)) {
|
|
4585
|
+
this.workspaceState.activeTabKey = resolveFallbackActiveKey();
|
|
4586
|
+
}
|
|
4587
|
+
} else if (
|
|
4588
|
+
isTerminalWorkspaceTabKey(activeKey)
|
|
4589
|
+
&& normalized.terminalDisplayMode !== 'tab'
|
|
4590
|
+
) {
|
|
4591
|
+
this.workspaceState.activeTabKey = resolveFallbackActiveKey();
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
const lastNonTerminalKey =
|
|
4595
|
+
this.workspaceState.lastNonTerminalTabKey || '';
|
|
4596
|
+
if (isFileWorkspaceTabKey(lastNonTerminalKey)) {
|
|
4597
|
+
const filePath = workspaceKeyToFilePath(lastNonTerminalKey);
|
|
4598
|
+
if (!this.editorState.openFiles.includes(filePath)) {
|
|
4599
|
+
this.workspaceState.lastNonTerminalTabKey = '';
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4378
4604
|
update(data) {
|
|
4379
4605
|
let changed = false;
|
|
4606
|
+
let workspaceChanged = false;
|
|
4380
4607
|
const nextManaged = normalizeManagedSessionMeta(data.managed);
|
|
4381
4608
|
if (
|
|
4382
4609
|
JSON.stringify(nextManaged) !== JSON.stringify(this.managed || null)
|
|
@@ -4414,8 +4641,37 @@ class Session {
|
|
|
4414
4641
|
this.env = data.env;
|
|
4415
4642
|
changed = true;
|
|
4416
4643
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4644
|
+
|
|
4645
|
+
const nextWorkspaceState = data.workspaceState
|
|
4646
|
+
&& typeof data.workspaceState === 'object'
|
|
4647
|
+
? data.workspaceState
|
|
4648
|
+
: (
|
|
4649
|
+
data.editorState
|
|
4650
|
+
&& typeof data.editorState === 'object'
|
|
4651
|
+
? data.editorState
|
|
4652
|
+
: null
|
|
4653
|
+
);
|
|
4654
|
+
if (
|
|
4655
|
+
nextWorkspaceState
|
|
4656
|
+
&& compareWorkspaceSnapshots(
|
|
4657
|
+
nextWorkspaceState,
|
|
4658
|
+
this.sharedWorkspaceState
|
|
4659
|
+
) > 0
|
|
4660
|
+
) {
|
|
4661
|
+
const previousSnapshot = JSON.stringify(this.sharedWorkspaceState);
|
|
4662
|
+
this.applySharedWorkspaceSnapshot(nextWorkspaceState);
|
|
4663
|
+
const nextSnapshot = JSON.stringify(this.sharedWorkspaceState);
|
|
4664
|
+
if (previousSnapshot !== nextSnapshot) {
|
|
4665
|
+
changed = true;
|
|
4666
|
+
workspaceChanged = true;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
if (
|
|
4671
|
+
data.cols
|
|
4672
|
+
&& data.rows
|
|
4673
|
+
&& (data.cols !== this.cols || data.rows !== this.rows)
|
|
4674
|
+
) {
|
|
4419
4675
|
this.cols = data.cols;
|
|
4420
4676
|
this.rows = data.rows;
|
|
4421
4677
|
if (this.previewTerm) {
|
|
@@ -4426,6 +4682,18 @@ class Session {
|
|
|
4426
4682
|
|
|
4427
4683
|
if (changed) {
|
|
4428
4684
|
this.updateTabUI();
|
|
4685
|
+
if (workspaceChanged) {
|
|
4686
|
+
if (this.fileTreeElement) {
|
|
4687
|
+
if (this.editorState.isVisible) {
|
|
4688
|
+
editorManager.refreshSessionTree(this);
|
|
4689
|
+
} else {
|
|
4690
|
+
this.fileTreeElement.innerHTML = '';
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
if (workspaceChanged && state.activeSessionKey === this.key) {
|
|
4695
|
+
refreshWorkspaceIfSessionActive(this);
|
|
4696
|
+
}
|
|
4429
4697
|
}
|
|
4430
4698
|
}
|
|
4431
4699
|
|
|
@@ -4480,6 +4748,7 @@ class Session {
|
|
|
4480
4748
|
const tab = tabListEl.querySelector(`[data-session-key="${this.key}"]`);
|
|
4481
4749
|
if (!tab) return;
|
|
4482
4750
|
|
|
4751
|
+
tab.classList.toggle('editor-open', !!this.editorState?.isVisible);
|
|
4483
4752
|
tab.classList.toggle('agent-managed-session', isAgentManagedSession(this));
|
|
4484
4753
|
tab.classList.toggle('agent-open', getAgentTabsForSession(this).length > 0);
|
|
4485
4754
|
|
|
@@ -4549,20 +4818,12 @@ class Session {
|
|
|
4549
4818
|
}
|
|
4550
4819
|
}
|
|
4551
4820
|
|
|
4552
|
-
saveState() {
|
|
4821
|
+
saveState({ touchWorkspace = false } = {}) {
|
|
4822
|
+
if (!touchWorkspace) {
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4553
4825
|
const pending = getPendingSession(this.key);
|
|
4554
|
-
pending.
|
|
4555
|
-
isVisible: this.editorState.isVisible,
|
|
4556
|
-
root: this.editorState.root,
|
|
4557
|
-
openFiles: this.editorState.openFiles,
|
|
4558
|
-
activeFilePath: this.editorState.activeFilePath,
|
|
4559
|
-
activeWorkspaceTabKey: this.workspaceState.activeTabKey || '',
|
|
4560
|
-
recentAgentTabKeys: Array.isArray(this.workspaceState.recentAgentTabKeys)
|
|
4561
|
-
? this.workspaceState.recentAgentTabKeys
|
|
4562
|
-
: [],
|
|
4563
|
-
terminalDisplayMode:
|
|
4564
|
-
this.workspaceState.terminalDisplayMode || 'auto'
|
|
4565
|
-
};
|
|
4826
|
+
pending.workspaceState = touchSharedWorkspace(this);
|
|
4566
4827
|
}
|
|
4567
4828
|
|
|
4568
4829
|
connect() {
|
|
@@ -5525,6 +5786,32 @@ class AgentTab {
|
|
|
5525
5786
|
}
|
|
5526
5787
|
}
|
|
5527
5788
|
|
|
5789
|
+
applyInventory(data) {
|
|
5790
|
+
const previousSession = this.getLinkedSession();
|
|
5791
|
+
this.runtimeId = data.runtimeId || this.runtimeId || '';
|
|
5792
|
+
this.runtimeKey = data.runtimeKey || this.runtimeKey || '';
|
|
5793
|
+
this.acpSessionId = data.acpSessionId || this.acpSessionId || '';
|
|
5794
|
+
this.agentId = data.agentId || this.agentId || '';
|
|
5795
|
+
this.agentLabel = data.agentLabel || this.agentLabel || 'Agent';
|
|
5796
|
+
this.title = typeof data.title === 'string' ? data.title : this.title;
|
|
5797
|
+
this.commandLabel = data.commandLabel || this.commandLabel || '';
|
|
5798
|
+
this.terminalSessionId = data.terminalSessionId || this.terminalSessionId;
|
|
5799
|
+
this.cwd = data.cwd || this.cwd || '';
|
|
5800
|
+
this.createdAt = data.createdAt || this.createdAt || new Date().toISOString();
|
|
5801
|
+
this.status = data.status || this.status || 'ready';
|
|
5802
|
+
this.busy = typeof data.busy === 'boolean' ? data.busy : this.busy;
|
|
5803
|
+
this.errorMessage = data.errorMessage || this.errorMessage || '';
|
|
5804
|
+
this.currentModeId = data.currentModeId || this.currentModeId || '';
|
|
5805
|
+
const nextSession = this.getLinkedSession();
|
|
5806
|
+
previousSession?.updateTabUI();
|
|
5807
|
+
if (nextSession && nextSession !== previousSession) {
|
|
5808
|
+
nextSession.updateTabUI();
|
|
5809
|
+
}
|
|
5810
|
+
if (nextSession) {
|
|
5811
|
+
refreshWorkspaceIfSessionActive(nextSession);
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5528
5815
|
async #waitForSettled(timeoutMs = 5000) {
|
|
5529
5816
|
const deadline = Date.now() + timeoutMs;
|
|
5530
5817
|
while (Date.now() < deadline) {
|
|
@@ -5673,7 +5960,7 @@ const state = {
|
|
|
5673
5960
|
};
|
|
5674
5961
|
|
|
5675
5962
|
const pendingChanges = {
|
|
5676
|
-
sessions: new Map() // sessionKey -> { resize,
|
|
5963
|
+
sessions: new Map() // sessionKey -> { resize, workspaceState, fileWrites: Map<path, content> }
|
|
5677
5964
|
};
|
|
5678
5965
|
|
|
5679
5966
|
if (typeof window !== 'undefined') {
|
|
@@ -5765,14 +6052,6 @@ function getAgentTabsForServer(serverId) {
|
|
|
5765
6052
|
);
|
|
5766
6053
|
}
|
|
5767
6054
|
|
|
5768
|
-
function hasActiveAgentSyncNeed(serverId) {
|
|
5769
|
-
return getAgentTabsForServer(serverId).some((tab) => (
|
|
5770
|
-
!!tab.busy
|
|
5771
|
-
|| tab.status === 'restoring'
|
|
5772
|
-
|| tab.isDrainingQueuedPrompt
|
|
5773
|
-
));
|
|
5774
|
-
}
|
|
5775
|
-
|
|
5776
6055
|
function shouldSyncManagedTerminalSession(server, nextSummary, _previous = null) {
|
|
5777
6056
|
if (!server || !nextSummary) return false;
|
|
5778
6057
|
const nextSessionId = String(nextSummary.terminalSessionId || '').trim();
|
|
@@ -8491,9 +8770,32 @@ function dispatchSyntheticKey(target, init) {
|
|
|
8491
8770
|
return event.defaultPrevented;
|
|
8492
8771
|
}
|
|
8493
8772
|
|
|
8773
|
+
function isUiElementVisible(element) {
|
|
8774
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
8775
|
+
const style = window.getComputedStyle(element);
|
|
8776
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
8777
|
+
return false;
|
|
8778
|
+
}
|
|
8779
|
+
return element.getClientRects().length > 0;
|
|
8780
|
+
}
|
|
8781
|
+
|
|
8494
8782
|
function getVirtualInputTarget() {
|
|
8783
|
+
const activeSession = getActiveSession();
|
|
8784
|
+
const activeWorkspaceKey = activeSession
|
|
8785
|
+
? editorManager?.getActiveWorkspaceTabKey(activeSession) || ''
|
|
8786
|
+
: '';
|
|
8787
|
+
if (
|
|
8788
|
+
activeSession
|
|
8789
|
+
&& isTerminalWorkspaceTabKey(activeWorkspaceKey)
|
|
8790
|
+
) {
|
|
8791
|
+
return {
|
|
8792
|
+
kind: 'terminal',
|
|
8793
|
+
session: activeSession
|
|
8794
|
+
};
|
|
8795
|
+
}
|
|
8495
8796
|
if (
|
|
8496
8797
|
editorManager?.editor
|
|
8798
|
+
&& isUiElementVisible(editorManager.monacoContainer)
|
|
8497
8799
|
&& typeof editorManager.editor.hasTextFocus === 'function'
|
|
8498
8800
|
&& editorManager.editor.hasTextFocus()
|
|
8499
8801
|
) {
|
|
@@ -8504,25 +8806,24 @@ function getVirtualInputTarget() {
|
|
|
8504
8806
|
};
|
|
8505
8807
|
}
|
|
8506
8808
|
const activeElement = document.activeElement;
|
|
8507
|
-
if (isTextEntryControl(activeElement)) {
|
|
8809
|
+
if (isTextEntryControl(activeElement) && isUiElementVisible(activeElement)) {
|
|
8508
8810
|
return { kind: 'text', element: activeElement };
|
|
8509
8811
|
}
|
|
8510
8812
|
if (
|
|
8511
|
-
|
|
8512
|
-
&& state.sessions.has(state.activeSessionKey)
|
|
8813
|
+
activeSession
|
|
8513
8814
|
&& terminalEl
|
|
8514
8815
|
&& activeElement
|
|
8515
8816
|
&& terminalEl.contains(activeElement)
|
|
8516
8817
|
) {
|
|
8517
8818
|
return {
|
|
8518
8819
|
kind: 'terminal',
|
|
8519
|
-
session:
|
|
8820
|
+
session: activeSession
|
|
8520
8821
|
};
|
|
8521
8822
|
}
|
|
8522
|
-
if (
|
|
8823
|
+
if (activeSession) {
|
|
8523
8824
|
return {
|
|
8524
8825
|
kind: 'terminal',
|
|
8525
|
-
session:
|
|
8826
|
+
session: activeSession
|
|
8526
8827
|
};
|
|
8527
8828
|
}
|
|
8528
8829
|
return { kind: 'none' };
|
|
@@ -8696,6 +8997,57 @@ function upsertAgentTab(server, data) {
|
|
|
8696
8997
|
return agentTab;
|
|
8697
8998
|
}
|
|
8698
8999
|
|
|
9000
|
+
function upsertAgentInventoryTab(server, data) {
|
|
9001
|
+
const key = makeAgentTabKey(server.id, data.id);
|
|
9002
|
+
const existing = state.agentTabs.get(key);
|
|
9003
|
+
if (existing) {
|
|
9004
|
+
existing.applyInventory(data);
|
|
9005
|
+
existing.connect();
|
|
9006
|
+
return existing;
|
|
9007
|
+
}
|
|
9008
|
+
const agentTab = new AgentTab(data, server);
|
|
9009
|
+
state.agentTabs.set(key, agentTab);
|
|
9010
|
+
return agentTab;
|
|
9011
|
+
}
|
|
9012
|
+
|
|
9013
|
+
function reconcileAgentInventory(server, inventory) {
|
|
9014
|
+
if (!server || !inventory || typeof inventory !== 'object') {
|
|
9015
|
+
return;
|
|
9016
|
+
}
|
|
9017
|
+
const restoring = !!inventory.restoring;
|
|
9018
|
+
const seenKeys = new Set();
|
|
9019
|
+
const touchedSessions = new Set();
|
|
9020
|
+
|
|
9021
|
+
for (const tabData of Array.isArray(inventory.tabs) ? inventory.tabs : []) {
|
|
9022
|
+
const agentTab = upsertAgentInventoryTab(server, tabData);
|
|
9023
|
+
seenKeys.add(agentTab.key);
|
|
9024
|
+
const session = agentTab.getLinkedSession();
|
|
9025
|
+
if (session) {
|
|
9026
|
+
touchedSessions.add(session.key);
|
|
9027
|
+
}
|
|
9028
|
+
}
|
|
9029
|
+
|
|
9030
|
+
if (!restoring) {
|
|
9031
|
+
for (const agentTab of getAgentTabsForServer(server.id)) {
|
|
9032
|
+
if (seenKeys.has(agentTab.key)) continue;
|
|
9033
|
+
const session = agentTab.getLinkedSession();
|
|
9034
|
+
if (session) {
|
|
9035
|
+
touchedSessions.add(session.key);
|
|
9036
|
+
}
|
|
9037
|
+
removeAgentTab(agentTab.key);
|
|
9038
|
+
}
|
|
9039
|
+
}
|
|
9040
|
+
|
|
9041
|
+
for (const sessionKey of touchedSessions) {
|
|
9042
|
+
const session = state.sessions.get(sessionKey);
|
|
9043
|
+
if (!session) continue;
|
|
9044
|
+
session.updateTabUI();
|
|
9045
|
+
if (state.activeSessionKey === session.key) {
|
|
9046
|
+
refreshWorkspaceIfSessionActive(session);
|
|
9047
|
+
}
|
|
9048
|
+
}
|
|
9049
|
+
}
|
|
9050
|
+
|
|
8699
9051
|
function noteRecentAgentTab(session, agentTabKey) {
|
|
8700
9052
|
if (!session || !agentTabKey) return;
|
|
8701
9053
|
const recent = Array.isArray(session.workspaceState?.recentAgentTabKeys)
|
|
@@ -9098,12 +9450,17 @@ async function syncServerList() {
|
|
|
9098
9450
|
async function fetchExpandedPaths(server) {
|
|
9099
9451
|
try {
|
|
9100
9452
|
const res = await server.fetch('/api/memory/expanded');
|
|
9101
|
-
if (res.ok)
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9453
|
+
if (!res.ok) return;
|
|
9454
|
+
const list = await res.json();
|
|
9455
|
+
server.expandedPaths.clear();
|
|
9456
|
+
for (const path of Array.isArray(list) ? list : []) {
|
|
9457
|
+
if (typeof path === 'string' && path.length > 0) {
|
|
9458
|
+
server.expandedPaths.add(path);
|
|
9459
|
+
}
|
|
9105
9460
|
}
|
|
9106
|
-
} catch (
|
|
9461
|
+
} catch (error) {
|
|
9462
|
+
console.error(error);
|
|
9463
|
+
}
|
|
9107
9464
|
}
|
|
9108
9465
|
|
|
9109
9466
|
async function syncServer(server) {
|
|
@@ -9114,9 +9471,6 @@ async function syncServer(server) {
|
|
|
9114
9471
|
const promise = (async () => {
|
|
9115
9472
|
const now = Date.now();
|
|
9116
9473
|
const wasReconnecting = server.connectionStatus === 'reconnecting';
|
|
9117
|
-
const shouldRefreshAgents = !server.agentStateLoaded
|
|
9118
|
-
|| wasReconnecting
|
|
9119
|
-
|| hasActiveAgentSyncNeed(server.id);
|
|
9120
9474
|
if (
|
|
9121
9475
|
wasReconnecting
|
|
9122
9476
|
&& server.nextSyncAt
|
|
@@ -9146,8 +9500,8 @@ async function syncServer(server) {
|
|
|
9146
9500
|
sessionUpdate.resize = pending.resize;
|
|
9147
9501
|
hasUpdate = true;
|
|
9148
9502
|
}
|
|
9149
|
-
if (pending.
|
|
9150
|
-
sessionUpdate.
|
|
9503
|
+
if (pending.workspaceState) {
|
|
9504
|
+
sessionUpdate.workspaceState = pending.workspaceState;
|
|
9151
9505
|
hasUpdate = true;
|
|
9152
9506
|
}
|
|
9153
9507
|
if (pending.fileWrites && pending.fileWrites.size > 0) {
|
|
@@ -9197,7 +9551,7 @@ async function syncServer(server) {
|
|
|
9197
9551
|
if (!pending) continue;
|
|
9198
9552
|
|
|
9199
9553
|
if (update.resize) delete pending.resize;
|
|
9200
|
-
if (update.
|
|
9554
|
+
if (update.workspaceState) delete pending.workspaceState;
|
|
9201
9555
|
if (update.fileWrites) {
|
|
9202
9556
|
for (const file of update.fileWrites) {
|
|
9203
9557
|
pending.fileWrites.delete(file.path);
|
|
@@ -9228,13 +9582,7 @@ async function syncServer(server) {
|
|
|
9228
9582
|
|
|
9229
9583
|
const sessions = Array.isArray(data) ? data : data.sessions;
|
|
9230
9584
|
reconcileSessions(server, sessions || []);
|
|
9231
|
-
|
|
9232
|
-
try {
|
|
9233
|
-
await syncAgentsForServer(server, { force: true });
|
|
9234
|
-
} catch (error) {
|
|
9235
|
-
console.warn('Failed to sync agents:', error);
|
|
9236
|
-
}
|
|
9237
|
-
}
|
|
9585
|
+
reconcileAgentInventory(server, data.agents);
|
|
9238
9586
|
} catch (error) {
|
|
9239
9587
|
if (!wasReconnecting) {
|
|
9240
9588
|
console.warn(
|
|
@@ -9926,11 +10274,7 @@ function createTabElement(session) {
|
|
|
9926
10274
|
toggleEditorBtn.title = 'Toggle File Editor';
|
|
9927
10275
|
toggleEditorBtn.onclick = (e) => {
|
|
9928
10276
|
e.stopPropagation();
|
|
9929
|
-
|
|
9930
|
-
switchToSession(session.key).then(() => editorManager.toggle());
|
|
9931
|
-
} else {
|
|
9932
|
-
editorManager.toggle();
|
|
9933
|
-
}
|
|
10277
|
+
editorManager.toggle(session);
|
|
9934
10278
|
};
|
|
9935
10279
|
tab.appendChild(toggleEditorBtn);
|
|
9936
10280
|
|
|
@@ -10270,12 +10614,65 @@ function renderServerControls() {
|
|
|
10270
10614
|
|
|
10271
10615
|
// #region Notification Manager
|
|
10272
10616
|
const notificationManager = new NotificationManager();
|
|
10617
|
+
const APP_NOTIFICATION_QUIET_MS = 30_000;
|
|
10618
|
+
const APP_NOTIFICATION_IDLE_MS = 3 * 60 * 1000;
|
|
10619
|
+
let appNotificationQuietUntil = Date.now() + APP_NOTIFICATION_QUIET_MS;
|
|
10620
|
+
let lastAppInteractionAt = Date.now();
|
|
10621
|
+
|
|
10622
|
+
function noteAppInteraction() {
|
|
10623
|
+
lastAppInteractionAt = Date.now();
|
|
10624
|
+
}
|
|
10625
|
+
|
|
10626
|
+
function enterAppNotificationQuietPeriod(duration = APP_NOTIFICATION_QUIET_MS) {
|
|
10627
|
+
appNotificationQuietUntil = Math.max(
|
|
10628
|
+
appNotificationQuietUntil,
|
|
10629
|
+
Date.now() + duration
|
|
10630
|
+
);
|
|
10631
|
+
}
|
|
10632
|
+
|
|
10633
|
+
function shouldNotifyConnectionStatus() {
|
|
10634
|
+
if (Date.now() < appNotificationQuietUntil) {
|
|
10635
|
+
return false;
|
|
10636
|
+
}
|
|
10637
|
+
if (document.visibilityState !== 'visible') {
|
|
10638
|
+
return false;
|
|
10639
|
+
}
|
|
10640
|
+
if (typeof document.hasFocus === 'function' && !document.hasFocus()) {
|
|
10641
|
+
return false;
|
|
10642
|
+
}
|
|
10643
|
+
if ((Date.now() - lastAppInteractionAt) > APP_NOTIFICATION_IDLE_MS) {
|
|
10644
|
+
return false;
|
|
10645
|
+
}
|
|
10646
|
+
return true;
|
|
10647
|
+
}
|
|
10648
|
+
|
|
10649
|
+
document.addEventListener('pointerdown', noteAppInteraction, {
|
|
10650
|
+
capture: true,
|
|
10651
|
+
passive: true
|
|
10652
|
+
});
|
|
10653
|
+
document.addEventListener('touchstart', noteAppInteraction, {
|
|
10654
|
+
capture: true,
|
|
10655
|
+
passive: true
|
|
10656
|
+
});
|
|
10657
|
+
document.addEventListener('keydown', noteAppInteraction, {
|
|
10658
|
+
capture: true
|
|
10659
|
+
});
|
|
10660
|
+
window.addEventListener('focus', () => {
|
|
10661
|
+
noteAppInteraction();
|
|
10662
|
+
enterAppNotificationQuietPeriod();
|
|
10663
|
+
});
|
|
10664
|
+
window.addEventListener('pageshow', () => {
|
|
10665
|
+
noteAppInteraction();
|
|
10666
|
+
enterAppNotificationQuietPeriod();
|
|
10667
|
+
});
|
|
10273
10668
|
|
|
10274
10669
|
document.addEventListener('click', () => {
|
|
10275
10670
|
notificationManager.requestPermission();
|
|
10276
10671
|
}, { once: true, capture: true });
|
|
10277
10672
|
document.addEventListener('visibilitychange', () => {
|
|
10278
10673
|
if (document.visibilityState === 'visible') {
|
|
10674
|
+
noteAppInteraction();
|
|
10675
|
+
enterAppNotificationQuietPeriod();
|
|
10279
10676
|
clearVisibleAttentionState();
|
|
10280
10677
|
}
|
|
10281
10678
|
});
|
|
@@ -10317,18 +10714,20 @@ function setStatus(server, status) {
|
|
|
10317
10714
|
statusMemory.set(server.id, status);
|
|
10318
10715
|
server.connectionStatus = status;
|
|
10319
10716
|
renderServerControls();
|
|
10320
|
-
|
|
10321
|
-
const activeServer = getActiveServer();
|
|
10322
|
-
if (!activeServer || activeServer.id !== server.id) return;
|
|
10323
10717
|
const hostName = getDisplayHost(server);
|
|
10324
10718
|
const target = hostName || 'host';
|
|
10719
|
+
const shouldNotify = shouldNotifyConnectionStatus();
|
|
10325
10720
|
|
|
10326
|
-
if (status === 'reconnecting') {
|
|
10721
|
+
if (status === 'reconnecting' && shouldNotify) {
|
|
10327
10722
|
alert(`Lost connection to ${target}. Reconnecting...`, {
|
|
10328
10723
|
type: 'warning',
|
|
10329
10724
|
title: 'Connection'
|
|
10330
10725
|
});
|
|
10331
|
-
} else if (
|
|
10726
|
+
} else if (
|
|
10727
|
+
status === 'connected'
|
|
10728
|
+
&& prevStatus === 'reconnecting'
|
|
10729
|
+
&& shouldNotify
|
|
10730
|
+
) {
|
|
10332
10731
|
alert(`Connection to ${target} restored.`, {
|
|
10333
10732
|
type: 'success',
|
|
10334
10733
|
title: 'Connection'
|
|
@@ -10338,7 +10737,7 @@ function setStatus(server, status) {
|
|
|
10338
10737
|
type: 'error',
|
|
10339
10738
|
title: 'Connection'
|
|
10340
10739
|
});
|
|
10341
|
-
} else if (status === 'connected' && !prevStatus) {
|
|
10740
|
+
} else if (status === 'connected' && !prevStatus && shouldNotify) {
|
|
10342
10741
|
alert(`Connected to ${target}.`, {
|
|
10343
10742
|
type: 'success',
|
|
10344
10743
|
title: 'Connection'
|