@volter/sdk 0.5.205 → 0.5.207

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
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/sdk",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.205",
5
+ "version": "0.5.207",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
@@ -487,7 +487,7 @@
487
487
  "@fortawesome/react-fontawesome": "^3.2.0",
488
488
  "@storybook/react": "10.6.0",
489
489
  "@types/node": "^22.0.0",
490
- "@volter/project": "0.5.205",
490
+ "@volter/project": "0.5.207",
491
491
  "mediabunny": "1.56.2",
492
492
  "postcss": "8.5.28",
493
493
  "rrweb": "2.1.6",
@@ -15,6 +15,7 @@ import {
15
15
  generationAccountProjection,
16
16
  ProviderCredentialTestResultSchema,
17
17
  } from '@volter/sdk/account';
18
+ import { LIMITED_VIEW_HEADER } from '@volter/sdk/session/limited-view';
18
19
  import { assertEditorServerAnswered } from '@volter/sdk/kit/editor-server-response';
19
20
  import { COLLABORATION_REMOTE_SHARE } from '@volter/sdk/kit/editor-session-attribution';
20
21
 
@@ -88,9 +89,14 @@ export function subscribeAccount(listener: () => void): () => void {
88
89
  listeners.add(listener);
89
90
  return () => listeners.delete(listener);
90
91
  }
92
+ /** A limited view has no account service and says so once (`x-volter-limited-view: unavailable`);
93
+ * asking again every 30 s only fills the page's network log with 503s. */
94
+ let accountUnavailable = false;
91
95
  export async function refreshAccount(): Promise<EditorAccountSnapshot> {
92
96
  try {
93
- const next = await responseSnapshot(await fetch('/__editor/account'), 'Account read failed');
97
+ const response = await fetch('/__editor/account');
98
+ if (response.headers.get(LIMITED_VIEW_HEADER) === 'unavailable') accountUnavailable = true;
99
+ const next = await responseSnapshot(response, 'Account read failed');
94
100
  publish(next);
95
101
  return next;
96
102
  } catch (cause) {
@@ -328,6 +334,6 @@ export function startAccountActivity(): () => void {
328
334
  } else {
329
335
  void refreshAccount().catch(() => undefined);
330
336
  }
331
- const timer = window.setInterval(() => void refreshAccount().catch(() => undefined), 30_000);
337
+ const timer = window.setInterval(() => { if (!accountUnavailable) void refreshAccount().catch(() => undefined); }, 30_000);
332
338
  return () => window.clearInterval(timer);
333
339
  }
@@ -202,6 +202,20 @@ export async function revealInFinder(targetPath: string): Promise<void> {
202
202
  assertEditorServerResponse(res, `Could not reveal ${targetPath}`);
203
203
  }
204
204
 
205
+ /**
206
+ * The file manager `revealInFinder` opens, named for the menus that offer it: Finder on a Mac,
207
+ * File Explorer on Windows (`explorer /select,`), the desktop's file manager elsewhere (`xdg-open`
208
+ * on the containing folder). `/__editor/reveal` is never shared (`NEVER_SHARE_PATHS`), so the
209
+ * person who can use it is at the host and the browser's platform is the host's — WSL aside,
210
+ * where the label is only a word.
211
+ */
212
+ export function fileManagerName(): string {
213
+ const platform = typeof navigator === 'undefined' ? '' : navigator.platform;
214
+ if (platform.includes('Mac')) return 'Finder';
215
+ if (platform.startsWith('Win')) return 'File Explorer';
216
+ return 'File Manager';
217
+ }
218
+
205
219
  /** Save a viewport thumbnail for the current project — background bookkeeping
206
220
  * for the recent-projects list, not a user-facing promise. */
207
221
  export async function saveThumbnail(dataUrl: string): Promise<boolean> {
@@ -24,7 +24,7 @@ import {
24
24
  contributedHeaderItems,
25
25
  subscribeContributedChrome,
26
26
  } from '@volter/sdk/kit/chrome-registry';
27
- import { revealInFinder } from '@volter/sdk/kit/editor-api';
27
+ import { fileManagerName, revealInFinder } from '@volter/sdk/kit/editor-api';
28
28
  import { getCurrentProject, onProjectChange } from '@volter/sdk/kit/project-manager';
29
29
  import {
30
30
  activeEditorWorkspace,
@@ -212,7 +212,7 @@ export function ProjectHeader({
212
212
  revealInFinder(projectPath);
213
213
  }}
214
214
  >
215
- Show in Finder
215
+ Show in {fileManagerName()}
216
216
  </MenuItem>
217
217
  )}
218
218
  <MenuItem
@@ -62,6 +62,12 @@ import {
62
62
  subscribeEditorLeaseView,
63
63
  } from '../editor-lease-view';
64
64
  import { useEditorStore } from '@volter/sdk/kit/editor-runtime';
65
+ import { documentPlayState, subscribeDocumentPlayState } from '@volter/sdk/kit/document-play-extension';
66
+ import {
67
+ openWorkspaceDocuments,
68
+ subscribeWorkspaceDocuments,
69
+ workspaceDocumentRegistryVersion,
70
+ } from '@volter/sdk/kit/workspace-document-registry';
65
71
  import { createHmrRegistrationGroup, type HmrRegistrationContext } from '@volter/sdk/kit/hmr-registration-group';
66
72
  import { useProjectMounts } from '../project-shape';
67
73
  import { getStorageBackend } from '@volter/sdk/kit/storage/index';
@@ -211,6 +217,19 @@ export function AssetMaterializationStatus() {
211
217
  );
212
218
  }
213
219
 
220
+ /** The open documents' own Play, as one word: playing if any plays, else paused if any is paused. */
221
+ function openDocumentsPlayState(): 'playing' | 'paused' | 'stopped' {
222
+ let paused = false;
223
+ for (const document of openWorkspaceDocuments()) {
224
+ const state = documentPlayState(document.descriptor.id);
225
+ if (state === 'playing') return 'playing';
226
+ if (state === 'paused') paused = true;
227
+ }
228
+ return paused ? 'paused' : 'stopped';
229
+ }
230
+
231
+ const stoppedOnServer = (): 'stopped' => 'stopped';
232
+
214
233
  /** §4.1 play state ("Play stopped" quiet; running/paused colored). */
215
234
  export function PlayStateStatus() {
216
235
  // EVERY hook above the first early return. This component rendered one hook
@@ -222,8 +241,14 @@ export function PlayStateStatus() {
222
241
  const mounts = useProjectMounts();
223
242
  const store = useEditorStore();
224
243
  useSyncExternalStore(store.subscribe, store.getShellSnapshot ?? store.getSnapshot);
244
+ // A DOCUMENT'S OWN PLAY is Play too. Cyclotron's Play runs a model's play script through the
245
+ // document Play tool, not the store's game, so this said "Play stopped" while the race ran (the
246
+ // 0.5.206 blind walk). Any open document counts, not only the active one: a source file opened
247
+ // beside a running race does not stop it. The store's game wins when it runs.
248
+ useSyncExternalStore(subscribeWorkspaceDocuments, workspaceDocumentRegistryVersion, workspaceDocumentRegistryVersion);
249
+ const documentPlay = useSyncExternalStore(subscribeDocumentPlayState, openDocumentsPlayState, stoppedOnServer);
225
250
  if (!mounts) return null;
226
- const state = store.playState;
251
+ const state = store.playState === 'stopped' ? documentPlay : store.playState;
227
252
  const view = {
228
253
  stopped: { label: 'Play stopped', tone: undefined },
229
254
  playing: { label: 'Playing', tone: 'success' },
@@ -208,6 +208,32 @@ export function subscribeDocumentPlayExtensions(listener: () => void): () => voi
208
208
  registry.listeners.add(listener);
209
209
  return () => { registry.listeners.delete(listener); };
210
210
  }
211
+ /** Whether a document's own Play runs, and whether it is paused: what a status line says. */
212
+ export function documentPlayState(documentId: string): 'playing' | 'paused' | 'stopped' {
213
+ for (const extension of registry.extensions.values()) {
214
+ if (!extension.playing(documentId)) continue;
215
+ return extension.transport?.clock(documentId).paused ? 'paused' : 'playing';
216
+ }
217
+ return 'stopped';
218
+ }
219
+ /** Every Play start and stop, and every tool's clock (a pause is a clock change). The clock moves
220
+ * every played frame, so this is for a reader whose snapshot changes rarely. */
221
+ export function subscribeDocumentPlayState(listener: () => void): () => void {
222
+ let clocks: (() => void)[] = [];
223
+ const rebind = (): void => {
224
+ for (const stop of clocks) stop();
225
+ clocks = [...registry.extensions.values()].flatMap((extension) =>
226
+ extension.transport ? [extension.transport.subscribeClock(listener)] : []);
227
+ };
228
+ const onRegistry = (): void => { rebind(); listener(); };
229
+ registry.listeners.add(onRegistry);
230
+ rebind();
231
+ return () => {
232
+ registry.listeners.delete(onRegistry);
233
+ for (const stop of clocks) stop();
234
+ clocks = [];
235
+ };
236
+ }
211
237
  export function registerDocumentPlayExtension(kind: string, extension: DocumentPlayExtension): () => void {
212
238
  if (registry.extensions.has(kind)) throw new Error(`A Play tool already owns ${kind} documents.`);
213
239
  registry.extensions.set(kind, extension);
@@ -349,7 +349,7 @@ export function keymapTable(id: EditorKeymapId): EditorKeymapTable {
349
349
  // Display
350
350
  // ---------------------------------------------------------------------------
351
351
 
352
- function isMacPlatform(): boolean {
352
+ export function isMacPlatform(): boolean {
353
353
  return typeof navigator !== 'undefined' && navigator.platform.includes('Mac');
354
354
  }
355
355