tabminal 3.0.2 → 3.0.4
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/README.md +1 -1
- package/package.json +2 -2
- package/public/app.js +449 -105
- 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/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> `Tab(ter)minal`, a Cloud-Native terminal and ACP agent workspace for desktop, tablet, and phone.
|
|
4
4
|
|
|
5
5
|
`Tabminal` combines persistent server-side terminal sessions, a built-in
|
|
6
|
-
workspace, multi-host access, and Agent Client Protocol (ACP) integrations in
|
|
6
|
+
workspace, multi-host access, and [Agent Client Protocol (ACP)](https://agentclientprotocol.com/get-started/introduction) integrations in
|
|
7
7
|
one UI. It is designed for people who want a real terminal, real files, and
|
|
8
8
|
real agent tooling without being tied to a desktop-only client let you code from
|
|
9
9
|
your desktop, tablet, or phone with an intelligent, persistent, and rich
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabminal",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "Tab(ter)minal, a Cloud-Native terminal and ACP agent workspace for desktop, tablet, and phone.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"main": "src/server.mjs",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/server.mjs",
|
|
12
|
-
"dev": "node --watch src/server.mjs",
|
|
12
|
+
"dev": "node --watch-path=src --watch-path=public src/server.mjs",
|
|
13
13
|
"build": "node build.mjs",
|
|
14
14
|
"test": "node --test",
|
|
15
15
|
"updep": "npx npm-check-updates -u && npm install",
|
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
|
|
package/src/acp-manager.mjs
CHANGED
|
@@ -941,6 +941,106 @@ function normalizePersistedTimelineOrder(value, fallback = 0) {
|
|
|
941
941
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
942
942
|
}
|
|
943
943
|
|
|
944
|
+
function normalizeReplayMessageEntry(message = {}) {
|
|
945
|
+
return {
|
|
946
|
+
role: typeof message.role === 'string'
|
|
947
|
+
? message.role
|
|
948
|
+
: 'assistant',
|
|
949
|
+
kind: typeof message.kind === 'string'
|
|
950
|
+
? message.kind
|
|
951
|
+
: 'message',
|
|
952
|
+
text: typeof message.text === 'string'
|
|
953
|
+
? message.text
|
|
954
|
+
: ''
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export function createRestoreReplayState(messages = []) {
|
|
959
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
const replayMessages = messages
|
|
963
|
+
.map((message) => normalizeReplayMessageEntry(message))
|
|
964
|
+
.filter((message) => message.text);
|
|
965
|
+
if (replayMessages.length === 0) {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
messages: replayMessages,
|
|
970
|
+
index: -1,
|
|
971
|
+
offset: 0,
|
|
972
|
+
started: false,
|
|
973
|
+
exhausted: false
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function consumeRestoredMessageReplay(state, role, kind, text) {
|
|
978
|
+
if (!state || state.exhausted) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
const chunk = typeof text === 'string' ? text : '';
|
|
982
|
+
if (!chunk) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const findReplayStart = () => {
|
|
987
|
+
for (let index = 0; index < state.messages.length; index += 1) {
|
|
988
|
+
const message = state.messages[index];
|
|
989
|
+
if (message.role !== role || message.kind !== kind) {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (message.text.startsWith(chunk)) {
|
|
993
|
+
state.index = index;
|
|
994
|
+
state.offset = chunk.length;
|
|
995
|
+
state.started = true;
|
|
996
|
+
if (state.offset >= message.text.length) {
|
|
997
|
+
state.index += 1;
|
|
998
|
+
state.offset = 0;
|
|
999
|
+
}
|
|
1000
|
+
if (state.index >= state.messages.length) {
|
|
1001
|
+
state.exhausted = true;
|
|
1002
|
+
}
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
state.exhausted = true;
|
|
1007
|
+
return false;
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (!state.started) {
|
|
1011
|
+
return findReplayStart();
|
|
1012
|
+
}
|
|
1013
|
+
if (
|
|
1014
|
+
state.index < 0
|
|
1015
|
+
|| state.index >= state.messages.length
|
|
1016
|
+
) {
|
|
1017
|
+
state.exhausted = true;
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const message = state.messages[state.index];
|
|
1022
|
+
if (message.role !== role || message.kind !== kind) {
|
|
1023
|
+
state.exhausted = true;
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const remaining = message.text.slice(state.offset);
|
|
1028
|
+
if (!remaining.startsWith(chunk)) {
|
|
1029
|
+
state.exhausted = true;
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
state.offset += chunk.length;
|
|
1034
|
+
if (state.offset >= message.text.length) {
|
|
1035
|
+
state.index += 1;
|
|
1036
|
+
state.offset = 0;
|
|
1037
|
+
}
|
|
1038
|
+
if (state.index >= state.messages.length) {
|
|
1039
|
+
state.exhausted = true;
|
|
1040
|
+
}
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
944
1044
|
function normalizePersistedMessage(message = {}, fallbackOrder = 0) {
|
|
945
1045
|
const nextMessage = cloneSerializable(message, {}) || {};
|
|
946
1046
|
nextMessage.id = typeof nextMessage.id === 'string'
|
|
@@ -1479,6 +1579,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1479
1579
|
syntheticStreams: new Map(),
|
|
1480
1580
|
syntheticStreamTurn: 0,
|
|
1481
1581
|
pendingUserEcho: null,
|
|
1582
|
+
restoreReplay: null,
|
|
1482
1583
|
currentModeId,
|
|
1483
1584
|
availableModes,
|
|
1484
1585
|
availableCommands,
|
|
@@ -1742,6 +1843,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1742
1843
|
usage: meta.usage || null,
|
|
1743
1844
|
terminals: meta.terminals || []
|
|
1744
1845
|
});
|
|
1846
|
+
tab.restoreReplay = createRestoreReplayState(meta.messages || []);
|
|
1745
1847
|
tab.status = 'restoring';
|
|
1746
1848
|
tab.busy = true;
|
|
1747
1849
|
|
|
@@ -1777,11 +1879,13 @@ class AcpRuntime extends EventEmitter {
|
|
|
1777
1879
|
tab.configOptions,
|
|
1778
1880
|
response?.models
|
|
1779
1881
|
);
|
|
1882
|
+
tab.restoreReplay = null;
|
|
1780
1883
|
tab.status = 'ready';
|
|
1781
1884
|
tab.busy = false;
|
|
1782
1885
|
tab.errorMessage = '';
|
|
1783
1886
|
return this.serializeTab(tab);
|
|
1784
1887
|
} catch (error) {
|
|
1888
|
+
tab.restoreReplay = null;
|
|
1785
1889
|
this.tabs.delete(tab.id);
|
|
1786
1890
|
this.sessionToTabId.delete(tab.acpSessionId);
|
|
1787
1891
|
throw error;
|
|
@@ -2349,6 +2453,9 @@ class AcpRuntime extends EventEmitter {
|
|
|
2349
2453
|
if (role === 'user' && kind === 'message' && this.#consumeUserEcho(tab, text)) {
|
|
2350
2454
|
return;
|
|
2351
2455
|
}
|
|
2456
|
+
if (consumeRestoredMessageReplay(tab.restoreReplay, role, kind, text)) {
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2352
2459
|
const streamKey = this.#getStreamKey(tab, update, role, kind);
|
|
2353
2460
|
const last = tab.messages[tab.messages.length - 1] || null;
|
|
2354
2461
|
|
|
@@ -3147,6 +3254,32 @@ export class AcpManager {
|
|
|
3147
3254
|
};
|
|
3148
3255
|
}
|
|
3149
3256
|
|
|
3257
|
+
async listInventory() {
|
|
3258
|
+
return {
|
|
3259
|
+
restoring: this.restoring,
|
|
3260
|
+
tabs: Array.from(this.tabs.values()).map((entry) => {
|
|
3261
|
+
const serialized = entry.serialize();
|
|
3262
|
+
return {
|
|
3263
|
+
id: serialized.id,
|
|
3264
|
+
runtimeId: serialized.runtimeId,
|
|
3265
|
+
runtimeKey: serialized.runtimeKey,
|
|
3266
|
+
acpSessionId: serialized.acpSessionId,
|
|
3267
|
+
agentId: serialized.agentId,
|
|
3268
|
+
agentLabel: serialized.agentLabel,
|
|
3269
|
+
commandLabel: serialized.commandLabel,
|
|
3270
|
+
title: serialized.title,
|
|
3271
|
+
terminalSessionId: serialized.terminalSessionId,
|
|
3272
|
+
cwd: serialized.cwd,
|
|
3273
|
+
createdAt: serialized.createdAt,
|
|
3274
|
+
status: serialized.status,
|
|
3275
|
+
busy: serialized.busy,
|
|
3276
|
+
errorMessage: serialized.errorMessage,
|
|
3277
|
+
currentModeId: serialized.currentModeId
|
|
3278
|
+
};
|
|
3279
|
+
})
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3150
3283
|
async createTab(options) {
|
|
3151
3284
|
await this.ensureConfigsLoaded();
|
|
3152
3285
|
const definition = this.definitions.find(
|
package/src/persistence.mjs
CHANGED
|
@@ -37,6 +37,7 @@ export const saveSession = async (id, data) => {
|
|
|
37
37
|
createdAt: data.createdAt,
|
|
38
38
|
// Editor State
|
|
39
39
|
editorState: data.editorState || {},
|
|
40
|
+
workspaceState: data.editorState || {},
|
|
40
41
|
executions: data.executions || []
|
|
41
42
|
};
|
|
42
43
|
await fs.writeFile(filePath, JSON.stringify(serializable, null, 2));
|
package/src/server.mjs
CHANGED
|
@@ -216,8 +216,11 @@ router.all('/api/heartbeat', async (ctx) => {
|
|
|
216
216
|
const { cols, rows } = update.resize;
|
|
217
217
|
if (cols && rows) session.resize(cols, rows);
|
|
218
218
|
}
|
|
219
|
-
if (update.editorState) {
|
|
220
|
-
terminalManager.updateSessionState(session.id, {
|
|
219
|
+
if (update.workspaceState || update.editorState) {
|
|
220
|
+
terminalManager.updateSessionState(session.id, {
|
|
221
|
+
workspaceState: update.workspaceState,
|
|
222
|
+
editorState: update.editorState
|
|
223
|
+
});
|
|
221
224
|
}
|
|
222
225
|
if (update.fileWrites) {
|
|
223
226
|
for (const file of update.fileWrites) {
|
|
@@ -235,6 +238,7 @@ router.all('/api/heartbeat', async (ctx) => {
|
|
|
235
238
|
|
|
236
239
|
ctx.body = {
|
|
237
240
|
sessions: terminalManager.listSessions(),
|
|
241
|
+
agents: await acpManager.listInventory(),
|
|
238
242
|
system: systemMonitor.getStats(),
|
|
239
243
|
runtime: {
|
|
240
244
|
bootId: SERVER_BOOT_ID
|
package/src/terminal-manager.mjs
CHANGED
|
@@ -68,6 +68,59 @@ function clearBashPromptEnv(env) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function uniqueStringList(values) {
|
|
72
|
+
if (!Array.isArray(values)) return [];
|
|
73
|
+
return Array.from(new Set(
|
|
74
|
+
values.filter(
|
|
75
|
+
(value) => typeof value === 'string' && value.length > 0
|
|
76
|
+
)
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeWorkspaceState(input = {}, fallback = {}) {
|
|
81
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
82
|
+
const base = fallback && typeof fallback === 'object' ? fallback : {};
|
|
83
|
+
return {
|
|
84
|
+
updatedAt: Number.isFinite(source.updatedAt)
|
|
85
|
+
? source.updatedAt
|
|
86
|
+
: (
|
|
87
|
+
Number.isFinite(base.updatedAt)
|
|
88
|
+
? base.updatedAt
|
|
89
|
+
: 0
|
|
90
|
+
),
|
|
91
|
+
updatedBy: typeof source.updatedBy === 'string'
|
|
92
|
+
? source.updatedBy
|
|
93
|
+
: (
|
|
94
|
+
typeof base.updatedBy === 'string'
|
|
95
|
+
? base.updatedBy
|
|
96
|
+
: ''
|
|
97
|
+
),
|
|
98
|
+
isVisible: !!source.isVisible,
|
|
99
|
+
openFiles: uniqueStringList(source.openFiles),
|
|
100
|
+
terminalDisplayMode: source.terminalDisplayMode === 'tab'
|
|
101
|
+
? 'tab'
|
|
102
|
+
: 'auto',
|
|
103
|
+
expandedPaths: uniqueStringList(source.expandedPaths)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function compareWorkspaceState(left, right) {
|
|
108
|
+
const leftUpdatedAt = Number.isFinite(left?.updatedAt) ? left.updatedAt : 0;
|
|
109
|
+
const rightUpdatedAt = Number.isFinite(right?.updatedAt)
|
|
110
|
+
? right.updatedAt
|
|
111
|
+
: 0;
|
|
112
|
+
if (leftUpdatedAt !== rightUpdatedAt) {
|
|
113
|
+
return leftUpdatedAt - rightUpdatedAt;
|
|
114
|
+
}
|
|
115
|
+
const leftUpdatedBy = typeof left?.updatedBy === 'string'
|
|
116
|
+
? left.updatedBy
|
|
117
|
+
: '';
|
|
118
|
+
const rightUpdatedBy = typeof right?.updatedBy === 'string'
|
|
119
|
+
? right.updatedBy
|
|
120
|
+
: '';
|
|
121
|
+
return leftUpdatedBy.localeCompare(rightUpdatedBy);
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
export class TerminalManager {
|
|
72
125
|
constructor() {
|
|
73
126
|
this.sessions = new Map();
|
|
@@ -228,7 +281,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
228
281
|
removeOnExit: options.removeOnExit !== false,
|
|
229
282
|
enableAiHijack: options.enableAiHijack !== false,
|
|
230
283
|
enableTitlePolling: options.enableTitlePolling !== false,
|
|
231
|
-
editorState: options.editorState,
|
|
284
|
+
editorState: normalizeWorkspaceState(options.editorState),
|
|
232
285
|
executions: options.executions
|
|
233
286
|
});
|
|
234
287
|
|
|
@@ -271,7 +324,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
271
324
|
rows: restoredData?.rows,
|
|
272
325
|
createdAt: restoredData?.createdAt,
|
|
273
326
|
title: restoredData?.title,
|
|
274
|
-
editorState: restoredData?.editorState,
|
|
327
|
+
editorState: restoredData?.workspaceState || restoredData?.editorState,
|
|
275
328
|
executions: restoredData?.executions,
|
|
276
329
|
restoreSnapshot: Boolean(restoredData),
|
|
277
330
|
persistent: true,
|
|
@@ -326,9 +379,17 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
326
379
|
updateSessionState(id, data) {
|
|
327
380
|
const session = this.sessions.get(id);
|
|
328
381
|
if (session) {
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
382
|
+
const nextWorkspaceState = data.workspaceState || data.editorState;
|
|
383
|
+
if (nextWorkspaceState) {
|
|
384
|
+
const normalized = normalizeWorkspaceState(
|
|
385
|
+
nextWorkspaceState,
|
|
386
|
+
session.editorState
|
|
387
|
+
);
|
|
388
|
+
if (
|
|
389
|
+
compareWorkspaceState(normalized, session.editorState) > 0
|
|
390
|
+
) {
|
|
391
|
+
session.editorState = normalized;
|
|
392
|
+
}
|
|
332
393
|
}
|
|
333
394
|
if (session.persistent) {
|
|
334
395
|
this.saveSessionState(session);
|
|
@@ -418,6 +479,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
418
479
|
exitStatus: s.exitStatus || null,
|
|
419
480
|
managed: s.managed || null,
|
|
420
481
|
editorState: s.editorState,
|
|
482
|
+
workspaceState: s.editorState,
|
|
421
483
|
executions: s.executions
|
|
422
484
|
}));
|
|
423
485
|
}
|