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/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(width, height) {
21
- return width <= COMPACT_WORKSPACE_MAX_WIDTH
22
- || (
23
- width <= COMPACT_WORKSPACE_MAX_LANDSCAPE_WIDTH
24
- && height <= COMPACT_WORKSPACE_MAX_SHORT_HEIGHT
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
- updateLayout();
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
@@ -2241,7 +2241,7 @@ kbd {
2241
2241
  min-width: 0;
2242
2242
  }
2243
2243
 
2244
- @media (max-width: 767px) {
2244
+ @media (max-width: 1408px) {
2245
2245
  .agent-panel-commands {
2246
2246
  display: none !important;
2247
2247
  }
@@ -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(
@@ -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, { editorState: update.editorState });
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
@@ -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
- // console.log(`[Manager] Updating session ${id} state:`, JSON.stringify(data));
330
- if (data.editorState) {
331
- session.editorState = { ...session.editorState, ...data.editorState };
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
  }