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/index.html
CHANGED
|
@@ -14,15 +14,33 @@
|
|
|
14
14
|
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
|
15
15
|
<script>
|
|
16
16
|
const COMPACT_WORKSPACE_MAX_WIDTH = 767;
|
|
17
|
-
const COMPACT_WORKSPACE_MAX_SHORT_HEIGHT = 500;
|
|
18
17
|
const COMPACT_WORKSPACE_MAX_LANDSCAPE_WIDTH = 950;
|
|
18
|
+
const COMPACT_WORKSPACE_ENTER_MAX_SHORT_HEIGHT = 500;
|
|
19
|
+
const COMPACT_WORKSPACE_EXIT_MAX_SHORT_HEIGHT = 540;
|
|
20
|
+
const SIDEBAR_LAYOUT_PAUSE_MS = 360;
|
|
21
|
+
let layoutLoopPausedUntil = 0;
|
|
19
22
|
|
|
20
|
-
function shouldUseCompactWorkspaceMode(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
function shouldUseCompactWorkspaceMode(
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
previousCompactWorkspaceMode = false
|
|
27
|
+
) {
|
|
28
|
+
if (width <= COMPACT_WORKSPACE_MAX_WIDTH) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isShortLandscapeCandidate = (
|
|
33
|
+
width > height
|
|
34
|
+
&& width <= COMPACT_WORKSPACE_MAX_LANDSCAPE_WIDTH
|
|
35
|
+
);
|
|
36
|
+
if (!isShortLandscapeCandidate) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const shortHeightThreshold = previousCompactWorkspaceMode
|
|
41
|
+
? COMPACT_WORKSPACE_EXIT_MAX_SHORT_HEIGHT
|
|
42
|
+
: COMPACT_WORKSPACE_ENTER_MAX_SHORT_HEIGHT;
|
|
43
|
+
return height <= shortHeightThreshold;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
// Mobile Layout Manager
|
|
@@ -46,12 +64,13 @@
|
|
|
46
64
|
document.documentElement.style.setProperty('--app-top', `${offsetTop}px`);
|
|
47
65
|
document.documentElement.style.setProperty('--app-left', `${offsetLeft}px`);
|
|
48
66
|
|
|
67
|
+
const previousCompactWorkspaceMode =
|
|
68
|
+
window.__tabminalCompactWorkspaceMode;
|
|
49
69
|
const compactWorkspaceMode = shouldUseCompactWorkspaceMode(
|
|
50
70
|
width,
|
|
51
|
-
height
|
|
71
|
+
height,
|
|
72
|
+
previousCompactWorkspaceMode
|
|
52
73
|
);
|
|
53
|
-
const previousCompactWorkspaceMode =
|
|
54
|
-
window.__tabminalCompactWorkspaceMode;
|
|
55
74
|
window.__tabminalCompactWorkspaceMode = compactWorkspaceMode;
|
|
56
75
|
|
|
57
76
|
if (document.body) {
|
|
@@ -94,6 +113,16 @@
|
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
115
|
|
|
116
|
+
function pauseLayoutLoop(duration = SIDEBAR_LAYOUT_PAUSE_MS) {
|
|
117
|
+
const now = performance.now();
|
|
118
|
+
layoutLoopPausedUntil = Math.max(
|
|
119
|
+
layoutLoopPausedUntil,
|
|
120
|
+
now + duration
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
window.__tabminalPauseLayoutLoop = pauseLayoutLoop;
|
|
125
|
+
|
|
97
126
|
if (window.visualViewport) {
|
|
98
127
|
window.visualViewport.addEventListener('resize', updateLayout);
|
|
99
128
|
window.visualViewport.addEventListener('scroll', updateLayout);
|
|
@@ -103,13 +132,34 @@
|
|
|
103
132
|
|
|
104
133
|
// High-performance Layout Loop (60fps)
|
|
105
134
|
function layoutLoop() {
|
|
106
|
-
|
|
135
|
+
if (performance.now() >= layoutLoopPausedUntil) {
|
|
136
|
+
updateLayout();
|
|
137
|
+
}
|
|
107
138
|
requestAnimationFrame(layoutLoop);
|
|
108
139
|
}
|
|
109
140
|
layoutLoop();
|
|
110
141
|
|
|
111
142
|
// Initial triggers
|
|
112
143
|
document.addEventListener('DOMContentLoaded', updateLayout);
|
|
144
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
145
|
+
const sidebar = document.getElementById('sidebar');
|
|
146
|
+
if (!sidebar) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let sidebarOpen = sidebar.classList.contains('open');
|
|
150
|
+
const observer = new MutationObserver(() => {
|
|
151
|
+
const isOpen = sidebar.classList.contains('open');
|
|
152
|
+
if (isOpen === sidebarOpen) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
sidebarOpen = isOpen;
|
|
156
|
+
pauseLayoutLoop();
|
|
157
|
+
});
|
|
158
|
+
observer.observe(sidebar, {
|
|
159
|
+
attributes: true,
|
|
160
|
+
attributeFilter: ['class']
|
|
161
|
+
});
|
|
162
|
+
});
|
|
113
163
|
window.addEventListener('load', updateLayout);
|
|
114
164
|
|
|
115
165
|
// Enhanced Touch Interceptor with Boundary Check
|
package/public/styles.css
CHANGED
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
|
}
|