tabminal 3.0.4 → 3.0.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabminal",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
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": {
package/public/app.js CHANGED
@@ -170,6 +170,14 @@ function isCompactWorkspaceMode() {
170
170
  return !!window.__tabminalCompactWorkspaceMode;
171
171
  }
172
172
 
173
+ function isCompactTerminalTabsMode() {
174
+ return !!window.__tabminalCompactTerminalTabsMode;
175
+ }
176
+
177
+ function isForcedTerminalWorkspaceMode() {
178
+ return isCompactWorkspaceMode() || isCompactTerminalTabsMode();
179
+ }
180
+
173
181
  function getTerminalFontSize() {
174
182
  return IS_MOBILE ? 14 : 12;
175
183
  }
@@ -286,6 +294,13 @@ if (sidebarToggle && sidebar && sidebarOverlay) {
286
294
  sidebarOverlay.classList.remove('open');
287
295
  };
288
296
 
297
+ sidebarToggle.addEventListener('pointerdown', (event) => {
298
+ if (!isCompactWorkspaceMode()) {
299
+ return;
300
+ }
301
+ event.preventDefault();
302
+ });
303
+
289
304
  sidebarToggle.addEventListener('click', () => {
290
305
  sidebar.classList.toggle('open');
291
306
  sidebarOverlay.classList.toggle('open');
@@ -711,13 +726,13 @@ class EditorManager {
711
726
  }
712
727
 
713
728
  canToggleTerminalWorkspaceMode(session = this.currentSession) {
714
- return !!session && !isCompactWorkspaceMode();
729
+ return !!session && !isForcedTerminalWorkspaceMode();
715
730
  }
716
731
 
717
732
  hasCompactWorkspaceTabs(session = this.currentSession) {
718
733
  return !!session
719
734
  && (
720
- isCompactWorkspaceMode()
735
+ isForcedTerminalWorkspaceMode()
721
736
  || this.isTerminalTabPinned(session)
722
737
  );
723
738
  }
@@ -784,7 +799,7 @@ class EditorManager {
784
799
  && this.terminalWrapper.contains(activeElement)
785
800
  );
786
801
 
787
- if (isCompactWorkspaceMode()) {
802
+ if (isForcedTerminalWorkspaceMode()) {
788
803
  session.sharedWorkspaceState.terminalDisplayMode = 'auto';
789
804
  this.updateTerminalLayoutButton();
790
805
  return;
@@ -10614,12 +10629,65 @@ function renderServerControls() {
10614
10629
 
10615
10630
  // #region Notification Manager
10616
10631
  const notificationManager = new NotificationManager();
10632
+ const APP_NOTIFICATION_QUIET_MS = 30_000;
10633
+ const APP_NOTIFICATION_IDLE_MS = 3 * 60 * 1000;
10634
+ let appNotificationQuietUntil = Date.now() + APP_NOTIFICATION_QUIET_MS;
10635
+ let lastAppInteractionAt = Date.now();
10636
+
10637
+ function noteAppInteraction() {
10638
+ lastAppInteractionAt = Date.now();
10639
+ }
10640
+
10641
+ function enterAppNotificationQuietPeriod(duration = APP_NOTIFICATION_QUIET_MS) {
10642
+ appNotificationQuietUntil = Math.max(
10643
+ appNotificationQuietUntil,
10644
+ Date.now() + duration
10645
+ );
10646
+ }
10647
+
10648
+ function shouldNotifyConnectionStatus() {
10649
+ if (Date.now() < appNotificationQuietUntil) {
10650
+ return false;
10651
+ }
10652
+ if (document.visibilityState !== 'visible') {
10653
+ return false;
10654
+ }
10655
+ if (typeof document.hasFocus === 'function' && !document.hasFocus()) {
10656
+ return false;
10657
+ }
10658
+ if ((Date.now() - lastAppInteractionAt) > APP_NOTIFICATION_IDLE_MS) {
10659
+ return false;
10660
+ }
10661
+ return true;
10662
+ }
10663
+
10664
+ document.addEventListener('pointerdown', noteAppInteraction, {
10665
+ capture: true,
10666
+ passive: true
10667
+ });
10668
+ document.addEventListener('touchstart', noteAppInteraction, {
10669
+ capture: true,
10670
+ passive: true
10671
+ });
10672
+ document.addEventListener('keydown', noteAppInteraction, {
10673
+ capture: true
10674
+ });
10675
+ window.addEventListener('focus', () => {
10676
+ noteAppInteraction();
10677
+ enterAppNotificationQuietPeriod();
10678
+ });
10679
+ window.addEventListener('pageshow', () => {
10680
+ noteAppInteraction();
10681
+ enterAppNotificationQuietPeriod();
10682
+ });
10617
10683
 
10618
10684
  document.addEventListener('click', () => {
10619
10685
  notificationManager.requestPermission();
10620
10686
  }, { once: true, capture: true });
10621
10687
  document.addEventListener('visibilitychange', () => {
10622
10688
  if (document.visibilityState === 'visible') {
10689
+ noteAppInteraction();
10690
+ enterAppNotificationQuietPeriod();
10623
10691
  clearVisibleAttentionState();
10624
10692
  }
10625
10693
  });
@@ -10661,18 +10729,20 @@ function setStatus(server, status) {
10661
10729
  statusMemory.set(server.id, status);
10662
10730
  server.connectionStatus = status;
10663
10731
  renderServerControls();
10664
-
10665
- const activeServer = getActiveServer();
10666
- if (!activeServer || activeServer.id !== server.id) return;
10667
10732
  const hostName = getDisplayHost(server);
10668
10733
  const target = hostName || 'host';
10734
+ const shouldNotify = shouldNotifyConnectionStatus();
10669
10735
 
10670
- if (status === 'reconnecting') {
10736
+ if (status === 'reconnecting' && shouldNotify) {
10671
10737
  alert(`Lost connection to ${target}. Reconnecting...`, {
10672
10738
  type: 'warning',
10673
10739
  title: 'Connection'
10674
10740
  });
10675
- } else if (status === 'connected' && prevStatus === 'reconnecting') {
10741
+ } else if (
10742
+ status === 'connected'
10743
+ && prevStatus === 'reconnecting'
10744
+ && shouldNotify
10745
+ ) {
10676
10746
  alert(`Connection to ${target} restored.`, {
10677
10747
  type: 'success',
10678
10748
  title: 'Connection'
@@ -10682,7 +10752,7 @@ function setStatus(server, status) {
10682
10752
  type: 'error',
10683
10753
  title: 'Connection'
10684
10754
  });
10685
- } else if (status === 'connected' && !prevStatus) {
10755
+ } else if (status === 'connected' && !prevStatus && shouldNotify) {
10686
10756
  alert(`Connected to ${target}.`, {
10687
10757
  type: 'success',
10688
10758
  title: 'Connection'
@@ -10809,7 +10879,7 @@ window.addEventListener('tabminal:layout-modechange', () => {
10809
10879
  && terminalEl.contains(activeElement)
10810
10880
  );
10811
10881
 
10812
- if (isCompactWorkspaceMode()) {
10882
+ if (isForcedTerminalWorkspaceMode()) {
10813
10883
  if (terminalHasFocus) {
10814
10884
  session.workspaceState.activeTabKey = TERMINAL_WORKSPACE_TAB_KEY;
10815
10885
  session.saveState();
package/public/index.html CHANGED
@@ -14,26 +14,37 @@
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
- const COMPACT_WORKSPACE_MAX_LANDSCAPE_WIDTH = 950;
17
+ const COMPACT_TERMINAL_TAB_MAX_WIDTH = 1408;
18
+ // Temporarily disabled short-landscape fallback. Pixel Fold and
19
+ // similar devices can oscillate around the keyboard-driven viewport
20
+ // boundary, which makes compact mode flap during sidebar animation.
21
+ // const COMPACT_WORKSPACE_MAX_LANDSCAPE_WIDTH = 950;
22
+ // const COMPACT_WORKSPACE_ENTER_MAX_SHORT_HEIGHT = 500;
23
+ // const COMPACT_WORKSPACE_EXIT_MAX_SHORT_HEIGHT = 540;
24
+ const SIDEBAR_LAYOUT_PAUSE_MS = 360;
25
+ let layoutLoopPausedUntil = 0;
26
+ let frozenLayoutViewport = null;
27
+ let sidebarLayoutThawTimer = 0;
19
28
 
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
- );
29
+ function shouldUseCompactWorkspaceMode(
30
+ width,
31
+ height,
32
+ previousCompactWorkspaceMode = false
33
+ ) {
34
+ // Keep the extra args to preserve the old signature for possible
35
+ // re-enable of the short-landscape fallback above.
36
+ void height;
37
+ void previousCompactWorkspaceMode;
38
+ return width <= COMPACT_WORKSPACE_MAX_WIDTH;
26
39
  }
27
40
 
28
- // Mobile Layout Manager
29
- function updateLayout() {
41
+ function readLayoutViewport() {
30
42
  const vv = window.visualViewport;
31
43
  let width = window.innerWidth;
32
44
  let height = window.innerHeight;
33
45
  let offsetTop = 0;
34
46
  let offsetLeft = 0;
35
47
 
36
- // Sanity check for iPadOS PWA bug where vv.height can be negative with physical keyboard
37
48
  if (vv && vv.height > 100) {
38
49
  width = vv.width;
39
50
  height = vv.height;
@@ -41,18 +52,43 @@
41
52
  offsetLeft = vv.offsetLeft;
42
53
  }
43
54
 
55
+ return {
56
+ width,
57
+ height,
58
+ offsetTop,
59
+ offsetLeft
60
+ };
61
+ }
62
+
63
+ // Mobile Layout Manager
64
+ function updateLayout() {
65
+ const layoutViewport = frozenLayoutViewport || readLayoutViewport();
66
+ const {
67
+ width,
68
+ height,
69
+ offsetTop,
70
+ offsetLeft
71
+ } = layoutViewport;
72
+
44
73
  document.documentElement.style.setProperty('--app-width', `${width}px`);
45
74
  document.documentElement.style.setProperty('--app-height', `${height}px`);
46
75
  document.documentElement.style.setProperty('--app-top', `${offsetTop}px`);
47
76
  document.documentElement.style.setProperty('--app-left', `${offsetLeft}px`);
48
77
 
78
+ const previousCompactWorkspaceMode =
79
+ window.__tabminalCompactWorkspaceMode;
80
+ const previousCompactTerminalTabsMode =
81
+ window.__tabminalCompactTerminalTabsMode;
49
82
  const compactWorkspaceMode = shouldUseCompactWorkspaceMode(
50
83
  width,
51
- height
84
+ height,
85
+ previousCompactWorkspaceMode
52
86
  );
53
- const previousCompactWorkspaceMode =
54
- window.__tabminalCompactWorkspaceMode;
87
+ const compactTerminalTabsMode =
88
+ width <= COMPACT_TERMINAL_TAB_MAX_WIDTH;
55
89
  window.__tabminalCompactWorkspaceMode = compactWorkspaceMode;
90
+ window.__tabminalCompactTerminalTabsMode =
91
+ compactTerminalTabsMode;
56
92
 
57
93
  if (document.body) {
58
94
  window.scrollTo(0, 0);
@@ -83,17 +119,56 @@
83
119
  }
84
120
 
85
121
  if (
86
- previousCompactWorkspaceMode !== undefined
87
- && previousCompactWorkspaceMode !== compactWorkspaceMode
122
+ (
123
+ previousCompactWorkspaceMode !== undefined
124
+ && previousCompactWorkspaceMode !== compactWorkspaceMode
125
+ )
126
+ || (
127
+ previousCompactTerminalTabsMode !== undefined
128
+ && previousCompactTerminalTabsMode
129
+ !== compactTerminalTabsMode
130
+ )
88
131
  ) {
89
132
  window.dispatchEvent(
90
133
  new CustomEvent('tabminal:layout-modechange', {
91
- detail: { compactWorkspaceMode }
134
+ detail: {
135
+ compactWorkspaceMode,
136
+ compactTerminalTabsMode
137
+ }
92
138
  })
93
139
  );
94
140
  }
95
141
  }
96
142
 
143
+ function pauseLayoutLoop(duration = SIDEBAR_LAYOUT_PAUSE_MS) {
144
+ const now = performance.now();
145
+ layoutLoopPausedUntil = Math.max(
146
+ layoutLoopPausedUntil,
147
+ now + duration
148
+ );
149
+ }
150
+
151
+ function freezeLayoutViewport() {
152
+ frozenLayoutViewport = readLayoutViewport();
153
+ if (sidebarLayoutThawTimer) {
154
+ clearTimeout(sidebarLayoutThawTimer);
155
+ sidebarLayoutThawTimer = 0;
156
+ }
157
+ }
158
+
159
+ function thawLayoutViewport() {
160
+ if (sidebarLayoutThawTimer) {
161
+ clearTimeout(sidebarLayoutThawTimer);
162
+ }
163
+ sidebarLayoutThawTimer = window.setTimeout(() => {
164
+ frozenLayoutViewport = null;
165
+ sidebarLayoutThawTimer = 0;
166
+ updateLayout();
167
+ }, SIDEBAR_LAYOUT_PAUSE_MS);
168
+ }
169
+
170
+ window.__tabminalPauseLayoutLoop = pauseLayoutLoop;
171
+
97
172
  if (window.visualViewport) {
98
173
  window.visualViewport.addEventListener('resize', updateLayout);
99
174
  window.visualViewport.addEventListener('scroll', updateLayout);
@@ -103,13 +178,39 @@
103
178
 
104
179
  // High-performance Layout Loop (60fps)
105
180
  function layoutLoop() {
106
- updateLayout();
181
+ if (performance.now() >= layoutLoopPausedUntil) {
182
+ updateLayout();
183
+ }
107
184
  requestAnimationFrame(layoutLoop);
108
185
  }
109
186
  layoutLoop();
110
187
 
111
188
  // Initial triggers
112
189
  document.addEventListener('DOMContentLoaded', updateLayout);
190
+ document.addEventListener('DOMContentLoaded', () => {
191
+ const sidebar = document.getElementById('sidebar');
192
+ if (!sidebar) {
193
+ return;
194
+ }
195
+ let sidebarOpen = sidebar.classList.contains('open');
196
+ const observer = new MutationObserver(() => {
197
+ const isOpen = sidebar.classList.contains('open');
198
+ if (isOpen === sidebarOpen) {
199
+ return;
200
+ }
201
+ sidebarOpen = isOpen;
202
+ pauseLayoutLoop();
203
+ if (isOpen) {
204
+ freezeLayoutViewport();
205
+ } else {
206
+ thawLayoutViewport();
207
+ }
208
+ });
209
+ observer.observe(sidebar, {
210
+ attributes: true,
211
+ attributeFilter: ['class']
212
+ });
213
+ });
113
214
  window.addEventListener('load', updateLayout);
114
215
 
115
216
  // 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
  }