overmux 0.0.1 → 0.0.2

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 (193) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +7 -0
  3. package/dist/auth/.vite/manifest.json +11 -0
  4. package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
  5. package/dist/auth/assets/index-CHIE7qYj.js +72 -0
  6. package/dist/auth/index.html +14 -0
  7. package/dist/bin.js +4504 -7870
  8. package/dist/bin.js.map +1 -0
  9. package/dist/docs/000-index.md +21 -0
  10. package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
  11. package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
  12. package/dist/docs/100-introduction/300-why-overmux.md +22 -0
  13. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  14. package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  15. package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  16. package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  17. package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  18. package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  19. package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  20. package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
  21. package/dist/docs/300-fundamentals/200-configuration.md +3 -0
  22. package/dist/docs/300-fundamentals/300-theming.md +3 -0
  23. package/dist/docs/300-fundamentals/400-server.md +3 -0
  24. package/dist/docs/300-fundamentals/500-client.md +3 -0
  25. package/dist/docs/300-fundamentals/600-operations.md +3 -0
  26. package/dist/docs/300-fundamentals/700-resources.md +3 -0
  27. package/dist/docs/300-fundamentals/800-streams.md +3 -0
  28. package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  29. package/dist/docs/400-reference/100-configuration.md +3 -0
  30. package/dist/docs/400-reference/200-server-api.md +3 -0
  31. package/dist/docs/400-reference/300-client-api.md +3 -0
  32. package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
  33. package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
  34. package/dist/docs/400-reference/400-cli/300-call.md +28 -0
  35. package/dist/docs/400-reference/400-cli/400-check.md +20 -0
  36. package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
  37. package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
  38. package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
  39. package/dist/docs/500-hosted-pages.md +31 -0
  40. package/dist/exports/client.d.ts +237 -0
  41. package/dist/exports/client.d.ts.map +1 -0
  42. package/dist/exports/client.js +1643 -0
  43. package/dist/exports/client.js.map +1 -0
  44. package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
  45. package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
  46. package/dist/exports/index.d.ts +2 -0
  47. package/dist/exports/index.js +49 -0
  48. package/dist/exports/index.js.map +1 -0
  49. package/dist/exports/notifications-kDp16bU_.js +56 -0
  50. package/dist/exports/notifications-kDp16bU_.js.map +1 -0
  51. package/dist/exports/server.d.ts +63 -0
  52. package/dist/exports/server.d.ts.map +1 -0
  53. package/dist/exports/server.js +1120 -0
  54. package/dist/exports/server.js.map +1 -0
  55. package/dist/exports/style.css +368 -0
  56. package/dist/internal/server/coordinator/server-child.d.ts +1 -0
  57. package/dist/internal/server/coordinator/server-child.js +3299 -0
  58. package/dist/internal/server/coordinator/server-child.js.map +1 -0
  59. package/docs/000-index.md +21 -0
  60. package/docs/100-introduction/100-what-is-overmux.md +7 -0
  61. package/docs/100-introduction/200-how-overmux-works.md +7 -0
  62. package/docs/100-introduction/300-why-overmux.md +22 -0
  63. package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  64. package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  65. package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  66. package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  67. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  68. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  69. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  70. package/docs/300-fundamentals/100-project-structure.md +3 -0
  71. package/docs/300-fundamentals/200-configuration.md +3 -0
  72. package/docs/300-fundamentals/300-theming.md +3 -0
  73. package/docs/300-fundamentals/400-server.md +3 -0
  74. package/docs/300-fundamentals/500-client.md +3 -0
  75. package/docs/300-fundamentals/600-operations.md +3 -0
  76. package/docs/300-fundamentals/700-resources.md +3 -0
  77. package/docs/300-fundamentals/800-streams.md +3 -0
  78. package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  79. package/docs/400-reference/100-configuration.md +3 -0
  80. package/docs/400-reference/200-server-api.md +3 -0
  81. package/docs/400-reference/300-client-api.md +3 -0
  82. package/docs/400-reference/400-cli/100-serve.md +33 -0
  83. package/docs/400-reference/400-cli/200-auth.md +52 -0
  84. package/docs/400-reference/400-cli/300-call.md +28 -0
  85. package/docs/400-reference/400-cli/400-check.md +20 -0
  86. package/docs/400-reference/400-cli/500-ai-context.md +102 -0
  87. package/docs/400-reference/400-cli/600-docs.md +23 -0
  88. package/docs/400-reference/400-cli/700-desktop.md +35 -0
  89. package/docs/500-hosted-pages.md +31 -0
  90. package/package.json +59 -18
  91. package/src/internal/cli/app.ts +72 -0
  92. package/src/internal/cli/bin.ts +3 -0
  93. package/src/internal/cli/commands/ai.ts +44 -0
  94. package/src/internal/cli/commands/auth.ts +195 -0
  95. package/src/internal/cli/commands/call.ts +128 -0
  96. package/src/internal/cli/commands/check.ts +48 -0
  97. package/src/internal/cli/commands/desktop-installer.ts +446 -0
  98. package/src/internal/cli/commands/desktop-release.ts +190 -0
  99. package/src/internal/cli/commands/desktop.ts +47 -0
  100. package/src/internal/cli/commands/docs.ts +34 -0
  101. package/src/internal/cli/commands/serve.ts +322 -0
  102. package/src/internal/cli/environment.ts +54 -0
  103. package/src/internal/cli/login.ts +46 -0
  104. package/src/internal/cli/managed-vite.ts +267 -0
  105. package/src/internal/cli/parse-port.ts +8 -0
  106. package/src/internal/client/auth/auth-api.ts +53 -0
  107. package/src/internal/client/auth/auth-shell.css +74 -0
  108. package/src/internal/client/auth/auth-shell.tsx +167 -0
  109. package/src/internal/client/auth/entry.tsx +26 -0
  110. package/src/internal/client/auth/index.html +13 -0
  111. package/src/internal/client/background-notifications.ts +194 -0
  112. package/src/internal/client/browser-api.ts +97 -0
  113. package/src/internal/client/client-definition.ts +184 -0
  114. package/src/internal/client/commands.tsx +260 -0
  115. package/src/internal/client/host/browser-log-forwarding.ts +149 -0
  116. package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
  117. package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
  118. package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
  119. package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
  120. package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
  121. package/src/internal/client/host/notification-forwarding.ts +22 -0
  122. package/src/internal/client/host/overmux-host.tsx +200 -0
  123. package/src/internal/client/host/recovery-screen.css +75 -0
  124. package/src/internal/client/host/recovery-screen.tsx +17 -0
  125. package/src/internal/client/host/update-popover.css +82 -0
  126. package/src/internal/client/host/update-popover.tsx +166 -0
  127. package/src/internal/client/index.ts +45 -0
  128. package/src/internal/client/overmux-react.ts +283 -0
  129. package/src/internal/client/shortcuts.tsx +373 -0
  130. package/src/internal/client/theme-scope.css +114 -0
  131. package/src/internal/client/theme-scope.tsx +107 -0
  132. package/src/internal/client/transport.ts +69 -0
  133. package/src/internal/client/websocket.ts +441 -0
  134. package/src/internal/server/auth/auth-http.ts +414 -0
  135. package/src/internal/server/auth/auth-service.ts +627 -0
  136. package/src/internal/server/auth/auth-shell-http.ts +90 -0
  137. package/src/internal/server/auth/instance-control.ts +381 -0
  138. package/src/internal/server/check-overmux.ts +364 -0
  139. package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
  140. package/src/internal/server/coordinator/server-child.ts +108 -0
  141. package/src/internal/server/coordinator/server-coordinator.ts +262 -0
  142. package/src/internal/server/coordinator/update-watcher.ts +54 -0
  143. package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
  144. package/src/internal/server/http/background-notification-handlers.ts +83 -0
  145. package/src/internal/server/http/control-handlers.ts +44 -0
  146. package/src/internal/server/http/create-http-app.ts +53 -0
  147. package/src/internal/server/http/development-web-backend.ts +83 -0
  148. package/src/internal/server/http/operation-handler.ts +93 -0
  149. package/src/internal/server/http/request-logging.ts +34 -0
  150. package/src/internal/server/http/routes.ts +241 -0
  151. package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
  152. package/src/internal/server/index.ts +16 -0
  153. package/src/internal/server/notifications/background-notification-payload.ts +74 -0
  154. package/src/internal/server/notifications/background-notification-service.ts +139 -0
  155. package/src/internal/server/notifications/background-notification-store.ts +162 -0
  156. package/src/internal/server/notifications/notification-service.ts +70 -0
  157. package/src/internal/server/notifications/private-json-file.ts +84 -0
  158. package/src/internal/server/paths.ts +13 -0
  159. package/src/internal/server/rate-limiter.ts +58 -0
  160. package/src/internal/server/runtime/create-runtime.ts +87 -0
  161. package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
  162. package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
  163. package/src/internal/server/runtime/runtime-operations.ts +78 -0
  164. package/src/internal/server/runtime/runtime-resources.ts +201 -0
  165. package/src/internal/server/runtime/runtime-streams.ts +130 -0
  166. package/src/internal/server/server-logger.ts +110 -0
  167. package/src/internal/server/server-startup-options.ts +51 -0
  168. package/src/internal/server/start-application-server.ts +412 -0
  169. package/src/internal/server/start-overmux-server.ts +26 -0
  170. package/src/internal/server/test-port.ts +23 -0
  171. package/src/internal/server/vite-websocket-proxy.ts +201 -0
  172. package/src/internal/server/websocket-connection.ts +586 -0
  173. package/src/internal/server/websocket-limits.ts +2 -0
  174. package/src/internal/server/websocket-server.ts +311 -0
  175. package/src/internal/shared/background-notifications.ts +37 -0
  176. package/src/internal/shared/index.ts +2 -0
  177. package/src/internal/shared/protocol.ts +317 -0
  178. package/src/internal/shared/routes.ts +3 -0
  179. package/src/public/ai-context.ts +44 -0
  180. package/src/public/client.ts +53 -0
  181. package/src/public/config.ts +287 -0
  182. package/src/public/contracts.ts +55 -0
  183. package/src/public/index.ts +57 -0
  184. package/src/public/notifications.ts +10 -0
  185. package/src/public/server.ts +16 -0
  186. package/dist/public/core.ts +0 -119
  187. package/dist/public/protocol.ts +0 -51
  188. package/dist/public/terminal.ts +0 -23
  189. package/dist/web/assets/index-DC___IUw.css +0 -2
  190. package/dist/web/assets/index-DF9Cptse.js +0 -74
  191. package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
  192. package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
  193. package/dist/web/index.html +0 -14
@@ -0,0 +1,45 @@
1
+ import { overmuxLogoutPath, resolveOvermuxReturnTo } from "@overmux/shared";
2
+ import { BrowserNotificationSettings } from "./browser-notification-settings";
3
+ import { HostedPage } from "./hosted-page";
4
+
5
+ const DesktopNotificationSettings = () => (
6
+ <div data-om-desktop-notifications="">
7
+ <h3>Desktop notifications</h3>
8
+ <p>
9
+ Notifications are delivered through the Overmux desktop app and managed by
10
+ the OS. Change notification permissions and preferences in your operating
11
+ system's settings.
12
+ </p>
13
+ </div>
14
+ );
15
+
16
+ export const HostedSettingsPage = () => {
17
+ const returnTo = resolveOvermuxReturnTo(
18
+ new URLSearchParams(location.search).get("returnTo"),
19
+ );
20
+ const desktopNotifications = Boolean(window.overmuxHost?.notifications);
21
+
22
+ return (
23
+ <HostedPage>
24
+ <header data-om-hosted-header="">
25
+ <h1>Overmux settings</h1>
26
+ <a href={returnTo}>Back to Overmux</a>
27
+ </header>
28
+ <section data-om-hosted-section="">
29
+ <h2>Notifications</h2>
30
+ {desktopNotifications ? (
31
+ <DesktopNotificationSettings />
32
+ ) : (
33
+ <BrowserNotificationSettings />
34
+ )}
35
+ </section>
36
+ <section data-om-hosted-section="">
37
+ <h2>Session</h2>
38
+ <p>End this Overmux session on this device.</p>
39
+ <a data-om-hosted-button="" href={overmuxLogoutPath}>
40
+ Log out
41
+ </a>
42
+ </section>
43
+ </HostedPage>
44
+ );
45
+ };
@@ -0,0 +1,22 @@
1
+ import { notificationEventSchema } from "../../shared/index";
2
+
3
+ import type { WebSocketTransport } from "../transport";
4
+
5
+ // Forwards server notifications to the optional desktop host injected at window.overmuxHost.
6
+ type NotificationHost = {
7
+ notifications?: { show: (notification: unknown) => void; version: 1 };
8
+ };
9
+
10
+ declare global {
11
+ interface Window {
12
+ overmuxHost?: NotificationHost;
13
+ }
14
+ }
15
+
16
+ export const installNotificationForwarding = (transport: WebSocketTransport) =>
17
+ transport.subscribeNotification((event) => {
18
+ const notification = notificationEventSchema.safeParse(event);
19
+ if (notification.success) {
20
+ window.overmuxHost?.notifications?.show(notification.data);
21
+ }
22
+ });
@@ -0,0 +1,200 @@
1
+ // This file is the overmux owned part of the client
2
+
3
+ import {
4
+ runtimeManifestSchema,
5
+ type RuntimeManifest,
6
+ } from "../../shared/index";
7
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
8
+ import {
9
+ Component,
10
+ createElement,
11
+ type ReactNode,
12
+ useCallback,
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ } from "react";
18
+
19
+ import { overmuxLogoutPath, overmuxSettingsPath } from "@overmux/shared";
20
+ import { OvermuxThemeScope } from "../theme-scope";
21
+ import { createOvermuxServerApi } from "../browser-api";
22
+ import { installBrowserLogForwarding } from "./browser-log-forwarding";
23
+ import { installNotificationForwarding } from "./notification-forwarding";
24
+ import type { ClientAppDefinition } from "../client-definition";
25
+ import { type RegisteredCommand, RuntimeContext } from "../commands";
26
+ import { ShortcutHost } from "../shortcuts";
27
+ import { createClientTransport, TransportContext } from "../transport";
28
+ import { HostedLogoutPage } from "./hosted-pages/logout-page";
29
+ import { HostedSettingsPage } from "./hosted-pages/settings-page";
30
+ import { RecoveryScreen } from "./recovery-screen";
31
+ import { UpdatePopover } from "./update-popover";
32
+
33
+ type ErrorBoundaryProps = {
34
+ children: ReactNode;
35
+ fallback: (error: Error) => ReactNode;
36
+ };
37
+
38
+ type ErrorBoundaryState = { error?: Error };
39
+
40
+ class RootErrorBoundary extends Component<
41
+ ErrorBoundaryProps,
42
+ ErrorBoundaryState
43
+ > {
44
+ state: ErrorBoundaryState = {};
45
+
46
+ static getDerivedStateFromError(error: Error) {
47
+ return { error };
48
+ }
49
+
50
+ render() {
51
+ return this.state.error
52
+ ? this.props.fallback(this.state.error)
53
+ : this.props.children;
54
+ }
55
+ }
56
+
57
+ class AuthenticationRequiredError extends Error {}
58
+
59
+ const redirectToLogin = () => location.replace("/login?reason=session-expired");
60
+
61
+ const loadRuntimeManifest = async () => {
62
+ const response = await fetch("/api/runtime-manifest", { cache: "no-store" });
63
+ if (response.status === 401) {
64
+ throw new AuthenticationRequiredError("Authentication required");
65
+ }
66
+ if (!response.ok) {
67
+ throw new Error(`Runtime manifest request failed: ${response.status}`);
68
+ }
69
+ return runtimeManifestSchema.parse(await response.json());
70
+ };
71
+
72
+ const OvermuxRuntime = ({
73
+ definition,
74
+ }: {
75
+ definition: ClientAppDefinition;
76
+ }) => {
77
+ const safeMode = new URLSearchParams(location.search).get("safe") === "1";
78
+ const transport = useMemo(createClientTransport, []);
79
+ const queryClient = useMemo(() => new QueryClient(), []);
80
+ const logForwardingDisposer = useRef<(() => void) | undefined>(undefined);
81
+ const [manifest, setManifest] = useState<RuntimeManifest>();
82
+ const [error, setError] = useState<string>();
83
+ useEffect(() => {
84
+ let active = true;
85
+ void loadRuntimeManifest()
86
+ .then((next) => {
87
+ if (!active) {
88
+ return;
89
+ }
90
+ setManifest(next);
91
+ transport.activate();
92
+ })
93
+ .catch((cause: unknown) => {
94
+ if (cause instanceof AuthenticationRequiredError) {
95
+ redirectToLogin();
96
+ return;
97
+ }
98
+ if (active) {
99
+ setError(cause instanceof Error ? cause.message : String(cause));
100
+ }
101
+ });
102
+ return () => {
103
+ active = false;
104
+ };
105
+ }, [transport]);
106
+ const registrations = useMemo(() => new Set<RegisteredCommand>(), []);
107
+ const [registrationRevision, setRegistrationRevision] = useState(0);
108
+ useEffect(() => {
109
+ if (manifest?.debug && !logForwardingDisposer.current) {
110
+ logForwardingDisposer.current = installBrowserLogForwarding(
111
+ transport.reportDiagnostic,
112
+ );
113
+ }
114
+ }, [manifest?.debug, transport]);
115
+ useEffect(
116
+ () =>
117
+ transport.subscribeStatus((status) => {
118
+ if (status === "authentication-required") {
119
+ redirectToLogin();
120
+ }
121
+ }),
122
+ [transport],
123
+ );
124
+ useEffect(
125
+ () => () => {
126
+ logForwardingDisposer.current?.();
127
+ transport.dispose();
128
+ },
129
+ [transport],
130
+ );
131
+ useEffect(() => installNotificationForwarding(transport), [transport]);
132
+ const refreshCommands = useCallback(
133
+ () => setRegistrationRevision((revision) => revision + 1),
134
+ [],
135
+ );
136
+ const registerCommand = useCallback(
137
+ (registration: RegisteredCommand) => {
138
+ registrations.add(registration);
139
+ refreshCommands();
140
+ return () => {
141
+ registrations.delete(registration);
142
+ refreshCommands();
143
+ };
144
+ },
145
+ [refreshCommands, registrations],
146
+ );
147
+ const overmuxServerApi = useMemo(
148
+ () =>
149
+ manifest ? createOvermuxServerApi({ manifest, transport }) : undefined,
150
+ [manifest, transport],
151
+ );
152
+ if (safeMode || !manifest || !overmuxServerApi) {
153
+ return <RecoveryScreen error={error} />;
154
+ }
155
+
156
+ const App = definition.component;
157
+ return (
158
+ <QueryClientProvider client={queryClient}>
159
+ <RuntimeContext.Provider
160
+ value={{
161
+ manifest,
162
+ overmuxServerApi,
163
+ refreshCommands,
164
+ registerCommand,
165
+ }}
166
+ >
167
+ <TransportContext.Provider value={transport}>
168
+ <ShortcutHost
169
+ definition={definition}
170
+ registrations={registrations}
171
+ registrationRevision={registrationRevision}
172
+ >
173
+ <RootErrorBoundary
174
+ fallback={(cause) => <RecoveryScreen error={cause.message} />}
175
+ >
176
+ {createElement(App)}
177
+ </RootErrorBoundary>
178
+ <UpdatePopover transport={transport} />
179
+ </ShortcutHost>
180
+ </TransportContext.Provider>
181
+ </RuntimeContext.Provider>
182
+ </QueryClientProvider>
183
+ );
184
+ };
185
+
186
+ export const OvermuxHost = ({
187
+ definition,
188
+ }: {
189
+ definition: ClientAppDefinition;
190
+ }) => (
191
+ <OvermuxThemeScope {...definition.appearance}>
192
+ {location.pathname === overmuxSettingsPath ? (
193
+ <HostedSettingsPage />
194
+ ) : location.pathname === overmuxLogoutPath ? (
195
+ <HostedLogoutPage />
196
+ ) : (
197
+ <OvermuxRuntime definition={definition} />
198
+ )}
199
+ </OvermuxThemeScope>
200
+ );
@@ -0,0 +1,75 @@
1
+ @layer om.components {
2
+ @scope ([data-om-recovery]) {
3
+ :where([data-om-recovery-reload]) {
4
+ min-block-size: 2.75rem;
5
+ border: 1px solid var(--om-color-border, var(--_om-color-border));
6
+ border-radius: var(--om-radius, 0.5rem);
7
+ color: inherit;
8
+ background: var(--om-color-surface, var(--_om-color-surface));
9
+ font: inherit;
10
+ transition:
11
+ background-color var(--om-motion-duration, 120ms) ease,
12
+ border-color var(--om-motion-duration, 120ms) ease;
13
+ }
14
+ :where([data-om-recovery-reload]):focus-visible {
15
+ outline: 2px solid var(--om-color-accent, var(--_om-color-accent));
16
+ outline-offset: 2px;
17
+ box-shadow: var(
18
+ --om-focus-ring,
19
+ 0 0 0 3px color-mix(in srgb, var(--_om-color-accent) 24%, transparent)
20
+ );
21
+ }
22
+ :where(:scope) {
23
+ position: fixed;
24
+ inset: 0;
25
+ display: grid;
26
+ overflow: auto;
27
+ place-items: center;
28
+ padding: calc(var(--om-spacing, 1rem) * 2);
29
+ color: var(--om-color-fg, var(--_om-color-fg));
30
+ background: var(--om-color-canvas, var(--_om-color-canvas));
31
+ }
32
+ :where([data-om-recovery-panel]) {
33
+ inline-size: min(100%, 36rem);
34
+ padding: calc(var(--om-spacing, 0.75rem) * 2);
35
+ border: 1px solid var(--om-color-border, var(--_om-color-border));
36
+ border-radius: var(--om-radius, 0.5rem);
37
+ background: var(--om-color-panel, var(--_om-color-panel));
38
+ box-shadow: var(--om-elevation, 0 1rem 2rem rgb(0 0 0 / 20%));
39
+ }
40
+ :where([data-om-recovery-title]) {
41
+ margin: 0;
42
+ font-size: 1.125rem;
43
+ }
44
+ :where([data-om-recovery-error]) {
45
+ margin-block: 1rem 0;
46
+ overflow-wrap: anywhere;
47
+ white-space: pre-wrap;
48
+ color: var(--om-color-danger, var(--_om-color-danger));
49
+ font-family: var(
50
+ --om-font-mono,
51
+ ui-monospace,
52
+ SFMono-Regular,
53
+ Consolas,
54
+ monospace
55
+ );
56
+ font-size: 0.875rem;
57
+ }
58
+ :where([data-om-recovery-reload]) {
59
+ margin-block-start: 1rem;
60
+ padding-inline: 0.75rem;
61
+ }
62
+ @media (prefers-reduced-motion: reduce) {
63
+ :where([data-om-recovery-reload]) {
64
+ scroll-behavior: auto;
65
+ transition-duration: 0.01ms;
66
+ }
67
+ }
68
+ @media (forced-colors: active) {
69
+ :where([data-om-recovery-reload]):focus-visible {
70
+ outline-color: Highlight;
71
+ box-shadow: none;
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,17 @@
1
+ import "./recovery-screen.css";
2
+
3
+ export const RecoveryScreen = ({ error }: { error?: string }) => (
4
+ <main data-om-recovery="">
5
+ <section data-om-recovery-panel="">
6
+ <h1 data-om-recovery-title="">Overmux recovery</h1>
7
+ {error ? <pre data-om-recovery-error="">{error}</pre> : null}
8
+ <button
9
+ data-om-recovery-reload=""
10
+ onClick={() => location.reload()}
11
+ type="button"
12
+ >
13
+ Reload
14
+ </button>
15
+ </section>
16
+ </main>
17
+ );
@@ -0,0 +1,82 @@
1
+ @layer om.components {
2
+ @scope (
3
+ [data-om-reconnecting],
4
+ [data-om-restarting],
5
+ [data-om-update]
6
+ ) {
7
+ :where(:scope[data-om-reconnecting]) {
8
+ position: fixed;
9
+ inset: 0;
10
+ z-index: 1002;
11
+ display: grid;
12
+ place-items: center;
13
+ padding: calc(var(--om-spacing, 1rem) * 2);
14
+ color: var(--om-color-fg, var(--_om-color-fg));
15
+ background: var(--om-color-canvas, var(--_om-color-canvas));
16
+ }
17
+ :where([data-om-reconnecting-panel]) {
18
+ inline-size: min(100%, 24rem);
19
+ padding: calc(var(--om-spacing, 0.75rem) * 2);
20
+ border: 1px solid var(--om-color-border, var(--_om-color-border));
21
+ border-radius: var(--om-radius, 0.5rem);
22
+ background: var(--om-color-panel, var(--_om-color-panel));
23
+ box-shadow: var(--om-elevation, 0 1rem 2rem rgb(0 0 0 / 20%));
24
+ text-align: center;
25
+ }
26
+ :where([data-om-reconnecting-title]) {
27
+ margin: 0;
28
+ font-size: 1.125rem;
29
+ }
30
+ :where([data-om-reconnecting-message]) {
31
+ margin-block: 0.5rem 0;
32
+ color: var(--om-color-muted, var(--_om-color-muted));
33
+ }
34
+ :where(:scope[data-om-update]) {
35
+ position: fixed;
36
+ inset-block-end: 0.75rem;
37
+ inset-inline-end: 0.75rem;
38
+ z-index: 1001;
39
+ inline-size: min(calc(100% - 1.5rem), 32rem);
40
+ padding: var(--om-spacing, 0.75rem);
41
+ border: 1px solid var(--om-color-border, var(--_om-color-border));
42
+ border-radius: var(--om-radius, 0.5rem);
43
+ background: var(--om-color-panel, var(--_om-color-panel));
44
+ box-shadow: var(--om-elevation, 0 0.75rem 2rem rgb(0 0 0 / 30%));
45
+ }
46
+ :where([data-om-update] p) {
47
+ margin: 0 0 0.75rem;
48
+ }
49
+ :where([data-om-update-reload]) {
50
+ min-block-size: 2.75rem;
51
+ padding-inline: 0.75rem;
52
+ border: 1px solid var(--om-color-border, var(--_om-color-border));
53
+ border-radius: var(--om-radius, 0.5rem);
54
+ color: inherit;
55
+ background: var(--om-color-surface, var(--_om-color-surface));
56
+ font: inherit;
57
+ }
58
+ :where([data-om-update-error]) {
59
+ margin-block: 0.75rem 0;
60
+ color: var(--om-color-danger, var(--_om-color-danger));
61
+ }
62
+ :where(:scope[data-om-restarting]) {
63
+ position: fixed;
64
+ inset: 0;
65
+ z-index: 1003;
66
+ display: grid;
67
+ place-items: center;
68
+ color: var(--om-color-fg, var(--_om-color-fg));
69
+ background: var(--om-color-canvas, var(--_om-color-canvas));
70
+ }
71
+ :where([data-om-restarting-panel]) {
72
+ text-align: center;
73
+ }
74
+ :where([data-om-restarting-title]) {
75
+ margin: 0;
76
+ font-size: 1.125rem;
77
+ }
78
+ :where([data-om-restarting-message]) {
79
+ color: var(--om-color-muted, var(--_om-color-muted));
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,166 @@
1
+ // Presents update availability and coordinates browser-side restart recovery.
2
+ // Every browser follows the server broadcast and reloads only after health returns.
3
+
4
+ import "./update-popover.css";
5
+
6
+ import { delay } from "@overmux/lib";
7
+ import { useCallback, useEffect, useReducer, useRef, useState } from "react";
8
+
9
+ import type { ServerLifecycleEvent } from "../../shared/index";
10
+ import type { ClientTransport, TransportStatus } from "../transport";
11
+
12
+ type UpdateState =
13
+ | { type: "current" }
14
+ | { type: "available"; error?: string }
15
+ | { type: "requesting" }
16
+ | { type: "restarting" };
17
+ type UpdateAction =
18
+ | ServerLifecycleEvent
19
+ | { type: "restart-requested" }
20
+ | { type: "restart-failed"; error: string };
21
+ type ConnectionState = "initial" | "connected" | "reconnecting";
22
+
23
+ const reduceUpdateState = (
24
+ _state: UpdateState,
25
+ action: UpdateAction,
26
+ ): UpdateState => {
27
+ if (action.type === "update-available") {
28
+ return { type: "available" };
29
+ }
30
+ if (action.type === "restart-requested") {
31
+ return { type: "requesting" };
32
+ }
33
+ if (action.type === "restart-failed") {
34
+ return { type: "available", error: action.error };
35
+ }
36
+ return { type: "restarting" };
37
+ };
38
+
39
+ const pollUntilHealthy = async (signal: AbortSignal): Promise<void> => {
40
+ if (signal.aborted) {
41
+ return;
42
+ }
43
+ try {
44
+ const response = await fetch("/api/health", {
45
+ cache: "no-store",
46
+ signal,
47
+ });
48
+ if (response.ok) {
49
+ location.reload();
50
+ return;
51
+ }
52
+ } catch {}
53
+ await delay(250);
54
+ return pollUntilHealthy(signal);
55
+ };
56
+
57
+ const ReconnectingScreen = () => (
58
+ <main aria-live="polite" data-om-reconnecting="" role="status">
59
+ <section data-om-reconnecting-panel="">
60
+ <h1 data-om-reconnecting-title="">Reconnecting</h1>
61
+ <p data-om-reconnecting-message="">
62
+ Reconnecting to Overmux. Your workspace will resume shortly.
63
+ </p>
64
+ </section>
65
+ </main>
66
+ );
67
+
68
+ const useUpdateState = (transport: ClientTransport) => {
69
+ const [state, dispatch] = useReducer(reduceUpdateState, { type: "current" });
70
+
71
+ useEffect(
72
+ () => transport.subscribeLifecycle((event) => dispatch(event)),
73
+ [transport],
74
+ );
75
+ useEffect(() => {
76
+ if (state.type !== "restarting") {
77
+ return;
78
+ }
79
+ const controller = new AbortController();
80
+ void pollUntilHealthy(controller.signal);
81
+ return () => controller.abort();
82
+ }, [state.type]);
83
+
84
+ const requestRestart = useCallback(async () => {
85
+ dispatch({ type: "restart-requested" });
86
+ try {
87
+ const response = await fetch("/api/restart", { method: "POST" });
88
+ if (!response.ok) {
89
+ throw new Error(`Restart request failed: ${response.status}`);
90
+ }
91
+ dispatch({ type: "restarting" });
92
+ } catch (cause) {
93
+ const error = cause instanceof Error ? cause.message : String(cause);
94
+ dispatch({ type: "restart-failed", error });
95
+ }
96
+ }, []);
97
+
98
+ return { requestRestart, state };
99
+ };
100
+
101
+ const useConnectionState = (transport: ClientTransport) => {
102
+ const connected = useRef(false);
103
+ const [connection, setConnection] = useState<ConnectionState>("initial");
104
+
105
+ useEffect(
106
+ () =>
107
+ transport.subscribeStatus((status: TransportStatus) => {
108
+ if (status === "connected") {
109
+ connected.current = true;
110
+ setConnection("connected");
111
+ return;
112
+ }
113
+ if (!connected.current) {
114
+ return;
115
+ }
116
+ connected.current = false;
117
+ setConnection("reconnecting");
118
+ }),
119
+ [transport],
120
+ );
121
+
122
+ return connection;
123
+ };
124
+
125
+ export const UpdatePopover = ({
126
+ transport,
127
+ }: {
128
+ transport: ClientTransport;
129
+ }) => {
130
+ const connection = useConnectionState(transport);
131
+ const { requestRestart, state } = useUpdateState(transport);
132
+
133
+ if (state.type === "restarting") {
134
+ return (
135
+ <main aria-live="polite" data-om-restarting="" role="status">
136
+ <section data-om-restarting-panel="">
137
+ <h1 data-om-restarting-title="">Restarting Overmux</h1>
138
+ <p data-om-restarting-message="">Waiting for the new version.</p>
139
+ </section>
140
+ </main>
141
+ );
142
+ }
143
+ if (connection === "reconnecting") {
144
+ return <ReconnectingScreen />;
145
+ }
146
+ if (state.type === "current") {
147
+ return null;
148
+ }
149
+
150
+ const error = state.type === "available" ? state.error : undefined;
151
+
152
+ return (
153
+ <aside data-om-update="" role="status">
154
+ <p>A new version is available. Reload to update.</p>
155
+ <button
156
+ data-om-update-reload=""
157
+ disabled={state.type === "requesting"}
158
+ onClick={() => void requestRestart()}
159
+ type="button"
160
+ >
161
+ Reload
162
+ </button>
163
+ {error ? <p data-om-update-error="">{error}</p> : null}
164
+ </aside>
165
+ );
166
+ };
@@ -0,0 +1,45 @@
1
+ export {
2
+ disableBackgroundNotifications,
3
+ enableBackgroundNotifications,
4
+ } from "./background-notifications";
5
+ export type { OvermuxServerApi } from "./browser-api";
6
+ export {
7
+ createOvermuxHooks,
8
+ type ResourceResult,
9
+ type StreamResult,
10
+ } from "./overmux-react";
11
+ export { OvermuxPortal, OvermuxThemeScope } from "./theme-scope";
12
+ export type {
13
+ OvermuxContrast,
14
+ OvermuxExternalPortalContainer,
15
+ OvermuxPortalProps,
16
+ OvermuxScheme,
17
+ OvermuxThemeScopeProps,
18
+ OvermuxStyle,
19
+ OvermuxThemeToken,
20
+ } from "./theme-scope";
21
+ export {
22
+ chordPrefixSchema,
23
+ commandBindingSchema,
24
+ defineCommandRegistry,
25
+ defineOvermuxClient,
26
+ formatKeyBinding,
27
+ formatShortcutBinding,
28
+ keyBindingSchema,
29
+ matchesKeyBinding,
30
+ shortcutBindingSchema,
31
+ } from "./client-definition";
32
+ export { skipToken, useCommand, useCommands } from "./commands";
33
+ export { useShortcutInputTarget } from "./shortcuts";
34
+ export type {
35
+ ChordPrefix,
36
+ CommandBinding,
37
+ CommandHandle,
38
+ ConditionalShortcutBinding,
39
+ KeyBinding,
40
+ KeySequence,
41
+ OvermuxClientAppearance,
42
+ ShortcutBinding,
43
+ } from "./client-definition";
44
+ export type { CommandEntry, SkipToken } from "./commands";
45
+ export type { ShortcutInputTarget } from "./shortcuts";