overmux 0.0.8 → 0.0.10

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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/bin.js +59 -21
  3. package/dist/bin.js.map +1 -1
  4. package/dist/docs/100-introduction/300-why-i-built-overmux.md +1 -1
  5. package/dist/docs/200-getting-started/200-install-overmux-desktop.md +13 -0
  6. package/dist/docs/400-reference/500-server/100-resources.md +25 -0
  7. package/dist/docs/400-reference/500-server/500-api.md +4 -2
  8. package/dist/docs/400-reference/600-client/010-commands.md +0 -29
  9. package/dist/docs/400-reference/600-client/030-native-web-view.md +166 -0
  10. package/dist/docs/400-reference/600-client/300-tech-stack-recommendations.md +1 -0
  11. package/dist/docs/400-reference/700-cli/600-docs.md +1 -1
  12. package/dist/exports/client.d.ts +44 -8
  13. package/dist/exports/client.d.ts.map +1 -1
  14. package/dist/exports/client.js +212 -40
  15. package/dist/exports/client.js.map +1 -1
  16. package/dist/exports/{index-Cz3xkCa4.d.ts → index-Dk11nKIv.d.ts} +2 -2
  17. package/dist/exports/index-Dk11nKIv.d.ts.map +1 -0
  18. package/dist/exports/index.d.ts +1 -1
  19. package/dist/exports/index.js +1 -1
  20. package/dist/exports/index.js.map +1 -1
  21. package/dist/internal/server/coordinator/server-child.js +75 -22
  22. package/dist/internal/server/coordinator/server-child.js.map +1 -1
  23. package/docs/100-introduction/300-why-i-built-overmux.md +1 -1
  24. package/docs/200-getting-started/200-install-overmux-desktop.md +13 -0
  25. package/docs/400-reference/500-server/100-resources.md +25 -0
  26. package/docs/400-reference/500-server/500-api.md +4 -2
  27. package/docs/400-reference/600-client/010-commands.md +0 -29
  28. package/docs/400-reference/600-client/030-native-web-view.md +166 -0
  29. package/docs/400-reference/600-client/300-tech-stack-recommendations.md +1 -0
  30. package/docs/400-reference/700-cli/600-docs.md +1 -1
  31. package/package.json +2 -2
  32. package/src/internal/client/client-definition.ts +9 -13
  33. package/src/internal/client/host/desktop-host.ts +28 -2
  34. package/src/internal/client/native-web-view.tsx +158 -0
  35. package/src/internal/client/overmux-react.ts +121 -27
  36. package/src/internal/client/shortcuts.tsx +17 -31
  37. package/src/internal/server/runtime/runtime-resources.ts +99 -20
  38. package/src/internal/server/websocket-connection.ts +27 -2
  39. package/src/public/client.ts +5 -0
  40. package/src/public/config.ts +1 -1
  41. package/dist/exports/index-Cz3xkCa4.d.ts.map +0 -1
@@ -3,7 +3,7 @@ import { a as notificationSchema, c as parseDeepLink, l as resolveOvermuxReturnT
3
3
  import { z } from "zod";
4
4
  import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
5
  import { Component, createContext, createElement, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
6
- import { formatKeyBinding, keyBindingSchema, matchesKeyBinding, platformBinding } from "@overmux/keybindings";
6
+ import { formatShortcutBinding, keyBindingSchema, keyBindingSequence, matchesKeyBinding, matchesShortcutBindingPrefix, normalizedShortcutBinding, shortcutBindingSchema } from "@overmux/keybindings";
7
7
  import { createPortal } from "react-dom";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/internal/shared/background-notifications.ts
@@ -320,7 +320,6 @@ const writeClipboardText = async (text) => {
320
320
  };
321
321
  //#endregion
322
322
  //#region src/internal/client/client-definition.ts
323
- const shortcutBindingSchema = z.union([keyBindingSchema, z.array(keyBindingSchema).min(2)]);
324
323
  const commandBindingSchema = z.union([shortcutBindingSchema, z.object({
325
324
  binding: shortcutBindingSchema,
326
325
  when: z.object({ media: z.string().trim().min(1) })
@@ -329,7 +328,6 @@ const chordPrefixSchema = z.object({
329
328
  binding: keyBindingSchema,
330
329
  unmatched: z.literal("replay-to-focused-input")
331
330
  });
332
- const formatShortcutBinding = (binding) => typeof binding === "string" ? formatKeyBinding(binding) : binding.map(formatKeyBinding).join(" ");
333
331
  const validateBindings = (bindings) => bindings?.forEach((binding) => commandBindingSchema.parse(binding));
334
332
  const defineCommandRegistry = () => (commands) => Object.fromEntries(Object.entries(commands).map(([id, command]) => {
335
333
  validateBindings(command.defaultBindings);
@@ -452,9 +450,95 @@ const useCanonicalInput = (input) => {
452
450
  const key = inputKey(input);
453
451
  return useMemo(() => key ? JSON.parse(key) : void 0, [key]);
454
452
  };
455
- const useResourceById = (id, input, skipped) => {
453
+ const pendingResourceInvalidations = /* @__PURE__ */ new WeakSet();
454
+ const invalidateResource = ({ queryClient, queryKey }) => {
455
+ const query = queryClient.getQueryCache().find({
456
+ exact: true,
457
+ queryKey
458
+ });
459
+ if (query && query.state.fetchStatus !== "idle") {
460
+ pendingResourceInvalidations.add(query);
461
+ return;
462
+ }
463
+ if (query) pendingResourceInvalidations.delete(query);
464
+ queryClient.invalidateQueries({
465
+ exact: true,
466
+ queryKey
467
+ });
468
+ };
469
+ const flushResourceInvalidation = ({ queryClient, queryKey }) => {
470
+ const query = queryClient.getQueryCache().find({
471
+ exact: true,
472
+ queryKey
473
+ });
474
+ if (!query || query.state.fetchStatus !== "idle") return;
475
+ if (pendingResourceInvalidations.delete(query)) queryClient.invalidateQueries({
476
+ exact: true,
477
+ queryKey
478
+ });
479
+ };
480
+ const useResourceInvalidation = ({ queryKey, skipped }) => {
481
+ const [id, canonicalInput] = queryKey;
456
482
  const { manifest, overmuxServerApi } = useRuntime();
457
483
  const queryClient = useQueryClient();
484
+ const invalidate = useCallback(() => invalidateResource({
485
+ queryClient,
486
+ queryKey
487
+ }), [queryClient, queryKey]);
488
+ useEffect(() => {
489
+ if (skipped) return;
490
+ let active = true;
491
+ const cache = queryClient.getQueryCache();
492
+ const observedQuery = cache.find({
493
+ exact: true,
494
+ queryKey
495
+ });
496
+ const unsubscribe = cache.subscribe((event) => {
497
+ if (event.type === "updated" && event.query === observedQuery && event.query.state.fetchStatus === "idle" && pendingResourceInvalidations.has(event.query)) queueMicrotask(() => {
498
+ if (active) flushResourceInvalidation({
499
+ queryClient,
500
+ queryKey
501
+ });
502
+ });
503
+ });
504
+ flushResourceInvalidation({
505
+ queryClient,
506
+ queryKey
507
+ });
508
+ return () => {
509
+ active = false;
510
+ unsubscribe();
511
+ };
512
+ }, [
513
+ queryClient,
514
+ queryKey,
515
+ skipped
516
+ ]);
517
+ useEffect(() => {
518
+ if (skipped || !manifest.resources.includes(id)) return;
519
+ let active = true;
520
+ const unsubscribe = overmuxServerApi.subscribeResource({
521
+ id,
522
+ input: canonicalInput,
523
+ onError: () => active && invalidate(),
524
+ onInvalidate: () => active && invalidate()
525
+ });
526
+ return () => {
527
+ active = false;
528
+ unsubscribe();
529
+ };
530
+ }, [
531
+ manifest.resources,
532
+ canonicalInput,
533
+ id,
534
+ invalidate,
535
+ overmuxServerApi,
536
+ skipped
537
+ ]);
538
+ return invalidate;
539
+ };
540
+ const useResourceById = (id, input, skipped) => {
541
+ const { overmuxServerApi } = useRuntime();
458
542
  const canonicalInput = useCanonicalInput(input);
459
543
  const queryKey = useMemo(() => [id, canonicalInput], [canonicalInput, id]);
460
544
  const query = useQuery({
@@ -465,32 +549,10 @@ const useResourceById = (id, input, skipped) => {
465
549
  }),
466
550
  queryKey
467
551
  });
468
- useEffect(() => !skipped && manifest.resources.includes(id) ? overmuxServerApi.subscribeResource({
469
- id,
470
- input: canonicalInput,
471
- onError: () => void queryClient.invalidateQueries({
472
- exact: true,
473
- queryKey
474
- }),
475
- onInvalidate: () => void queryClient.invalidateQueries({
476
- exact: true,
477
- queryKey
478
- })
479
- }) : void 0, [
480
- manifest.resources,
481
- canonicalInput,
482
- id,
483
- overmuxServerApi,
484
- queryClient,
552
+ const refetch = useResourceInvalidation({
485
553
  queryKey,
486
554
  skipped
487
- ]);
488
- const refetch = useCallback(() => {
489
- queryClient.invalidateQueries({
490
- exact: true,
491
- queryKey
492
- });
493
- }, [queryClient, queryKey]);
555
+ });
494
556
  if (skipped) return;
495
557
  if (query.status === "pending") return {
496
558
  refetch,
@@ -855,12 +917,6 @@ const installDeepLinkNavigation = ({ getInstance, navigate = defaultNavigate })
855
917
  };
856
918
  //#endregion
857
919
  //#region src/internal/client/shortcuts.tsx
858
- const bindingSequence = (binding) => typeof binding === "string" ? [binding] : [...binding];
859
- const normalizedBinding = (binding) => bindingSequence(binding).map((key) => platformBinding(key));
860
- const sequenceMatches = (binding, pending, event) => {
861
- const sequence = normalizedBinding(binding);
862
- return sequence.length > pending.length && pending.every((key, index) => sequence[index] === key) && matchesKeyBinding(sequence[pending.length], event);
863
- };
864
920
  const isTextInput = (element) => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLElement && (element.isContentEditable || element.getAttribute("role") === "textbox");
865
921
  const acceptsTextInput = (bindings) => {
866
922
  if (!(document.activeElement instanceof Element)) return false;
@@ -868,7 +924,7 @@ const acceptsTextInput = (bindings) => {
868
924
  if (!terminal) return false;
869
925
  if (terminal.closest("[data-copy-mode]")) return true;
870
926
  return bindings.some((binding) => {
871
- const sequence = bindingSequence(binding);
927
+ const sequence = keyBindingSequence(binding);
872
928
  return sequence.length > 1 || sequence[0]?.startsWith("F") || sequence[0]?.includes("+");
873
929
  });
874
930
  };
@@ -966,8 +1022,8 @@ const ShortcutHost = ({ children, definition, registrations, registrationRevisio
966
1022
  window.addEventListener("focusin", refreshFocus);
967
1023
  return () => window.removeEventListener("focusin", refreshFocus);
968
1024
  }, []);
1025
+ const shortcuts = flattenShortcuts(definition, (media) => window.matchMedia(media).matches);
969
1026
  useEffect(() => {
970
- const shortcuts = flattenShortcuts(definition, (media) => window.matchMedia(media).matches);
971
1027
  let pending = [];
972
1028
  let pendingEvents = [];
973
1029
  let pendingTarget;
@@ -997,7 +1053,11 @@ const ShortcutHost = ({ children, definition, registrations, registrationRevisio
997
1053
  clearPending();
998
1054
  return;
999
1055
  }
1000
- const matches = shortcuts.flatMap((shortcut) => shortcut.bindings.filter((binding) => sequenceMatches(binding, pending, event) && canRunShortcut(shortcut, event, registrations)).map((binding) => ({
1056
+ const matches = shortcuts.flatMap((shortcut) => shortcut.bindings.filter((binding) => matchesShortcutBindingPrefix({
1057
+ binding,
1058
+ input: event,
1059
+ pending
1060
+ }) && canRunShortcut(shortcut, event, registrations)).map((binding) => ({
1001
1061
  binding,
1002
1062
  shortcut
1003
1063
  })));
@@ -1009,14 +1069,14 @@ const ShortcutHost = ({ children, definition, registrations, registrationRevisio
1009
1069
  }
1010
1070
  return;
1011
1071
  }
1012
- const complete = matches.find(({ binding }) => normalizedBinding(binding).length === pending.length + 1);
1072
+ const complete = matches.find(({ binding }) => normalizedShortcutBinding(binding).length === pending.length + 1);
1013
1073
  if (complete) {
1014
1074
  clearPending();
1015
1075
  if (runShortcut(complete.shortcut, event, registrations)) event.stopPropagation();
1016
1076
  return;
1017
1077
  }
1018
1078
  if (!pending.length) pendingTarget = definition.chordPrefixes?.some((prefix) => matchesKeyBinding(prefix.binding, event)) ? selectShortcutInputTarget(inputTargets, document.activeElement) : void 0;
1019
- pending = [...pending, normalizedBinding(matches[0].binding)[pending.length]];
1079
+ pending = [...pending, normalizedShortcutBinding(matches[0].binding)[pending.length]];
1020
1080
  pendingEvents = [...pendingEvents, event];
1021
1081
  event.preventDefault();
1022
1082
  event.stopPropagation();
@@ -1842,6 +1902,118 @@ const OvermuxHost = ({ definition }) => /* @__PURE__ */ jsx(OvermuxThemeScope, {
1842
1902
  children: location.pathname === "/_overmux/settings" ? /* @__PURE__ */ jsx(HostedSettingsPage, {}) : location.pathname === "/_overmux/logout" ? /* @__PURE__ */ jsx(HostedLogoutPage, {}) : /* @__PURE__ */ jsx(OvermuxRuntime, { definition })
1843
1903
  });
1844
1904
  //#endregion
1845
- export { OvermuxHost, OvermuxPortal, OvermuxThemeScope, createOvermuxHooks, defineCommandRegistry, defineOvermuxClient, disableBackgroundNotifications, enableBackgroundNotifications, formatShortcutBinding, readClipboardText, skipToken, useCommand, useCommands, useShortcutInputTarget, writeClipboardText };
1905
+ //#region src/internal/client/native-web-view.tsx
1906
+ const command = async (view, input, url) => {
1907
+ try {
1908
+ await view.bridge.command(input);
1909
+ } catch (error) {
1910
+ view.report({
1911
+ url,
1912
+ code: "ERR_DESKTOP_BRIDGE",
1913
+ message: String(error)
1914
+ });
1915
+ }
1916
+ };
1917
+ const trackBounds = (element, view) => {
1918
+ let previous = "";
1919
+ let frame = 0;
1920
+ const measure = () => {
1921
+ const rect = element.getBoundingClientRect();
1922
+ const visible = !document.hidden && element.checkVisibility({
1923
+ opacityProperty: true,
1924
+ visibilityProperty: true
1925
+ });
1926
+ const bounds = {
1927
+ x: Math.round(rect.x),
1928
+ y: Math.round(rect.y),
1929
+ width: visible ? Math.max(0, Math.round(rect.width)) : 0,
1930
+ height: visible ? Math.max(0, Math.round(rect.height)) : 0
1931
+ };
1932
+ const key = JSON.stringify(bounds);
1933
+ if (key !== previous) {
1934
+ previous = key;
1935
+ view.bridge.setBounds(view.id, bounds);
1936
+ }
1937
+ };
1938
+ const tick = () => {
1939
+ measure();
1940
+ frame = requestAnimationFrame(tick);
1941
+ };
1942
+ tick();
1943
+ document.addEventListener("visibilitychange", measure);
1944
+ return () => {
1945
+ cancelAnimationFrame(frame);
1946
+ document.removeEventListener("visibilitychange", measure);
1947
+ };
1948
+ };
1949
+ const NativeWebView = ({ url, className, style, fallback, onLoadError, allowedHttpOrigins = [], passthroughBindings = [] }) => {
1950
+ const bridge = typeof window === "undefined" || window.overmuxHost?.version !== 1 || window.overmuxHost.nativeWebView?.version !== 1 ? void 0 : window.overmuxHost.nativeWebView;
1951
+ const element = useRef(null);
1952
+ const mounted = useRef(void 0);
1953
+ const latest = useRef({
1954
+ url,
1955
+ onLoadError
1956
+ });
1957
+ latest.current = {
1958
+ url,
1959
+ onLoadError
1960
+ };
1961
+ const origins = JSON.stringify(allowedHttpOrigins);
1962
+ const bindings = JSON.stringify(passthroughBindings);
1963
+ useEffect(() => {
1964
+ if (!bridge || !element.current) return;
1965
+ const view = {
1966
+ id: crypto.randomUUID(),
1967
+ active: true,
1968
+ bridge,
1969
+ report: (error) => {
1970
+ queueMicrotask(() => {
1971
+ if (view.active) latest.current.onLoadError?.(error);
1972
+ });
1973
+ }
1974
+ };
1975
+ mounted.current = view;
1976
+ const unsubscribe = bridge.onLoadError(({ id, error }) => {
1977
+ if (id === view.id) view.report(error);
1978
+ });
1979
+ command(view, {
1980
+ type: "create",
1981
+ id: view.id
1982
+ }, latest.current.url);
1983
+ const stopTracking = trackBounds(element.current, view);
1984
+ return () => {
1985
+ view.active = false;
1986
+ mounted.current = void 0;
1987
+ stopTracking();
1988
+ unsubscribe();
1989
+ command(view, {
1990
+ type: "destroy",
1991
+ id: view.id
1992
+ }, latest.current.url);
1993
+ };
1994
+ }, [bridge]);
1995
+ useEffect(() => {
1996
+ const view = mounted.current;
1997
+ if (view) command(view, {
1998
+ type: "configure",
1999
+ id: view.id,
2000
+ url,
2001
+ allowedHttpOrigins: JSON.parse(origins),
2002
+ passthroughBindings: JSON.parse(bindings)
2003
+ }, url);
2004
+ }, [
2005
+ bindings,
2006
+ bridge,
2007
+ url,
2008
+ origins
2009
+ ]);
2010
+ return bridge ? /* @__PURE__ */ jsx("div", {
2011
+ ref: element,
2012
+ className,
2013
+ style
2014
+ }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
2015
+ };
2016
+ //#endregion
2017
+ export { NativeWebView, OvermuxHost, OvermuxPortal, OvermuxThemeScope, createOvermuxHooks, defineCommandRegistry, defineOvermuxClient, disableBackgroundNotifications, enableBackgroundNotifications, formatShortcutBinding, readClipboardText, skipToken, useCommand, useCommands, useShortcutInputTarget, writeClipboardText };
1846
2018
 
1847
2019
  //# sourceMappingURL=client.js.map