@gen3/workspaces 0.12.51 → 0.12.53

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.
Files changed (59) hide show
  1. package/dist/dts/components/HorizontalAccordian.js +2 -2
  2. package/dist/dts/components/HorizontalAccordian.js.map +1 -1
  3. package/dist/dts/components/MicroContainerReduxPanel.d.ts.map +1 -1
  4. package/dist/dts/components/MicroContainerReduxPanel.js +13 -12
  5. package/dist/dts/components/MicroContainerReduxPanel.js.map +1 -1
  6. package/dist/dts/components/TierSelectorLanding.d.ts.map +1 -1
  7. package/dist/dts/components/TierSelectorLanding.js +5 -4
  8. package/dist/dts/components/TierSelectorLanding.js.map +1 -1
  9. package/dist/dts/components/UpgradeActionsPanel.d.ts.map +1 -1
  10. package/dist/dts/components/UpgradeActionsPanel.js +4 -4
  11. package/dist/dts/components/UpgradeActionsPanel.js.map +1 -1
  12. package/dist/dts/components/WorkspaceTierCard.d.ts +1 -1
  13. package/dist/dts/components/WorkspaceTierCard.d.ts.map +1 -1
  14. package/dist/dts/components/WorkspaceTierCard.js +2 -2
  15. package/dist/dts/components/WorkspaceTierCard.js.map +1 -1
  16. package/dist/dts/components/WorkspaceTierCard.stories.d.ts +1 -1
  17. package/dist/dts/components/types.d.ts +1 -0
  18. package/dist/dts/components/types.d.ts.map +1 -1
  19. package/dist/dts/core/jegGatewayApi.d.ts +8 -8
  20. package/dist/dts/providers/MicroContainerReduxProvider.d.ts +2 -1
  21. package/dist/dts/providers/MicroContainerReduxProvider.d.ts.map +1 -1
  22. package/dist/dts/providers/MicroContainerReduxProvider.js +2 -2
  23. package/dist/dts/providers/MicroContainerReduxProvider.js.map +1 -1
  24. package/dist/dts/providers/useJEGWorkspaceResourceMonitor.d.ts +1 -1
  25. package/dist/dts/providers/useJEGWorkspaceResourceMonitor.d.ts.map +1 -1
  26. package/dist/dts/providers/useJEGWorkspaceResourceMonitor.js +5 -3
  27. package/dist/dts/providers/useJEGWorkspaceResourceMonitor.js.map +1 -1
  28. package/dist/dts/server/workspaceAssetsHandler.d.ts.map +1 -1
  29. package/dist/dts/server/workspaceAssetsHandler.js +3 -0
  30. package/dist/dts/server/workspaceAssetsHandler.js.map +1 -1
  31. package/dist/dts/types.d.ts +1 -1
  32. package/dist/dts/types.d.ts.map +1 -1
  33. package/dist/dts/workspace/WorkspaceCenter.d.ts.map +1 -1
  34. package/dist/dts/workspace/WorkspaceCenter.js +3 -2
  35. package/dist/dts/workspace/WorkspaceCenter.js.map +1 -1
  36. package/dist/dts/workspace/WorkspaceLayout/SettingsPanel.d.ts.map +1 -1
  37. package/dist/dts/workspace/WorkspaceLayout/SettingsPanel.js +1 -2
  38. package/dist/dts/workspace/WorkspaceLayout/SettingsPanel.js.map +1 -1
  39. package/dist/dts/workspace/WorkspaceLayout/WorkspaceToolbar.d.ts.map +1 -1
  40. package/dist/dts/workspace/WorkspaceLayout/WorkspaceToolbar.js +2 -2
  41. package/dist/dts/workspace/WorkspaceLayout/WorkspaceToolbar.js.map +1 -1
  42. package/dist/dts/workspace/tiers/FreeWorkspace.d.ts.map +1 -1
  43. package/dist/dts/workspace/tiers/FreeWorkspace.js +29 -18
  44. package/dist/dts/workspace/tiers/FreeWorkspace.js.map +1 -1
  45. package/dist/dts/workspace/tiers/RemoteComputeWorkspace.d.ts.map +1 -1
  46. package/dist/dts/workspace/tiers/RemoteComputeWorkspace.js +31 -19
  47. package/dist/dts/workspace/tiers/RemoteComputeWorkspace.js.map +1 -1
  48. package/dist/dts/workspace/tiers/types.d.ts +2 -0
  49. package/dist/dts/workspace/tiers/types.d.ts.map +1 -1
  50. package/dist/index/cjs/index.js +132 -114
  51. package/dist/index/cjs/index.js.map +1 -1
  52. package/dist/index/esm/index.js +133 -115
  53. package/dist/index/esm/index.js.map +1 -1
  54. package/dist/index.d.ts +4 -1
  55. package/dist/server/cjs/server.js +4 -1
  56. package/dist/server/cjs/server.js.map +1 -1
  57. package/dist/server/esm/server.js +4 -1
  58. package/dist/server/esm/server.js.map +1 -1
  59. package/package.json +10 -10
@@ -1,18 +1,18 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import React, { useCallback, useMemo, useState, useRef, useEffect, createContext, useImperativeHandle, forwardRef } from 'react';
3
3
  import { mergeDefaultTailwindClassnames, ACTIVITY_CHANNEL, convertSecondsToMilliseconds, InfoRolloverButton, OverflowTooltippedLabel, PanelErrorBoundary, ProtectedContent, FixedNavPageLayout, ContentSource, getNavPageLayoutPropsFromConfig } from '@gen3/frontend';
4
- import { Card, Stack, Badge, Text, List, Group, Tooltip, Button, LoadingOverlay, Alert, Loader, Divider, Select, ActionIcon, TextInput, Textarea, Table, Scroller, Tabs, Center } from '@mantine/core';
4
+ import { Card, Stack, Badge, Text, List, Group, Tooltip, Button, Container, SimpleGrid, LoadingOverlay, Alert, Loader, Divider, Select, ActionIcon, TextInput, Textarea, Table, Scroller, Tabs, Center } from '@mantine/core';
5
5
  import { Icon } from '@iconify-icon/react';
6
+ import { useInterval, useElementSize, useDisclosure, useResizeObserver } from '@mantine/hooks';
6
7
  import { GEN3_API, GEN3_WORKSPACE_API, gen3Api, useCoreSelector, selectJEGActiveWorkspaceStatus, useCoreDispatch, setJEGActiveWorkspaceStatus, WorkspaceStatus, setJEGRequestedWorkspaceStatus, RequestedWorkspaceStatus, setJEGActiveWorkspaceId, isFetchBaseQueryError, selectJEGRequestedWorkspaceStatus, selectJEGRequestedWorkspaceStatusTimestamp, isTimeGreaterThan, formatUptimeInMinutes, selectJEGKernelIds, removeManyJEGActiveKernels, upsertManyJEGActiveKernels, normalizeRtkError, selectAllJEGKernels, addJEGActiveKernel, selectWorkspaceTier, setWorkspaceTier, selectWorkspaceFullscreen, setWorkspaceFullscreen, useGetDictionaryFromUrlQuery, useUserAuth, GEN3_COMMONS_NAME } from '@gen3/core';
7
8
  import { useDeepCompareEffect } from 'use-deep-compare';
8
9
  import { notifications } from '@mantine/notifications';
9
- import { useElementSize, useDisclosure, useResizeObserver } from '@mantine/hooks';
10
10
  import { IoSettingsSharp } from 'react-icons/io5';
11
11
  import { BsFillInfoCircleFill } from 'react-icons/bs';
12
12
  import { MdKeyboardArrowDown, MdKeyboardArrowRight } from 'react-icons/md';
13
13
  import { merge } from 'lodash';
14
14
 
15
- const WorkspaceTierCard = ({ label, description, tier, features = [], tooltip = 'Launch workspace', buttonLabel = 'Launch', onSelectTier, baseColor })=>{
15
+ const WorkspaceTierCard = ({ label, description, tier, tierLabel = undefined, features = [], tooltip = 'Launch workspace', buttonLabel = 'Launch', onSelectTier, baseColor })=>{
16
16
  const handleClick = useCallback(()=>onSelectTier(tier), [
17
17
  onSelectTier,
18
18
  tier
@@ -47,7 +47,7 @@ const WorkspaceTierCard = ({ label, description, tier, features = [], tooltip =
47
47
  color: "primary.4",
48
48
  size: "lg",
49
49
  fw: 500,
50
- children: tier !== 'free' ? 'paid' : 'free'
50
+ children: tierLabel ?? (tier === 'remote' ? 'paid' : 'free')
51
51
  })
52
52
  })
53
53
  }),
@@ -113,10 +113,10 @@ const WorkspaceTierCard = ({ label, description, tier, features = [], tooltip =
113
113
  };
114
114
 
115
115
  const DEFAULT_CLASSNAMES = {
116
- root: 'mx-auto w-full max-w-7xl p-6 md:p-10',
117
- background: 'relative overflow-hidden md:p-12',
116
+ root: 'mx-auto w-full h-full overflow-auto max-w-7xl p-6 sm:p-2 md:p-4 lg:p-8',
117
+ background: 'relative overflow-hidden sm:p-2 md:p-4 lg:p-8',
118
118
  label: 'text-md uppercase tracking-[0.28em] text-primary',
119
- description: 'text-4xl font-black leading-tight text-primary md:text-3xl sm:text-2xl mt-4 mb-4',
119
+ description: 'font-black leading-tight text-primary md:text-3xl sm:text-2xl xs:text-lg mt-4 mb-4',
120
120
  additionalDescription: 'max-w-2xl text-md font-medium text-base-contrast',
121
121
  button: 'mt-10 flex w-full items-center justify-center rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-contrast shadow-sm hover:bg-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary'
122
122
  };
@@ -127,36 +127,43 @@ const TierSelectorLanding = ({ cards, onSelectTier, classNames, label = 'Gen3 Ju
127
127
  const mergedClassnames = useMemo(()=>mergeDefaultTailwindClassnames(DEFAULT_CLASSNAMES, classNames ?? {}), [
128
128
  classNames
129
129
  ]);
130
- return /*#__PURE__*/ jsx("div", {
130
+ return /*#__PURE__*/ jsx(Container, {
131
+ size: "xl",
131
132
  className: mergedClassnames.root,
132
- children: /*#__PURE__*/ jsxs("div", {
133
+ children: /*#__PURE__*/ jsxs(Stack, {
134
+ gap: "lg",
133
135
  className: mergedClassnames.background,
134
136
  children: [
135
- /*#__PURE__*/ jsx("div", {
136
- className: "mb-10 flex flex-col gap-6 md:flex-row md:items-end md:justify-between",
137
- children: /*#__PURE__*/ jsxs("div", {
138
- children: [
139
- /*#__PURE__*/ jsx("p", {
140
- className: "text-md uppercase tracking-[0.28em] text-primary",
141
- children: label
142
- }),
143
- /*#__PURE__*/ jsx("p", {
144
- className: mergedClassnames.description,
145
- children: description
146
- }),
147
- additionalDescriptions.map((desc)=>/*#__PURE__*/ jsx("p", {
148
- className: mergedClassnames.additionalDescription,
149
- children: desc
150
- }, desc.slice(0, 20)))
151
- ]
152
- })
137
+ /*#__PURE__*/ jsxs(Stack, {
138
+ gap: "xs",
139
+ children: [
140
+ /*#__PURE__*/ jsx(Text, {
141
+ className: mergedClassnames.label,
142
+ children: label
143
+ }),
144
+ /*#__PURE__*/ jsx(Text, {
145
+ className: mergedClassnames.description,
146
+ children: description
147
+ }),
148
+ additionalDescriptions.map((desc)=>/*#__PURE__*/ jsx(Text, {
149
+ className: mergedClassnames.additionalDescription,
150
+ children: desc
151
+ }, desc.slice(0, 20)))
152
+ ]
153
153
  }),
154
- /*#__PURE__*/ jsx("div", {
155
- className: "grid gap-5 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1",
154
+ /*#__PURE__*/ jsx(SimpleGrid, {
155
+ cols: {
156
+ base: 2,
157
+ sm: 2,
158
+ md: 2,
159
+ lg: 3
160
+ },
161
+ spacing: "md",
156
162
  role: "list",
157
163
  "aria-label": "Workspace tier selection",
158
164
  children: Object.values(cards).map((card)=>/*#__PURE__*/ jsx(WorkspaceTierCard, {
159
165
  tier: card.tier,
166
+ tierLabel: card.tierLabel,
160
167
  onSelectTier: onSelectTier,
161
168
  label: card.label,
162
169
  description: card.description,
@@ -196,49 +203,60 @@ const FreeWorkspace = ({ assetBaseUrl = '/api/workspace-assets/free', onReady, o
196
203
  }
197
204
  };
198
205
  }, []);
199
- // Handle setting up iframe event listeners
206
+ // Handle setting up iframe event listeners for activity detection.
207
+ // Tries same-origin contentDocument access first; falls back to
208
+ // focus/blur heuristics when the iframe is cross-origin.
200
209
  useEffect(()=>{
201
210
  if (!jupyterReady) return;
202
211
  const updateUserActivity = ()=>{
203
- const timestamp = Date.now();
204
212
  if (broadcastChannelRef.current) {
205
213
  broadcastChannelRef.current.postMessage({
206
214
  type: 'activity-update',
207
- timestamp
215
+ timestamp: Date.now()
208
216
  });
209
217
  }
210
218
  };
211
219
  const iframe = iframeRef.current;
212
- if (!iframe?.contentWindow) {
213
- return;
214
- }
215
- try {
216
- const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
217
- if (iframeDoc) {
218
- iframeDoc.addEventListener('mousedown', updateUserActivity);
219
- iframeDoc.addEventListener('keypress', updateUserActivity);
220
- iframeDoc.addEventListener('scroll', updateUserActivity);
221
- iframeDoc.addEventListener('click', updateUserActivity);
222
- iframeDoc.addEventListener('touchstart', updateUserActivity);
220
+ let usedDirectListeners = false;
221
+ if (iframe?.contentWindow) {
222
+ try {
223
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
224
+ if (iframeDoc) {
225
+ iframeDoc.addEventListener('mousedown', updateUserActivity);
226
+ iframeDoc.addEventListener('keypress', updateUserActivity);
227
+ iframeDoc.addEventListener('scroll', updateUserActivity);
228
+ iframeDoc.addEventListener('touchstart', updateUserActivity);
229
+ usedDirectListeners = true;
230
+ }
231
+ } catch {
232
+ // cross-origin — fall through to focus/blur fallback
223
233
  }
224
- } catch (error) {
225
- console.warn('Cannot access iframe content - cross-origin restriction', error);
234
+ }
235
+ // Fallback: treat iframe receiving focus as user activity.
236
+ // This fires when the user clicks into the iframe even if we
237
+ // cannot attach listeners to its document directly.
238
+ if (!usedDirectListeners) {
239
+ window.addEventListener('blur', updateUserActivity);
240
+ window.addEventListener('focus', updateUserActivity);
226
241
  }
227
242
  return ()=>{
228
- if (iframe?.contentWindow) {
243
+ if (usedDirectListeners && iframe?.contentWindow) {
229
244
  try {
230
245
  const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
231
246
  if (iframeDoc) {
232
247
  iframeDoc.removeEventListener('mousedown', updateUserActivity);
233
248
  iframeDoc.removeEventListener('keypress', updateUserActivity);
234
249
  iframeDoc.removeEventListener('scroll', updateUserActivity);
235
- iframeDoc.removeEventListener('click', updateUserActivity);
236
250
  iframeDoc.removeEventListener('touchstart', updateUserActivity);
237
251
  }
238
252
  } catch {
239
253
  // Ignore cleanup errors
240
254
  }
241
255
  }
256
+ if (!usedDirectListeners) {
257
+ window.removeEventListener('blur', updateUserActivity);
258
+ window.removeEventListener('focus', updateUserActivity);
259
+ }
242
260
  };
243
261
  }, [
244
262
  jupyterReady
@@ -683,6 +701,7 @@ const notifyUser = (title, message, status = 0)=>{
683
701
  [1]: 'utility.2',
684
702
  [2]: 'utility.4'
685
703
  };
704
+ notifications.clean();
686
705
  notifications.show({
687
706
  title,
688
707
  message,
@@ -706,7 +725,7 @@ const workspaceShutdownAlertLimit = 30000; // 5 minutes: 5 * 60 * 1000 TODO Figu
706
725
  /**
707
726
  * Monitors resource usage.
708
727
  * Handles state changes and polls for workspace status using the JEG APIs
709
- */ const useJEGWorkspaceResourceMonitor = (workspaceId = null, monitorWorkspace)=>{
728
+ */ const useJEGWorkspaceResourceMonitor = (workspaceId = null, monitorWorkspace, startTimeLimit = MAXIMUM_START_TIME_IN_MINUTES)=>{
710
729
  // start with default polling interval of 1 second
711
730
  const [pollingInterval, setPollingInterval] = useState(convertSecondsToMilliseconds(1));
712
731
  const [error, setError] = useState(null);
@@ -870,8 +889,8 @@ const workspaceShutdownAlertLimit = 30000; // 5 minutes: 5 * 60 * 1000 TODO Figu
870
889
  activeStatus
871
890
  ]);
872
891
  useEffect(()=>{
873
- // time out if exceeding maximum start timee
874
- if (requestedStatus === RequestedWorkspaceStatus.Launch && isTimeGreaterThan(requestedStatusTimestamp, MAXIMUM_START_TIME_IN_MINUTES)) {
892
+ // time out if exceeding maximum start time
893
+ if (requestedStatus === RequestedWorkspaceStatus.Launch && isTimeGreaterThan(requestedStatusTimestamp, startTimeLimit)) {
875
894
  if (workspaceId) terminateWorkspace(encodeURIComponent(workspaceId));
876
895
  dispatch(setJEGRequestedWorkspaceStatus(RequestedWorkspaceStatus.Terminate));
877
896
  setError('Workspace failed to start. Shutting down');
@@ -882,7 +901,8 @@ const workspaceShutdownAlertLimit = 30000; // 5 minutes: 5 * 60 * 1000 TODO Figu
882
901
  dispatch,
883
902
  workspaceStatusData,
884
903
  workspaceId,
885
- terminateWorkspace
904
+ terminateWorkspace,
905
+ startTimeLimit
886
906
  ]);
887
907
  };
888
908
 
@@ -899,10 +919,10 @@ const useMicroContainerReduxContext = ()=>{
899
919
  }
900
920
  return context;
901
921
  };
902
- function MicroContainerReduxProvider({ children, identifierTag, enabled }) {
922
+ function MicroContainerReduxProvider({ children, identifierTag, enabled, startTimeLimit = undefined }) {
903
923
  const tag = identifierTag ?? (process.env.NEXT_PUBLIC_MICRO_CONTAINER_TAG || '');
904
924
  const value = useMicroContainerRedux(tag, enabled);
905
- useJEGWorkspaceResourceMonitor(value.containerHash);
925
+ useJEGWorkspaceResourceMonitor(value.containerHash, true, startTimeLimit);
906
926
  return /*#__PURE__*/ jsx(MicroContainerReduxProviderContext.Provider, {
907
927
  value: value,
908
928
  children: children
@@ -936,19 +956,21 @@ const MicroContainerReduxPanel = ({ compact = false })=>{
936
956
  coreDispatch
937
957
  ]);
938
958
  /* ── Elapsed timer for launching state ── */ const [elapsedSec, setElapsedSec] = useState(0);
959
+ const interval = useInterval(()=>{
960
+ const elapsed = Math.floor((Date.now() - requestedStatusTimestamp) / 1000);
961
+ setElapsedSec(elapsed);
962
+ }, 1000);
939
963
  useEffect(()=>{
940
- if (status !== WorkspaceStatus.Launching) {
964
+ if (status === WorkspaceStatus.Launching) {
965
+ interval.start();
966
+ } else {
941
967
  setElapsedSec(0);
942
- return;
968
+ interval.stop();
943
969
  }
944
- const interval = Math.floor((Date.now() - requestedStatusTimestamp) / 1000);
945
- const t = setInterval(()=>{
946
- setElapsedSec(interval);
947
- }, 1000);
948
- return ()=>clearInterval(t);
970
+ return interval.stop;
949
971
  }, [
950
- requestedStatusTimestamp,
951
- status
972
+ status,
973
+ interval
952
974
  ]);
953
975
  /* ── Auto-reset after 15 s in launch-error state ── */ useEffect(()=>{
954
976
  if (status !== WorkspaceStatus.LaunchError) return;
@@ -1002,9 +1024,6 @@ const MicroContainerReduxPanel = ({ compact = false })=>{
1002
1024
  });
1003
1025
  }
1004
1026
  /* ── launching ── */ if (status === WorkspaceStatus.Launching) {
1005
- const mins = Math.floor(elapsedSec / 60);
1006
- const secs = elapsedSec % 60;
1007
- const elapsed = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
1008
1027
  return /*#__PURE__*/ jsxs("div", {
1009
1028
  role: "status",
1010
1029
  className: PanelStyle$2,
@@ -1027,7 +1046,9 @@ const MicroContainerReduxPanel = ({ compact = false })=>{
1027
1046
  className: "mt-1 text-sm text-base-darker",
1028
1047
  children: [
1029
1048
  "Elapsed: ",
1030
- elapsed
1049
+ elapsedSec,
1050
+ " ",
1051
+ elapsedSec === 1 ? 'second' : 'seconds'
1031
1052
  ]
1032
1053
  }),
1033
1054
  /*#__PURE__*/ jsx("p", {
@@ -1175,49 +1196,60 @@ const MicroContainerReduxPanel = ({ compact = false })=>{
1175
1196
  }
1176
1197
  };
1177
1198
  }, []);
1178
- // Handle setting up iframe event listeners
1199
+ // Handle setting up iframe event listeners for activity detection.
1200
+ // Tries same-origin contentDocument access first; falls back to
1201
+ // focus/blur heuristics when the iframe is cross-origin.
1179
1202
  useEffect(()=>{
1180
1203
  if (!jupyterReady) return;
1181
1204
  const updateUserActivity = ()=>{
1182
- const timestamp = Date.now();
1183
1205
  if (broadcastChannelRef.current) {
1184
1206
  broadcastChannelRef.current.postMessage({
1185
1207
  type: 'activity-update',
1186
- timestamp
1208
+ timestamp: Date.now()
1187
1209
  });
1188
1210
  }
1189
1211
  };
1190
1212
  const iframe = iframeRef.current;
1191
- if (!iframe?.contentWindow) {
1192
- return;
1193
- }
1194
- try {
1195
- const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
1196
- if (iframeDoc) {
1197
- iframeDoc.addEventListener('mousedown', updateUserActivity);
1198
- iframeDoc.addEventListener('keypress', updateUserActivity);
1199
- iframeDoc.addEventListener('scroll', updateUserActivity);
1200
- iframeDoc.addEventListener('click', updateUserActivity);
1201
- iframeDoc.addEventListener('touchstart', updateUserActivity);
1213
+ let usedDirectListeners = false;
1214
+ if (iframe?.contentWindow) {
1215
+ try {
1216
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
1217
+ if (iframeDoc) {
1218
+ iframeDoc.addEventListener('mousedown', updateUserActivity);
1219
+ iframeDoc.addEventListener('keypress', updateUserActivity);
1220
+ iframeDoc.addEventListener('scroll', updateUserActivity);
1221
+ iframeDoc.addEventListener('touchstart', updateUserActivity);
1222
+ usedDirectListeners = true;
1223
+ }
1224
+ } catch {
1225
+ // cross-origin — fall through to focus/blur fallback
1202
1226
  }
1203
- } catch (error) {
1204
- console.warn('Cannot access iframe content - cross-origin restriction', error);
1227
+ }
1228
+ // Fallback: treat iframe receiving focus as user activity.
1229
+ // This fires when the user clicks into the iframe even if we
1230
+ // cannot attach listeners to its document directly.
1231
+ if (!usedDirectListeners) {
1232
+ window.addEventListener('blur', updateUserActivity);
1233
+ window.addEventListener('focus', updateUserActivity);
1205
1234
  }
1206
1235
  return ()=>{
1207
- if (iframe?.contentWindow) {
1236
+ if (usedDirectListeners && iframe?.contentWindow) {
1208
1237
  try {
1209
1238
  const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
1210
1239
  if (iframeDoc) {
1211
1240
  iframeDoc.removeEventListener('mousedown', updateUserActivity);
1212
1241
  iframeDoc.removeEventListener('keypress', updateUserActivity);
1213
1242
  iframeDoc.removeEventListener('scroll', updateUserActivity);
1214
- iframeDoc.removeEventListener('click', updateUserActivity);
1215
1243
  iframeDoc.removeEventListener('touchstart', updateUserActivity);
1216
1244
  }
1217
1245
  } catch {
1218
1246
  // Ignore cleanup errors
1219
1247
  }
1220
1248
  }
1249
+ if (!usedDirectListeners) {
1250
+ window.removeEventListener('blur', updateUserActivity);
1251
+ window.removeEventListener('focus', updateUserActivity);
1252
+ }
1221
1253
  };
1222
1254
  }, [
1223
1255
  jupyterReady
@@ -1413,7 +1445,8 @@ const MicroContainerReduxPanel = ({ compact = false })=>{
1413
1445
  className: "min-h-0 flex-1 border-0",
1414
1446
  onLoad: ()=>{
1415
1447
  setLoading(false);
1416
- setJupyterReady(true);
1448
+ // jupyterReady is set by the polling effect once the
1449
+ // JupyterLite app reports status === 'ready'
1417
1450
  },
1418
1451
  onError: ()=>{
1419
1452
  setLoadError(true);
@@ -2152,27 +2185,11 @@ const UpgradeActionsPanel = ({ currentTier, onUpgradeToRemote, onRequestQuotaInc
2152
2185
  /*#__PURE__*/ jsxs("div", {
2153
2186
  className: "space-y-2 p-4 pt-0",
2154
2187
  children: [
2155
- /*#__PURE__*/ jsxs(Stack, {
2156
- children: [
2157
- /*#__PURE__*/ jsxs(Group, {
2158
- justify: "space-between",
2159
- wrap: "nowrap",
2160
- children: [
2161
- /*#__PURE__*/ jsx("p", {
2162
- className: "text-md font-bold text-base-contrast-max",
2163
- children: "Current Tier"
2164
- }),
2165
- /*#__PURE__*/ jsx(Badge, {
2166
- color: "primary.3",
2167
- children: currentTier
2168
- })
2169
- ]
2170
- }),
2171
- /*#__PURE__*/ jsx("p", {
2172
- className: "mt-1 text-sm text-base-contrast-max",
2173
- children: currentTier === 'free' ? 'Upgrade to remote kernels for long-running jobs and larger memory footprints.' : 'Remote compute is active. You can request expanded quota for larger workloads.'
2174
- })
2175
- ]
2188
+ /*#__PURE__*/ jsx(Stack, {
2189
+ children: /*#__PURE__*/ jsx("p", {
2190
+ className: "my-1 text-sm text-base-contrast-max",
2191
+ children: currentTier === 'free' ? 'Upgrade to remote kernels for long-running jobs and larger memory footprints.' : 'Remote compute is active. You can request expanded quota for larger workloads.'
2192
+ })
2176
2193
  }),
2177
2194
  currentTier === 'free' && /*#__PURE__*/ jsx(Button, {
2178
2195
  onClick: onUpgradeToRemote,
@@ -2239,7 +2256,7 @@ function HorizontalAccordion({ label, expanded, setExpanded, children, expandedW
2239
2256
  });
2240
2257
  const mergedClassNames = mergeDefaultTailwindClassnames(STYLING_DEFAULTS, classNames);
2241
2258
  return /*#__PURE__*/ jsx("div", {
2242
- className: `overflow-hidden shrink-0 ${mergedClassNames.root}`,
2259
+ className: `${mergedClassNames.root}`,
2243
2260
  style: {
2244
2261
  width: expanded ? expandedWidth : collapsedWidth,
2245
2262
  transition: `width ${TRANSITION_MS}ms ease`
@@ -2274,7 +2291,7 @@ function HorizontalAccordion({ label, expanded, setExpanded, children, expandedW
2274
2291
  ]
2275
2292
  }),
2276
2293
  /*#__PURE__*/ jsx("div", {
2277
- className: "overflow-hidden h-full shrink-0 px-2",
2294
+ className: "overflow-hidden px-2",
2278
2295
  style: {
2279
2296
  opacity: expanded ? 1 : 0,
2280
2297
  pointerEvents: expanded ? 'auto' : 'none',
@@ -2302,9 +2319,8 @@ const SettingsPanel = ({ showKernels, expanded, setExpanded, width = 380 })=>{
2302
2319
  setExpanded: setExpanded,
2303
2320
  rightSide: true,
2304
2321
  expandedWidth: width,
2305
- children: /*#__PURE__*/ jsxs(Stack, {
2306
- gap: "sm",
2307
- className: PanelStyle,
2322
+ children: /*#__PURE__*/ jsxs("div", {
2323
+ className: `${PanelStyle} overflow-y-scroll`,
2308
2324
  children: [
2309
2325
  showKernels && /*#__PURE__*/ jsx(KernelLifecyclePanel, {}),
2310
2326
  /*#__PURE__*/ jsx(UpgradeActionsPanel, {
@@ -2320,7 +2336,7 @@ const WorkspaceToolbar = /*#__PURE__*/ forwardRef(({ toolbarConfiguration }, ref
2320
2336
  const workspaceTier = useCoreSelector((state)=>selectWorkspaceTier(state));
2321
2337
  const workspaceStatus = useCoreSelector((state)=>selectJEGActiveWorkspaceStatus(state));
2322
2338
  const isFullscreen = useCoreSelector((state)=>selectWorkspaceFullscreen(state));
2323
- const { showStatus, showStop } = toolbarConfiguration || {};
2339
+ const { showStatus, showStop, tierLabel } = toolbarConfiguration || {};
2324
2340
  const coreDispatch = useCoreDispatch();
2325
2341
  const toggleFullscreen = useCallback(()=>{
2326
2342
  coreDispatch(setWorkspaceFullscreen(!isFullscreen));
@@ -2373,8 +2389,8 @@ const WorkspaceToolbar = /*#__PURE__*/ forwardRef(({ toolbarConfiguration }, ref
2373
2389
  children: toolbarConfiguration?.description
2374
2390
  }),
2375
2391
  /*#__PURE__*/ jsx(Badge, {
2376
- "aria-label": `Workspace tier: ${workspaceTier}`,
2377
- children: workspaceTier
2392
+ "aria-label": `Workspace tier: ${tierLabel ?? workspaceTier}`,
2393
+ children: tierLabel ?? workspaceTier
2378
2394
  })
2379
2395
  ]
2380
2396
  })
@@ -3973,7 +3989,7 @@ const WorkspaceCenter = ({ workspaces, landingPage, tierConfiguration })=>{
3973
3989
  ]);
3974
3990
  if (!workspaceTier) {
3975
3991
  return /*#__PURE__*/ jsx("div", {
3976
- className: "w-full bg-base-lightest ",
3992
+ className: "w-full h-full overflow-hidden bg-base-lightest ",
3977
3993
  children: /*#__PURE__*/ jsx(TierSelectorLanding, {
3978
3994
  cards: workspaces,
3979
3995
  onSelectTier: handleSelectTier,
@@ -3991,9 +4007,11 @@ const WorkspaceCenter = ({ workspaces, landingPage, tierConfiguration })=>{
3991
4007
  });
3992
4008
  }
3993
4009
  if (workspaceTier === 'remote') {
4010
+ const config = tierConfiguration.remote;
3994
4011
  return /*#__PURE__*/ jsx(ProtectedContent, {
3995
4012
  children: /*#__PURE__*/ jsx(MicroContainerReduxProvider, {
3996
4013
  enabled: true,
4014
+ startTimeLimit: config?.startTimeLimit,
3997
4015
  children: /*#__PURE__*/ jsx(WorkspaceLayout, {
3998
4016
  tierConfiguration: tierConfiguration.remote,
3999
4017
  children: /*#__PURE__*/ jsx(RemoteComputeWorkspace, {