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.
- package/CHANGELOG.md +16 -0
- package/README.md +7 -0
- package/dist/auth/.vite/manifest.json +11 -0
- package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
- package/dist/auth/assets/index-CHIE7qYj.js +72 -0
- package/dist/auth/index.html +14 -0
- package/dist/bin.js +4504 -7870
- package/dist/bin.js.map +1 -0
- package/dist/docs/000-index.md +21 -0
- package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/dist/docs/100-introduction/300-why-overmux.md +22 -0
- package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
- package/dist/docs/300-fundamentals/200-configuration.md +3 -0
- package/dist/docs/300-fundamentals/300-theming.md +3 -0
- package/dist/docs/300-fundamentals/400-server.md +3 -0
- package/dist/docs/300-fundamentals/500-client.md +3 -0
- package/dist/docs/300-fundamentals/600-operations.md +3 -0
- package/dist/docs/300-fundamentals/700-resources.md +3 -0
- package/dist/docs/300-fundamentals/800-streams.md +3 -0
- package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/dist/docs/400-reference/100-configuration.md +3 -0
- package/dist/docs/400-reference/200-server-api.md +3 -0
- package/dist/docs/400-reference/300-client-api.md +3 -0
- package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
- package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
- package/dist/docs/400-reference/400-cli/300-call.md +28 -0
- package/dist/docs/400-reference/400-cli/400-check.md +20 -0
- package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
- package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/dist/docs/500-hosted-pages.md +31 -0
- package/dist/exports/client.d.ts +237 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +1643 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
- package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
- package/dist/exports/index.d.ts +2 -0
- package/dist/exports/index.js +49 -0
- package/dist/exports/index.js.map +1 -0
- package/dist/exports/notifications-kDp16bU_.js +56 -0
- package/dist/exports/notifications-kDp16bU_.js.map +1 -0
- package/dist/exports/server.d.ts +63 -0
- package/dist/exports/server.d.ts.map +1 -0
- package/dist/exports/server.js +1120 -0
- package/dist/exports/server.js.map +1 -0
- package/dist/exports/style.css +368 -0
- package/dist/internal/server/coordinator/server-child.d.ts +1 -0
- package/dist/internal/server/coordinator/server-child.js +3299 -0
- package/dist/internal/server/coordinator/server-child.js.map +1 -0
- package/docs/000-index.md +21 -0
- package/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/docs/100-introduction/300-why-overmux.md +22 -0
- package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/docs/300-fundamentals/100-project-structure.md +3 -0
- package/docs/300-fundamentals/200-configuration.md +3 -0
- package/docs/300-fundamentals/300-theming.md +3 -0
- package/docs/300-fundamentals/400-server.md +3 -0
- package/docs/300-fundamentals/500-client.md +3 -0
- package/docs/300-fundamentals/600-operations.md +3 -0
- package/docs/300-fundamentals/700-resources.md +3 -0
- package/docs/300-fundamentals/800-streams.md +3 -0
- package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/docs/400-reference/100-configuration.md +3 -0
- package/docs/400-reference/200-server-api.md +3 -0
- package/docs/400-reference/300-client-api.md +3 -0
- package/docs/400-reference/400-cli/100-serve.md +33 -0
- package/docs/400-reference/400-cli/200-auth.md +52 -0
- package/docs/400-reference/400-cli/300-call.md +28 -0
- package/docs/400-reference/400-cli/400-check.md +20 -0
- package/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/docs/400-reference/400-cli/600-docs.md +23 -0
- package/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/docs/500-hosted-pages.md +31 -0
- package/package.json +59 -18
- package/src/internal/cli/app.ts +72 -0
- package/src/internal/cli/bin.ts +3 -0
- package/src/internal/cli/commands/ai.ts +44 -0
- package/src/internal/cli/commands/auth.ts +195 -0
- package/src/internal/cli/commands/call.ts +128 -0
- package/src/internal/cli/commands/check.ts +48 -0
- package/src/internal/cli/commands/desktop-installer.ts +446 -0
- package/src/internal/cli/commands/desktop-release.ts +190 -0
- package/src/internal/cli/commands/desktop.ts +47 -0
- package/src/internal/cli/commands/docs.ts +34 -0
- package/src/internal/cli/commands/serve.ts +322 -0
- package/src/internal/cli/environment.ts +54 -0
- package/src/internal/cli/login.ts +46 -0
- package/src/internal/cli/managed-vite.ts +267 -0
- package/src/internal/cli/parse-port.ts +8 -0
- package/src/internal/client/auth/auth-api.ts +53 -0
- package/src/internal/client/auth/auth-shell.css +74 -0
- package/src/internal/client/auth/auth-shell.tsx +167 -0
- package/src/internal/client/auth/entry.tsx +26 -0
- package/src/internal/client/auth/index.html +13 -0
- package/src/internal/client/background-notifications.ts +194 -0
- package/src/internal/client/browser-api.ts +97 -0
- package/src/internal/client/client-definition.ts +184 -0
- package/src/internal/client/commands.tsx +260 -0
- package/src/internal/client/host/browser-log-forwarding.ts +149 -0
- package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
- package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
- package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
- package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
- package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
- package/src/internal/client/host/notification-forwarding.ts +22 -0
- package/src/internal/client/host/overmux-host.tsx +200 -0
- package/src/internal/client/host/recovery-screen.css +75 -0
- package/src/internal/client/host/recovery-screen.tsx +17 -0
- package/src/internal/client/host/update-popover.css +82 -0
- package/src/internal/client/host/update-popover.tsx +166 -0
- package/src/internal/client/index.ts +45 -0
- package/src/internal/client/overmux-react.ts +283 -0
- package/src/internal/client/shortcuts.tsx +373 -0
- package/src/internal/client/theme-scope.css +114 -0
- package/src/internal/client/theme-scope.tsx +107 -0
- package/src/internal/client/transport.ts +69 -0
- package/src/internal/client/websocket.ts +441 -0
- package/src/internal/server/auth/auth-http.ts +414 -0
- package/src/internal/server/auth/auth-service.ts +627 -0
- package/src/internal/server/auth/auth-shell-http.ts +90 -0
- package/src/internal/server/auth/instance-control.ts +381 -0
- package/src/internal/server/check-overmux.ts +364 -0
- package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
- package/src/internal/server/coordinator/server-child.ts +108 -0
- package/src/internal/server/coordinator/server-coordinator.ts +262 -0
- package/src/internal/server/coordinator/update-watcher.ts +54 -0
- package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
- package/src/internal/server/http/background-notification-handlers.ts +83 -0
- package/src/internal/server/http/control-handlers.ts +44 -0
- package/src/internal/server/http/create-http-app.ts +53 -0
- package/src/internal/server/http/development-web-backend.ts +83 -0
- package/src/internal/server/http/operation-handler.ts +93 -0
- package/src/internal/server/http/request-logging.ts +34 -0
- package/src/internal/server/http/routes.ts +241 -0
- package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
- package/src/internal/server/index.ts +16 -0
- package/src/internal/server/notifications/background-notification-payload.ts +74 -0
- package/src/internal/server/notifications/background-notification-service.ts +139 -0
- package/src/internal/server/notifications/background-notification-store.ts +162 -0
- package/src/internal/server/notifications/notification-service.ts +70 -0
- package/src/internal/server/notifications/private-json-file.ts +84 -0
- package/src/internal/server/paths.ts +13 -0
- package/src/internal/server/rate-limiter.ts +58 -0
- package/src/internal/server/runtime/create-runtime.ts +87 -0
- package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
- package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
- package/src/internal/server/runtime/runtime-operations.ts +78 -0
- package/src/internal/server/runtime/runtime-resources.ts +201 -0
- package/src/internal/server/runtime/runtime-streams.ts +130 -0
- package/src/internal/server/server-logger.ts +110 -0
- package/src/internal/server/server-startup-options.ts +51 -0
- package/src/internal/server/start-application-server.ts +412 -0
- package/src/internal/server/start-overmux-server.ts +26 -0
- package/src/internal/server/test-port.ts +23 -0
- package/src/internal/server/vite-websocket-proxy.ts +201 -0
- package/src/internal/server/websocket-connection.ts +586 -0
- package/src/internal/server/websocket-limits.ts +2 -0
- package/src/internal/server/websocket-server.ts +311 -0
- package/src/internal/shared/background-notifications.ts +37 -0
- package/src/internal/shared/index.ts +2 -0
- package/src/internal/shared/protocol.ts +317 -0
- package/src/internal/shared/routes.ts +3 -0
- package/src/public/ai-context.ts +44 -0
- package/src/public/client.ts +53 -0
- package/src/public/config.ts +287 -0
- package/src/public/contracts.ts +55 -0
- package/src/public/index.ts +57 -0
- package/src/public/notifications.ts +10 -0
- package/src/public/server.ts +16 -0
- package/dist/public/core.ts +0 -119
- package/dist/public/protocol.ts +0 -51
- package/dist/public/terminal.ts +0 -23
- package/dist/web/assets/index-DC___IUw.css +0 -2
- package/dist/web/assets/index-DF9Cptse.js +0 -74
- package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
- package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
- package/dist/web/index.html +0 -14
|
@@ -0,0 +1,1643 @@
|
|
|
1
|
+
import "./style.css";
|
|
2
|
+
import { i as overmuxLogoutPath, r as notificationSchema, s as resolveOvermuxReturnTo } from "./notifications-kDp16bU_.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { Component, createContext, createElement, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
6
|
+
import { formatKeyBinding, formatKeyBinding as formatKeyBinding$1, keyBindingSchema, keyBindingSchema as keyBindingSchema$1, matchesKeyBinding, matchesKeyBinding as matchesKeyBinding$1, platformBinding } from "@overmux/keybindings";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/internal/shared/background-notifications.ts
|
|
10
|
+
const deliveryKeySchema = z.string().min(1).max(1024).regex(/^[\w-]+$/u);
|
|
11
|
+
const backgroundNotificationSubscriptionSchema = z.object({
|
|
12
|
+
endpoint: z.url().max(4096).refine((endpoint) => {
|
|
13
|
+
const url = new URL(endpoint);
|
|
14
|
+
return url.protocol === "https:" && !url.username && !url.password;
|
|
15
|
+
}),
|
|
16
|
+
expirationTime: z.number().int().nonnegative().nullable(),
|
|
17
|
+
keys: z.object({
|
|
18
|
+
auth: deliveryKeySchema,
|
|
19
|
+
p256dh: deliveryKeySchema
|
|
20
|
+
}).strict()
|
|
21
|
+
}).strict();
|
|
22
|
+
const backgroundNotificationPublicKeyResponseSchema = z.object({ publicKey: deliveryKeySchema }).strict();
|
|
23
|
+
z.object({ endpoint: backgroundNotificationSubscriptionSchema.shape.endpoint }).strict();
|
|
24
|
+
const webSocketCloseCode = {
|
|
25
|
+
authenticationRevoked: 4001,
|
|
26
|
+
authenticationUnavailable: 4002
|
|
27
|
+
};
|
|
28
|
+
const runtimeManifestSchema = z.object({
|
|
29
|
+
debug: z.boolean(),
|
|
30
|
+
operations: z.array(z.string().min(1)),
|
|
31
|
+
protocolVersion: z.literal(9),
|
|
32
|
+
resources: z.array(z.string().min(1)),
|
|
33
|
+
streams: z.array(z.string().min(1))
|
|
34
|
+
});
|
|
35
|
+
const updateAvailableEventSchema = z.object({ type: z.literal("update-available") });
|
|
36
|
+
const restartingEventSchema = z.object({ type: z.literal("restarting") });
|
|
37
|
+
z.discriminatedUnion("type", [updateAvailableEventSchema, restartingEventSchema]);
|
|
38
|
+
const notificationEventSchema = z.object({
|
|
39
|
+
notification: notificationSchema,
|
|
40
|
+
type: z.literal("notification")
|
|
41
|
+
}).strict();
|
|
42
|
+
const operationIdSchema = z.string().min(1).max(256);
|
|
43
|
+
const registrationNameSchema = z.string().min(1).max(256);
|
|
44
|
+
const resourceReadMessageSchema = z.object({
|
|
45
|
+
input: z.unknown().optional(),
|
|
46
|
+
operationId: operationIdSchema,
|
|
47
|
+
resourceName: registrationNameSchema,
|
|
48
|
+
type: z.literal("resource-read")
|
|
49
|
+
});
|
|
50
|
+
const resourceSubscribeMessageSchema = z.object({
|
|
51
|
+
input: z.unknown().optional(),
|
|
52
|
+
operationId: operationIdSchema,
|
|
53
|
+
resourceName: registrationNameSchema,
|
|
54
|
+
subscriptionId: operationIdSchema,
|
|
55
|
+
type: z.literal("resource-subscribe")
|
|
56
|
+
});
|
|
57
|
+
const resourceUnsubscribeMessageSchema = z.object({
|
|
58
|
+
subscriptionId: operationIdSchema,
|
|
59
|
+
type: z.literal("resource-unsubscribe")
|
|
60
|
+
});
|
|
61
|
+
const streamOpenMessageSchema = z.object({
|
|
62
|
+
input: z.unknown().optional(),
|
|
63
|
+
operationId: operationIdSchema,
|
|
64
|
+
streamId: operationIdSchema,
|
|
65
|
+
streamName: registrationNameSchema,
|
|
66
|
+
type: z.literal("stream-open")
|
|
67
|
+
});
|
|
68
|
+
const streamMessageSchema = z.object({
|
|
69
|
+
message: z.unknown(),
|
|
70
|
+
streamId: operationIdSchema,
|
|
71
|
+
type: z.literal("stream-message")
|
|
72
|
+
});
|
|
73
|
+
const streamCloseMessageSchema = z.object({
|
|
74
|
+
streamId: operationIdSchema,
|
|
75
|
+
type: z.literal("stream-close")
|
|
76
|
+
});
|
|
77
|
+
const clientDiagnosticEventSchema = z.object({
|
|
78
|
+
arguments: z.string().max(16100),
|
|
79
|
+
level: z.enum([
|
|
80
|
+
"log",
|
|
81
|
+
"info",
|
|
82
|
+
"warn",
|
|
83
|
+
"error",
|
|
84
|
+
"debug"
|
|
85
|
+
]),
|
|
86
|
+
message: z.string().max(4100),
|
|
87
|
+
stack: z.string().max(8100).optional(),
|
|
88
|
+
timestamp: z.iso.datetime(),
|
|
89
|
+
type: z.literal("client-diagnostic"),
|
|
90
|
+
url: z.string().max(2100).optional()
|
|
91
|
+
});
|
|
92
|
+
const clientProtocolMessageSchema = z.discriminatedUnion("type", [
|
|
93
|
+
resourceReadMessageSchema,
|
|
94
|
+
resourceSubscribeMessageSchema,
|
|
95
|
+
resourceUnsubscribeMessageSchema,
|
|
96
|
+
streamOpenMessageSchema,
|
|
97
|
+
streamMessageSchema,
|
|
98
|
+
streamCloseMessageSchema,
|
|
99
|
+
clientDiagnosticEventSchema
|
|
100
|
+
]);
|
|
101
|
+
const resourceResultMessageSchema = z.object({
|
|
102
|
+
operationId: operationIdSchema,
|
|
103
|
+
output: z.unknown(),
|
|
104
|
+
type: z.literal("resource-result")
|
|
105
|
+
});
|
|
106
|
+
const resourceInvalidatedMessageSchema = z.object({
|
|
107
|
+
subscriptionId: operationIdSchema,
|
|
108
|
+
type: z.literal("resource-invalidated")
|
|
109
|
+
});
|
|
110
|
+
const streamOpenedMessageSchema = z.object({
|
|
111
|
+
operationId: operationIdSchema,
|
|
112
|
+
streamId: operationIdSchema,
|
|
113
|
+
type: z.literal("stream-opened")
|
|
114
|
+
});
|
|
115
|
+
const streamOutputMessageSchema = z.object({
|
|
116
|
+
message: z.unknown(),
|
|
117
|
+
streamId: operationIdSchema,
|
|
118
|
+
type: z.literal("stream-output")
|
|
119
|
+
});
|
|
120
|
+
const streamClosedMessageSchema = z.object({
|
|
121
|
+
streamId: operationIdSchema,
|
|
122
|
+
type: z.literal("stream-closed")
|
|
123
|
+
});
|
|
124
|
+
const protocolErrorMessageSchema = z.object({
|
|
125
|
+
code: z.enum([
|
|
126
|
+
"bad-request",
|
|
127
|
+
"conflict",
|
|
128
|
+
"internal",
|
|
129
|
+
"not-found"
|
|
130
|
+
]),
|
|
131
|
+
message: z.string(),
|
|
132
|
+
operationId: operationIdSchema.optional(),
|
|
133
|
+
streamId: operationIdSchema.optional(),
|
|
134
|
+
subscriptionId: operationIdSchema.optional(),
|
|
135
|
+
type: z.literal("error")
|
|
136
|
+
});
|
|
137
|
+
const serverProtocolMessageSchema = z.discriminatedUnion("type", [
|
|
138
|
+
resourceResultMessageSchema,
|
|
139
|
+
resourceInvalidatedMessageSchema,
|
|
140
|
+
streamOpenedMessageSchema,
|
|
141
|
+
streamOutputMessageSchema,
|
|
142
|
+
streamClosedMessageSchema,
|
|
143
|
+
protocolErrorMessageSchema,
|
|
144
|
+
updateAvailableEventSchema,
|
|
145
|
+
restartingEventSchema,
|
|
146
|
+
notificationEventSchema
|
|
147
|
+
]);
|
|
148
|
+
const byteMarker = "$overmuxBytes";
|
|
149
|
+
const encodeValue = (value) => {
|
|
150
|
+
if (value instanceof Uint8Array) return { [byteMarker]: Array.from(value) };
|
|
151
|
+
if (Array.isArray(value)) return value.map(encodeValue);
|
|
152
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, encodeValue(item)]));
|
|
153
|
+
return value;
|
|
154
|
+
};
|
|
155
|
+
const decodeValue = (value) => {
|
|
156
|
+
if (Array.isArray(value)) return value.map(decodeValue);
|
|
157
|
+
if (typeof value !== "object" || value === null) return value;
|
|
158
|
+
const record = value;
|
|
159
|
+
if (Object.keys(record).length === 1 && Array.isArray(record[byteMarker]) && record[byteMarker].every((item) => Number.isInteger(item) && item >= 0 && item <= 255)) return Uint8Array.from(record[byteMarker]);
|
|
160
|
+
return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, decodeValue(item)]));
|
|
161
|
+
};
|
|
162
|
+
const binaryMarker = "$overmuxBinary";
|
|
163
|
+
const binaryMagic = 1330468865;
|
|
164
|
+
const decodeBinaryValue = (value, payload) => {
|
|
165
|
+
if (Array.isArray(value)) return value.map((item) => decodeBinaryValue(item, payload));
|
|
166
|
+
if (typeof value !== "object" || value === null) return value;
|
|
167
|
+
const record = value;
|
|
168
|
+
const marker = record[binaryMarker];
|
|
169
|
+
if (Object.keys(record).length === 1 && Array.isArray(marker) && marker.length === 2 && marker.every((item) => Number.isInteger(item) && item >= 0)) {
|
|
170
|
+
const [offset, length] = marker;
|
|
171
|
+
if (offset + length > payload.byteLength) throw new Error("Invalid Overmux binary payload range");
|
|
172
|
+
return payload.slice(offset, offset + length);
|
|
173
|
+
}
|
|
174
|
+
return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, decodeBinaryValue(item, payload)]));
|
|
175
|
+
};
|
|
176
|
+
const encodeProtocolMessage = (message) => JSON.stringify(encodeValue(message));
|
|
177
|
+
const decodeProtocolMessage = (message) => decodeValue(JSON.parse(message));
|
|
178
|
+
const decodeProtocolFrame = (frame) => {
|
|
179
|
+
if (typeof frame === "string") return decodeProtocolMessage(frame);
|
|
180
|
+
const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame, 0, frame.byteLength);
|
|
181
|
+
if (bytes.byteLength < 8) throw new Error("Invalid Overmux binary frame");
|
|
182
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
183
|
+
if (view.getUint32(0) !== binaryMagic) throw new Error("Invalid Overmux binary frame magic");
|
|
184
|
+
const headerLength = view.getUint32(4);
|
|
185
|
+
if (8 + headerLength > bytes.byteLength) throw new Error("Invalid Overmux binary frame header");
|
|
186
|
+
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)));
|
|
187
|
+
return decodeBinaryValue(header, bytes.subarray(8 + headerLength));
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/internal/client/background-notifications.ts
|
|
191
|
+
const enabledStorageKey = "overmux.backgroundNotificationsEnabled";
|
|
192
|
+
const isIos = () => /iPad|iPhone|iPod/u.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
|
|
193
|
+
const isStandalone = () => window.matchMedia("(display-mode: standalone)").matches || Boolean(navigator.standalone);
|
|
194
|
+
const supportStatus = () => {
|
|
195
|
+
if (window.overmuxHost?.notifications) return "unsupported";
|
|
196
|
+
if (isIos() && !isStandalone()) return "ios-home-screen";
|
|
197
|
+
if (!window.isSecureContext || !("Notification" in window) || !("serviceWorker" in navigator) || !("PushManager" in window)) return "unsupported";
|
|
198
|
+
};
|
|
199
|
+
const decodeApplicationServerKey = (value) => {
|
|
200
|
+
const padded = `${value}${"=".repeat((4 - value.length % 4) % 4)}`;
|
|
201
|
+
return Uint8Array.from(atob(padded.replaceAll("-", "+").replaceAll("_", "/")), (character) => character.charCodeAt(0));
|
|
202
|
+
};
|
|
203
|
+
const encodeApplicationServerKey = (value) => {
|
|
204
|
+
if (!value) return;
|
|
205
|
+
const binary = String.fromCharCode(...new Uint8Array(value));
|
|
206
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
207
|
+
};
|
|
208
|
+
const request = async (path, init) => {
|
|
209
|
+
const response = await fetch(path, {
|
|
210
|
+
cache: "no-store",
|
|
211
|
+
...init
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) throw new Error(`Background notification request failed: ${response.status}`);
|
|
214
|
+
return response;
|
|
215
|
+
};
|
|
216
|
+
const getPublicKey = async () => {
|
|
217
|
+
const response = await request("/api/background-notifications/public-key");
|
|
218
|
+
return backgroundNotificationPublicKeyResponseSchema.parse(await response.json()).publicKey;
|
|
219
|
+
};
|
|
220
|
+
const serializeSubscription = (subscription) => {
|
|
221
|
+
const json = subscription.toJSON();
|
|
222
|
+
return backgroundNotificationSubscriptionSchema.parse({
|
|
223
|
+
endpoint: subscription.endpoint,
|
|
224
|
+
expirationTime: subscription.expirationTime ?? null,
|
|
225
|
+
keys: json.keys
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
const enableSubscription = async (subscription) => {
|
|
229
|
+
await request("/api/background-notifications/subscription", {
|
|
230
|
+
body: JSON.stringify(serializeSubscription(subscription)),
|
|
231
|
+
headers: { "Content-Type": "application/json" },
|
|
232
|
+
method: "PUT"
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
const disableSubscription = async (subscription) => {
|
|
236
|
+
await request("/api/background-notifications/subscription", {
|
|
237
|
+
body: JSON.stringify({ endpoint: subscription.endpoint }),
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
method: "DELETE"
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
const subscribe = async (registration, publicKey) => registration.pushManager.subscribe({
|
|
243
|
+
applicationServerKey: decodeApplicationServerKey(publicKey),
|
|
244
|
+
userVisibleOnly: true
|
|
245
|
+
});
|
|
246
|
+
const canManageSubscription = () => window.isSecureContext && "serviceWorker" in navigator && "PushManager" in window;
|
|
247
|
+
const removeCurrentSubscription = async () => {
|
|
248
|
+
if (!canManageSubscription()) return;
|
|
249
|
+
const subscription = await (await navigator.serviceWorker.getRegistration())?.pushManager.getSubscription();
|
|
250
|
+
if (!subscription) return;
|
|
251
|
+
await disableSubscription(subscription);
|
|
252
|
+
if (!await subscription.unsubscribe()) throw new Error("The background subscription could not be removed");
|
|
253
|
+
};
|
|
254
|
+
const reconcileBackgroundNotifications = async () => {
|
|
255
|
+
const registration = await navigator.serviceWorker.ready;
|
|
256
|
+
const publicKey = await getPublicKey();
|
|
257
|
+
let subscription = await registration.pushManager.getSubscription();
|
|
258
|
+
const subscriptionKey = subscription ? encodeApplicationServerKey(subscription.options.applicationServerKey) : void 0;
|
|
259
|
+
if (subscription && subscriptionKey !== publicKey) {
|
|
260
|
+
await disableSubscription(subscription);
|
|
261
|
+
if (!await subscription.unsubscribe()) throw new Error("The previous background subscription is still active");
|
|
262
|
+
subscription = null;
|
|
263
|
+
}
|
|
264
|
+
if (!subscription) subscription = await subscribe(registration, publicKey);
|
|
265
|
+
await enableSubscription(subscription);
|
|
266
|
+
};
|
|
267
|
+
const enableBackgroundNotifications = async () => {
|
|
268
|
+
if (supportStatus()) {
|
|
269
|
+
localStorage.removeItem(enabledStorageKey);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if ((Notification.permission === "default" ? await Notification.requestPermission() : Notification.permission) !== "granted") {
|
|
273
|
+
localStorage.removeItem(enabledStorageKey);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
await reconcileBackgroundNotifications();
|
|
277
|
+
localStorage.setItem(enabledStorageKey, "true");
|
|
278
|
+
return true;
|
|
279
|
+
};
|
|
280
|
+
const disableBackgroundNotifications = async () => {
|
|
281
|
+
localStorage.removeItem(enabledStorageKey);
|
|
282
|
+
await removeCurrentSubscription();
|
|
283
|
+
};
|
|
284
|
+
const reconciledStatus = async () => {
|
|
285
|
+
const unavailable = supportStatus();
|
|
286
|
+
if (unavailable) {
|
|
287
|
+
await disableBackgroundNotifications().catch(() => void 0);
|
|
288
|
+
return unavailable;
|
|
289
|
+
}
|
|
290
|
+
if (Notification.permission !== "granted") {
|
|
291
|
+
await disableBackgroundNotifications().catch(() => void 0);
|
|
292
|
+
return Notification.permission === "denied" ? "denied" : "disabled";
|
|
293
|
+
}
|
|
294
|
+
if (localStorage.getItem(enabledStorageKey) !== "true") {
|
|
295
|
+
await disableBackgroundNotifications();
|
|
296
|
+
return "disabled";
|
|
297
|
+
}
|
|
298
|
+
await reconcileBackgroundNotifications();
|
|
299
|
+
return "enabled";
|
|
300
|
+
};
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/internal/client/client-definition.ts
|
|
303
|
+
const shortcutBindingSchema = z.union([keyBindingSchema$1, z.array(keyBindingSchema$1).min(2)]);
|
|
304
|
+
const commandBindingSchema = z.union([shortcutBindingSchema, z.object({
|
|
305
|
+
binding: shortcutBindingSchema,
|
|
306
|
+
when: z.object({ media: z.string().trim().min(1) })
|
|
307
|
+
})]);
|
|
308
|
+
const chordPrefixSchema = z.object({
|
|
309
|
+
binding: keyBindingSchema$1,
|
|
310
|
+
unmatched: z.literal("replay-to-focused-input")
|
|
311
|
+
});
|
|
312
|
+
const formatShortcutBinding = (binding) => typeof binding === "string" ? formatKeyBinding$1(binding) : binding.map(formatKeyBinding$1).join(" ");
|
|
313
|
+
const validateBindings = (bindings) => bindings?.forEach((binding) => commandBindingSchema.parse(binding));
|
|
314
|
+
const defineCommandRegistry = () => (commands) => Object.fromEntries(Object.entries(commands).map(([id, command]) => {
|
|
315
|
+
validateBindings(command.defaultBindings);
|
|
316
|
+
return [id, {
|
|
317
|
+
...command,
|
|
318
|
+
id
|
|
319
|
+
}];
|
|
320
|
+
}));
|
|
321
|
+
const defineOvermuxClient = (definition) => {
|
|
322
|
+
definition.chordPrefixes?.forEach((prefix) => chordPrefixSchema.parse(prefix));
|
|
323
|
+
Object.values(definition.shortcutOverrides ?? {}).forEach(validateBindings);
|
|
324
|
+
return definition;
|
|
325
|
+
};
|
|
326
|
+
const isConditionalShortcutBinding = (binding) => typeof binding !== "string" && !Array.isArray(binding);
|
|
327
|
+
const resolveShortcutBinding = (binding) => isConditionalShortcutBinding(binding) ? binding.binding : binding;
|
|
328
|
+
const configuredCommandBindings = (definition, command) => definition.shortcutOverrides?.[command.id] ?? command.defaultBindings ?? [];
|
|
329
|
+
const activeCommandBindings = (bindings, matchesMedia) => bindings.flatMap((binding) => !isConditionalShortcutBinding(binding) || matchesMedia(binding.when.media) ? [resolveShortcutBinding(binding)] : []);
|
|
330
|
+
const shortcutMediaQueries = (definition) => [...new Set(Object.values(definition.commands).flatMap((command) => configuredCommandBindings(definition, command).flatMap((binding) => isConditionalShortcutBinding(binding) ? [binding.when.media] : [])))];
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/internal/client/commands.tsx
|
|
333
|
+
const skipToken = Symbol("overmux command registration");
|
|
334
|
+
const RuntimeContext = createContext(void 0);
|
|
335
|
+
const useRuntime = () => {
|
|
336
|
+
const runtime = useContext(RuntimeContext);
|
|
337
|
+
if (!runtime) throw new Error("Overmux hooks must be used inside an active Overmux client runtime");
|
|
338
|
+
return runtime;
|
|
339
|
+
};
|
|
340
|
+
const executeRegistration = async (command, registration, overmuxServerApi) => {
|
|
341
|
+
const params = command.params ? command.params.parse(registration.params) : void 0;
|
|
342
|
+
if (registration.run) {
|
|
343
|
+
await registration.run();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (typeof command.run !== "function") throw new Error(`Command has no implementation: ${command.id}`);
|
|
347
|
+
await command.run({
|
|
348
|
+
overmuxServerApi,
|
|
349
|
+
params
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
const createRegisteredCommand = ({ command, getRegistration, overmuxServerApi }) => ({
|
|
353
|
+
command,
|
|
354
|
+
element: () => {
|
|
355
|
+
const registration = getRegistration();
|
|
356
|
+
return registration === skipToken ? void 0 : registration.element?.current;
|
|
357
|
+
},
|
|
358
|
+
enabled: () => {
|
|
359
|
+
const registration = getRegistration();
|
|
360
|
+
return registration === skipToken ? false : registration.enabled ?? true;
|
|
361
|
+
},
|
|
362
|
+
execute: () => {
|
|
363
|
+
const registration = getRegistration();
|
|
364
|
+
return registration === skipToken ? Promise.resolve() : executeRegistration(command, registration, overmuxServerApi);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
const useCommand = (command, registration) => {
|
|
368
|
+
const { overmuxServerApi, refreshCommands, registerCommand } = useRuntime();
|
|
369
|
+
const registrationRef = useRef(registration);
|
|
370
|
+
registrationRef.current = registration;
|
|
371
|
+
const skipped = registration === skipToken;
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (skipped) return;
|
|
374
|
+
return registerCommand(createRegisteredCommand({
|
|
375
|
+
command,
|
|
376
|
+
getRegistration: () => registrationRef.current,
|
|
377
|
+
overmuxServerApi
|
|
378
|
+
}));
|
|
379
|
+
}, [
|
|
380
|
+
command,
|
|
381
|
+
overmuxServerApi,
|
|
382
|
+
registerCommand,
|
|
383
|
+
skipped
|
|
384
|
+
]);
|
|
385
|
+
const enabled = skipped ? false : registration.enabled ?? true;
|
|
386
|
+
useEffect(refreshCommands, [
|
|
387
|
+
skipped ? void 0 : registration.element,
|
|
388
|
+
enabled,
|
|
389
|
+
refreshCommands,
|
|
390
|
+
skipped
|
|
391
|
+
]);
|
|
392
|
+
};
|
|
393
|
+
const elementDepth$1 = (element) => {
|
|
394
|
+
let depth = 0;
|
|
395
|
+
for (let current = element; current; current = current.parentElement) depth += 1;
|
|
396
|
+
return depth;
|
|
397
|
+
};
|
|
398
|
+
const selectCommandRegistration = (command, registrations, activeElement) => {
|
|
399
|
+
const candidates = [...registrations].filter((registration) => registration.command === command);
|
|
400
|
+
return candidates.filter((registration) => {
|
|
401
|
+
const element = registration.element();
|
|
402
|
+
return Boolean(element && activeElement && element.contains(activeElement));
|
|
403
|
+
}).reduce((selected, candidate) => {
|
|
404
|
+
const selectedElement = selected?.element();
|
|
405
|
+
const candidateElement = candidate.element();
|
|
406
|
+
if (!selectedElement || !candidateElement) return candidate;
|
|
407
|
+
return elementDepth$1(candidateElement) >= elementDepth$1(selectedElement) ? candidate : selected;
|
|
408
|
+
}, void 0) ?? candidates.at(-1);
|
|
409
|
+
};
|
|
410
|
+
const currentActiveElement = () => document.activeElement;
|
|
411
|
+
const selectedRegistration = (command, registrations, getActiveElement = currentActiveElement) => selectCommandRegistration(command, registrations, getActiveElement());
|
|
412
|
+
const executeCommand = async (command, registrations, getActiveElement = currentActiveElement) => {
|
|
413
|
+
const selected = selectedRegistration(command, registrations, getActiveElement);
|
|
414
|
+
if (selected?.enabled()) await selected.execute();
|
|
415
|
+
};
|
|
416
|
+
const createCommandEntries = (definition, registrations, getActiveElement = currentActiveElement, matchesMedia = (media) => typeof window !== "undefined" && window.matchMedia(media).matches) => Object.values(definition.commands).flatMap((command) => {
|
|
417
|
+
const selected = selectedRegistration(command, registrations, getActiveElement);
|
|
418
|
+
return selected ? [{
|
|
419
|
+
bindings: activeCommandBindings(configuredCommandBindings(definition, command), matchesMedia),
|
|
420
|
+
enabled: selected.enabled(),
|
|
421
|
+
execute: () => executeCommand(command, registrations, getActiveElement),
|
|
422
|
+
id: command.id,
|
|
423
|
+
title: command.title
|
|
424
|
+
}] : [];
|
|
425
|
+
});
|
|
426
|
+
const CommandsContext = createContext([]);
|
|
427
|
+
const useCommands = () => useContext(CommandsContext);
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/internal/client/overmux-react.ts
|
|
430
|
+
const inputKey = (input) => JSON.stringify(input) ?? "";
|
|
431
|
+
const useCanonicalInput = (input) => {
|
|
432
|
+
const key = inputKey(input);
|
|
433
|
+
return useMemo(() => key ? JSON.parse(key) : void 0, [key]);
|
|
434
|
+
};
|
|
435
|
+
const useResourceById = (id, input, skipped) => {
|
|
436
|
+
const { manifest, overmuxServerApi } = useRuntime();
|
|
437
|
+
const queryClient = useQueryClient();
|
|
438
|
+
const canonicalInput = useCanonicalInput(input);
|
|
439
|
+
const queryKey = useMemo(() => [id, canonicalInput], [canonicalInput, id]);
|
|
440
|
+
const query = useQuery({
|
|
441
|
+
enabled: !skipped,
|
|
442
|
+
queryFn: () => overmuxServerApi.readResource({
|
|
443
|
+
id,
|
|
444
|
+
input: canonicalInput
|
|
445
|
+
}),
|
|
446
|
+
queryKey
|
|
447
|
+
});
|
|
448
|
+
useEffect(() => !skipped && manifest.resources.includes(id) ? overmuxServerApi.subscribeResource({
|
|
449
|
+
id,
|
|
450
|
+
input: canonicalInput,
|
|
451
|
+
onError: () => void queryClient.invalidateQueries({
|
|
452
|
+
exact: true,
|
|
453
|
+
queryKey
|
|
454
|
+
}),
|
|
455
|
+
onInvalidate: () => void queryClient.invalidateQueries({
|
|
456
|
+
exact: true,
|
|
457
|
+
queryKey
|
|
458
|
+
})
|
|
459
|
+
}) : void 0, [
|
|
460
|
+
manifest.resources,
|
|
461
|
+
canonicalInput,
|
|
462
|
+
id,
|
|
463
|
+
overmuxServerApi,
|
|
464
|
+
queryClient,
|
|
465
|
+
queryKey,
|
|
466
|
+
skipped
|
|
467
|
+
]);
|
|
468
|
+
const refetch = useCallback(() => {
|
|
469
|
+
queryClient.invalidateQueries({
|
|
470
|
+
exact: true,
|
|
471
|
+
queryKey
|
|
472
|
+
});
|
|
473
|
+
}, [queryClient, queryKey]);
|
|
474
|
+
if (skipped) return;
|
|
475
|
+
if (query.status === "pending") return {
|
|
476
|
+
refetch,
|
|
477
|
+
status: "pending"
|
|
478
|
+
};
|
|
479
|
+
if (query.status === "error") return query.data === void 0 ? {
|
|
480
|
+
error: query.error,
|
|
481
|
+
refetch,
|
|
482
|
+
status: "error"
|
|
483
|
+
} : {
|
|
484
|
+
data: query.data,
|
|
485
|
+
error: query.error,
|
|
486
|
+
refetch,
|
|
487
|
+
status: "error"
|
|
488
|
+
};
|
|
489
|
+
return {
|
|
490
|
+
data: query.data,
|
|
491
|
+
refetch,
|
|
492
|
+
status: "success"
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
const useOperationById = (id, signal) => {
|
|
496
|
+
const { overmuxServerApi } = useRuntime();
|
|
497
|
+
return useMutation({ mutationFn: (input) => overmuxServerApi.executeOperation(id, input, signal) });
|
|
498
|
+
};
|
|
499
|
+
const useStreamById = (id, input) => {
|
|
500
|
+
const { manifest, overmuxServerApi } = useRuntime();
|
|
501
|
+
const streamAvailable = manifest.streams.includes(id);
|
|
502
|
+
const [state, setState] = useState({ status: "opening" });
|
|
503
|
+
const listeners = useMemo(() => /* @__PURE__ */ new Set(), []);
|
|
504
|
+
const canonicalInput = useCanonicalInput(input);
|
|
505
|
+
const streamRef = useRef(void 0);
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
streamRef.current?.close();
|
|
508
|
+
streamRef.current = void 0;
|
|
509
|
+
if (!streamAvailable) {
|
|
510
|
+
setState({ status: "closed" });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
setState({ status: "opening" });
|
|
514
|
+
const stream = overmuxServerApi.openStream({
|
|
515
|
+
id,
|
|
516
|
+
input: canonicalInput,
|
|
517
|
+
onClose: () => setState({ status: "closed" }),
|
|
518
|
+
onError: (error) => setState({
|
|
519
|
+
error,
|
|
520
|
+
status: "closed"
|
|
521
|
+
}),
|
|
522
|
+
onOpen: () => setState({ status: "open" }),
|
|
523
|
+
onMessage: (message) => listeners.forEach((listener) => listener(message))
|
|
524
|
+
});
|
|
525
|
+
streamRef.current = stream;
|
|
526
|
+
return () => {
|
|
527
|
+
stream.close();
|
|
528
|
+
if (streamRef.current === stream) streamRef.current = void 0;
|
|
529
|
+
};
|
|
530
|
+
}, [
|
|
531
|
+
canonicalInput,
|
|
532
|
+
id,
|
|
533
|
+
listeners,
|
|
534
|
+
overmuxServerApi,
|
|
535
|
+
streamAvailable
|
|
536
|
+
]);
|
|
537
|
+
const close = useCallback(() => {
|
|
538
|
+
streamRef.current?.close();
|
|
539
|
+
streamRef.current = void 0;
|
|
540
|
+
setState({ status: "closed" });
|
|
541
|
+
}, []);
|
|
542
|
+
const send = useCallback((message) => streamRef.current?.send(message) ?? false, []);
|
|
543
|
+
const subscribe = useCallback((listener) => {
|
|
544
|
+
listeners.add(listener);
|
|
545
|
+
return () => listeners.delete(listener);
|
|
546
|
+
}, [listeners]);
|
|
547
|
+
return {
|
|
548
|
+
close,
|
|
549
|
+
error: streamAvailable ? state.error : /* @__PURE__ */ new Error(`Stream is not available: ${id}`),
|
|
550
|
+
send,
|
|
551
|
+
status: streamAvailable ? state.status : "closed",
|
|
552
|
+
subscribe
|
|
553
|
+
};
|
|
554
|
+
};
|
|
555
|
+
const createOvermuxHooks = () => ({
|
|
556
|
+
useOperation: ({ id, signal }) => {
|
|
557
|
+
return useOperationById(id, signal);
|
|
558
|
+
},
|
|
559
|
+
useResource: (({ id, input }) => useResourceById(id, input, input === skipToken)),
|
|
560
|
+
useStream: ({ id, input }) => useStreamById(id, input)
|
|
561
|
+
});
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/internal/client/theme-scope.tsx
|
|
564
|
+
const PortalRootContext = createContext(void 0);
|
|
565
|
+
const useOvermuxPortalContainer = (external) => {
|
|
566
|
+
const scopeRoot = useContext(PortalRootContext);
|
|
567
|
+
if (external) return external.container;
|
|
568
|
+
if (scopeRoot !== void 0) return scopeRoot;
|
|
569
|
+
return typeof document === "undefined" ? null : document.body;
|
|
570
|
+
};
|
|
571
|
+
const OvermuxPortal = ({ children, external }) => {
|
|
572
|
+
const container = useOvermuxPortalContainer(external);
|
|
573
|
+
return container ? createPortal(children, container) : null;
|
|
574
|
+
};
|
|
575
|
+
const OvermuxThemeScope = ({ children, className, contrast = "auto", scheme = "system", style }) => {
|
|
576
|
+
const [portalRoot, setPortalRoot] = useState(null);
|
|
577
|
+
const capturePortalRoot = useCallback((element) => {
|
|
578
|
+
setPortalRoot(element);
|
|
579
|
+
}, []);
|
|
580
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
581
|
+
className,
|
|
582
|
+
"data-om-contrast": contrast,
|
|
583
|
+
"data-om-scheme": scheme,
|
|
584
|
+
"data-om-scope": "",
|
|
585
|
+
style,
|
|
586
|
+
children: [/* @__PURE__ */ jsx(PortalRootContext.Provider, {
|
|
587
|
+
value: portalRoot,
|
|
588
|
+
children
|
|
589
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
590
|
+
"data-om-overlay-root": "",
|
|
591
|
+
ref: capturePortalRoot
|
|
592
|
+
})]
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/internal/client/browser-api.ts
|
|
597
|
+
const availableName = (names, kind, name) => {
|
|
598
|
+
if (!names.includes(name)) throw new Error(`${kind} is not available: ${name}`);
|
|
599
|
+
return name;
|
|
600
|
+
};
|
|
601
|
+
const createOvermuxServerApi = ({ manifest, transport }) => ({
|
|
602
|
+
executeOperation: (id, input, signal) => {
|
|
603
|
+
if (!manifest.operations.includes(id)) throw new Error(`Operation is not available: ${id}`);
|
|
604
|
+
return transport.invokeOperation({
|
|
605
|
+
input,
|
|
606
|
+
name: id,
|
|
607
|
+
signal
|
|
608
|
+
});
|
|
609
|
+
},
|
|
610
|
+
openStream: ({ id, ...input }) => transport.openStream({
|
|
611
|
+
...input,
|
|
612
|
+
streamName: availableName(manifest.streams, "Stream", id)
|
|
613
|
+
}),
|
|
614
|
+
readResource: ({ id, input }) => transport.readResource({
|
|
615
|
+
input,
|
|
616
|
+
resourceName: availableName(manifest.resources, "Resource", id)
|
|
617
|
+
}),
|
|
618
|
+
subscribeResource: ({ id, ...input }) => transport.subscribeResource({
|
|
619
|
+
...input,
|
|
620
|
+
resourceName: availableName(manifest.resources, "Resource", id)
|
|
621
|
+
})
|
|
622
|
+
});
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/internal/client/host/browser-log-forwarding.ts
|
|
625
|
+
const maxStringLength = 2e3;
|
|
626
|
+
const truncate = (value, length = maxStringLength) => value.length <= length ? value : `${value.slice(0, length)}…[truncated]`;
|
|
627
|
+
const serializeValue = (value, seen, depth = 0) => {
|
|
628
|
+
if (typeof value === "string") return truncate(value);
|
|
629
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
630
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
631
|
+
if (typeof value === "undefined" || typeof value === "symbol") return String(value);
|
|
632
|
+
if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
|
|
633
|
+
if (depth >= 4) return "[Max depth]";
|
|
634
|
+
if (typeof value !== "object") return truncate(String(value));
|
|
635
|
+
if (seen.has(value)) return "[Circular]";
|
|
636
|
+
seen.add(value);
|
|
637
|
+
if (value instanceof Error) return {
|
|
638
|
+
message: truncate(value.message),
|
|
639
|
+
name: value.name,
|
|
640
|
+
...value.stack ? { stack: truncate(value.stack, 8e3) } : {}
|
|
641
|
+
};
|
|
642
|
+
if (Array.isArray(value)) return value.slice(0, 25).map((item) => serializeValue(item, seen, depth + 1));
|
|
643
|
+
try {
|
|
644
|
+
return Object.fromEntries(Object.entries(value).slice(0, 25).map(([key, item]) => [key, serializeValue(item, seen, depth + 1)]));
|
|
645
|
+
} catch {
|
|
646
|
+
return `[Unserializable ${Object.prototype.toString.call(value)}]`;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
const serializeBrowserLogArguments = (values) => {
|
|
650
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
651
|
+
const serialized = values.map((value) => serializeValue(value, seen));
|
|
652
|
+
const json = JSON.stringify(serialized);
|
|
653
|
+
return truncate(json, 16e3);
|
|
654
|
+
};
|
|
655
|
+
const diagnosticUrl = () => {
|
|
656
|
+
try {
|
|
657
|
+
return `${location.origin}${location.pathname}`;
|
|
658
|
+
} catch {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const stackOf = (values) => {
|
|
663
|
+
const error = values.find((value) => value instanceof Error);
|
|
664
|
+
return error instanceof Error && error.stack ? truncate(error.stack, 8e3) : void 0;
|
|
665
|
+
};
|
|
666
|
+
const installBrowserLogForwarding = (report) => {
|
|
667
|
+
const methods = [
|
|
668
|
+
"log",
|
|
669
|
+
"info",
|
|
670
|
+
"warn",
|
|
671
|
+
"error",
|
|
672
|
+
"debug"
|
|
673
|
+
];
|
|
674
|
+
const originals = Object.fromEntries(methods.map((level) => [level, console[level].bind(console)]));
|
|
675
|
+
let reporting = false;
|
|
676
|
+
const emit = (level, values, stack) => {
|
|
677
|
+
if (reporting) return;
|
|
678
|
+
reporting = true;
|
|
679
|
+
try {
|
|
680
|
+
const serializedArguments = serializeBrowserLogArguments(values);
|
|
681
|
+
report({
|
|
682
|
+
arguments: serializedArguments,
|
|
683
|
+
level,
|
|
684
|
+
message: truncate(values.map((value) => typeof value === "string" ? value : serializedArguments).join(" "), 4e3),
|
|
685
|
+
...stack ? { stack: truncate(stack, 8e3) } : {},
|
|
686
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
687
|
+
type: "client-diagnostic",
|
|
688
|
+
...diagnosticUrl() ? { url: diagnosticUrl() } : {}
|
|
689
|
+
});
|
|
690
|
+
} finally {
|
|
691
|
+
reporting = false;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
methods.forEach((level) => {
|
|
695
|
+
console[level] = (...values) => {
|
|
696
|
+
originals[level](...values);
|
|
697
|
+
emit(level, values, stackOf(values));
|
|
698
|
+
};
|
|
699
|
+
});
|
|
700
|
+
const onError = (event) => emit("error", [event.message], event.error?.stack);
|
|
701
|
+
const onRejection = (event) => emit("error", ["Unhandled rejection", event.reason], stackOf([event.reason]));
|
|
702
|
+
window.addEventListener("error", onError);
|
|
703
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
704
|
+
return () => {
|
|
705
|
+
methods.forEach((level) => {
|
|
706
|
+
console[level] = originals[level];
|
|
707
|
+
});
|
|
708
|
+
window.removeEventListener("error", onError);
|
|
709
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
710
|
+
};
|
|
711
|
+
};
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/internal/client/host/notification-forwarding.ts
|
|
714
|
+
const installNotificationForwarding = (transport) => transport.subscribeNotification((event) => {
|
|
715
|
+
const notification = notificationEventSchema.safeParse(event);
|
|
716
|
+
if (notification.success) window.overmuxHost?.notifications?.show(notification.data);
|
|
717
|
+
});
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/internal/client/shortcuts.tsx
|
|
720
|
+
const bindingSequence = (binding) => typeof binding === "string" ? [binding] : [...binding];
|
|
721
|
+
const normalizedBinding = (binding) => bindingSequence(binding).map((key) => platformBinding(key));
|
|
722
|
+
const sequenceMatches = (binding, pending, event) => {
|
|
723
|
+
const sequence = normalizedBinding(binding);
|
|
724
|
+
return sequence.length > pending.length && pending.every((key, index) => sequence[index] === key) && matchesKeyBinding$1(sequence[pending.length], event);
|
|
725
|
+
};
|
|
726
|
+
const isTextInput = (element) => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLElement && (element.isContentEditable || element.getAttribute("role") === "textbox");
|
|
727
|
+
const acceptsTextInput = (bindings) => {
|
|
728
|
+
if (!(document.activeElement instanceof Element)) return false;
|
|
729
|
+
const terminal = document.activeElement.closest(".xterm");
|
|
730
|
+
if (!terminal) return false;
|
|
731
|
+
if (terminal.closest("[data-copy-mode]")) return true;
|
|
732
|
+
return bindings.some((binding) => {
|
|
733
|
+
const sequence = bindingSequence(binding);
|
|
734
|
+
return sequence.length > 1 || sequence[0]?.startsWith("F") || sequence[0]?.includes("+");
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
const flattenShortcuts = (definition, matchesMedia) => Object.values(definition.commands).flatMap((command) => {
|
|
738
|
+
const bindings = activeCommandBindings(configuredCommandBindings(definition, command), matchesMedia);
|
|
739
|
+
return bindings.length ? [{
|
|
740
|
+
bindings,
|
|
741
|
+
command
|
|
742
|
+
}] : [];
|
|
743
|
+
});
|
|
744
|
+
const canRunShortcut = (shortcut, event, registrations) => {
|
|
745
|
+
const selected = selectCommandRegistration(shortcut.command, registrations, document.activeElement);
|
|
746
|
+
return Boolean(selected?.enabled() && !event.repeat && !(isTextInput(document.activeElement) && !acceptsTextInput(shortcut.bindings)));
|
|
747
|
+
};
|
|
748
|
+
const runShortcut = (shortcut, event, registrations) => {
|
|
749
|
+
if (!canRunShortcut(shortcut, event, registrations)) return false;
|
|
750
|
+
executeCommand(shortcut.command, registrations);
|
|
751
|
+
event.preventDefault();
|
|
752
|
+
return true;
|
|
753
|
+
};
|
|
754
|
+
const ShortcutInputTargetsContext = createContext(void 0);
|
|
755
|
+
const useShortcutInputTarget = (target) => {
|
|
756
|
+
const register = useContext(ShortcutInputTargetsContext);
|
|
757
|
+
const { container, input } = target;
|
|
758
|
+
useEffect(() => register?.({
|
|
759
|
+
container,
|
|
760
|
+
input
|
|
761
|
+
}), [
|
|
762
|
+
container,
|
|
763
|
+
input,
|
|
764
|
+
register
|
|
765
|
+
]);
|
|
766
|
+
};
|
|
767
|
+
const elementDepth = (element) => {
|
|
768
|
+
let depth = 0;
|
|
769
|
+
for (let current = element; current; current = current.parentElement) depth += 1;
|
|
770
|
+
return depth;
|
|
771
|
+
};
|
|
772
|
+
const selectShortcutInputTarget = (targets, activeElement) => [...targets].filter((target) => {
|
|
773
|
+
const container = target.container.current;
|
|
774
|
+
return Boolean(container && activeElement && container.contains(activeElement));
|
|
775
|
+
}).reduce((selected, candidate) => {
|
|
776
|
+
const selectedElement = selected?.container.current;
|
|
777
|
+
const candidateElement = candidate.container.current;
|
|
778
|
+
if (!selectedElement || !candidateElement) return candidate;
|
|
779
|
+
return elementDepth(candidateElement) >= elementDepth(selectedElement) ? candidate : selected;
|
|
780
|
+
}, void 0);
|
|
781
|
+
const replayedKeyboardEvents = /* @__PURE__ */ new WeakSet();
|
|
782
|
+
const keyboardEventProperties = [
|
|
783
|
+
"charCode",
|
|
784
|
+
"keyCode",
|
|
785
|
+
"which"
|
|
786
|
+
];
|
|
787
|
+
const replayKeyboardEvent = (target, source) => {
|
|
788
|
+
const replay = new KeyboardEvent(source.type, {
|
|
789
|
+
altKey: source.altKey,
|
|
790
|
+
bubbles: true,
|
|
791
|
+
cancelable: true,
|
|
792
|
+
code: source.code,
|
|
793
|
+
composed: true,
|
|
794
|
+
ctrlKey: source.ctrlKey,
|
|
795
|
+
key: source.key,
|
|
796
|
+
location: source.location,
|
|
797
|
+
metaKey: source.metaKey,
|
|
798
|
+
repeat: source.repeat,
|
|
799
|
+
shiftKey: source.shiftKey
|
|
800
|
+
});
|
|
801
|
+
keyboardEventProperties.forEach((property) => Object.defineProperty(replay, property, { value: source[property] }));
|
|
802
|
+
replayedKeyboardEvents.add(replay);
|
|
803
|
+
target.dispatchEvent(replay);
|
|
804
|
+
};
|
|
805
|
+
const replayChord = (target, events) => {
|
|
806
|
+
const input = target?.input.current;
|
|
807
|
+
if (!input) return;
|
|
808
|
+
events.forEach((event) => replayKeyboardEvent(input, event));
|
|
809
|
+
};
|
|
810
|
+
const ShortcutHost = ({ children, definition, registrations, registrationRevision }) => {
|
|
811
|
+
const [, setFocusRevision] = useState(0);
|
|
812
|
+
const [inputTargets] = useState(() => /* @__PURE__ */ new Set());
|
|
813
|
+
const [mediaRevision, setMediaRevision] = useState(0);
|
|
814
|
+
const registerInputTarget = useCallback((target) => {
|
|
815
|
+
inputTargets.add(target);
|
|
816
|
+
return () => {
|
|
817
|
+
inputTargets.delete(target);
|
|
818
|
+
};
|
|
819
|
+
}, [inputTargets]);
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
const refreshMedia = () => setMediaRevision((revision) => revision + 1);
|
|
822
|
+
const queries = shortcutMediaQueries(definition).map((media) => window.matchMedia(media));
|
|
823
|
+
queries.forEach((query) => query.addEventListener("change", refreshMedia));
|
|
824
|
+
return () => queries.forEach((query) => query.removeEventListener("change", refreshMedia));
|
|
825
|
+
}, [definition]);
|
|
826
|
+
useEffect(() => {
|
|
827
|
+
const refreshFocus = () => setFocusRevision((revision) => revision + 1);
|
|
828
|
+
window.addEventListener("focusin", refreshFocus);
|
|
829
|
+
return () => window.removeEventListener("focusin", refreshFocus);
|
|
830
|
+
}, []);
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
const shortcuts = flattenShortcuts(definition, (media) => window.matchMedia(media).matches);
|
|
833
|
+
let pending = [];
|
|
834
|
+
let pendingEvents = [];
|
|
835
|
+
let pendingTarget;
|
|
836
|
+
let timeout;
|
|
837
|
+
const clearPending = () => {
|
|
838
|
+
pending = [];
|
|
839
|
+
pendingEvents = [];
|
|
840
|
+
pendingTarget = void 0;
|
|
841
|
+
if (timeout !== void 0) {
|
|
842
|
+
window.clearTimeout(timeout);
|
|
843
|
+
timeout = void 0;
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
const replayPending = (events = pendingEvents) => {
|
|
847
|
+
replayChord(pendingTarget, events);
|
|
848
|
+
clearPending();
|
|
849
|
+
};
|
|
850
|
+
const scheduleTimeout = () => {
|
|
851
|
+
if (timeout !== void 0) window.clearTimeout(timeout);
|
|
852
|
+
timeout = window.setTimeout(() => replayPending(), 1e3);
|
|
853
|
+
};
|
|
854
|
+
const onKeyDown = (event) => {
|
|
855
|
+
if (replayedKeyboardEvents.has(event)) return;
|
|
856
|
+
if (pending.length && event.key === "Escape") {
|
|
857
|
+
event.preventDefault();
|
|
858
|
+
event.stopPropagation();
|
|
859
|
+
clearPending();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const matches = shortcuts.flatMap((shortcut) => shortcut.bindings.filter((binding) => sequenceMatches(binding, pending, event) && canRunShortcut(shortcut, event, registrations)).map((binding) => ({
|
|
863
|
+
binding,
|
|
864
|
+
shortcut
|
|
865
|
+
})));
|
|
866
|
+
if (!matches.length) {
|
|
867
|
+
if (pending.length) {
|
|
868
|
+
event.preventDefault();
|
|
869
|
+
event.stopPropagation();
|
|
870
|
+
replayPending([...pendingEvents, event]);
|
|
871
|
+
}
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const complete = matches.find(({ binding }) => normalizedBinding(binding).length === pending.length + 1);
|
|
875
|
+
if (complete) {
|
|
876
|
+
clearPending();
|
|
877
|
+
if (runShortcut(complete.shortcut, event, registrations)) event.stopPropagation();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (!pending.length) pendingTarget = definition.chordPrefixes?.some((prefix) => matchesKeyBinding$1(prefix.binding, event)) ? selectShortcutInputTarget(inputTargets, document.activeElement) : void 0;
|
|
881
|
+
pending = [...pending, normalizedBinding(matches[0].binding)[pending.length]];
|
|
882
|
+
pendingEvents = [...pendingEvents, event];
|
|
883
|
+
event.preventDefault();
|
|
884
|
+
event.stopPropagation();
|
|
885
|
+
scheduleTimeout();
|
|
886
|
+
};
|
|
887
|
+
window.addEventListener("keydown", onKeyDown, { capture: true });
|
|
888
|
+
return () => {
|
|
889
|
+
window.removeEventListener("keydown", onKeyDown, { capture: true });
|
|
890
|
+
if (timeout !== void 0) window.clearTimeout(timeout);
|
|
891
|
+
};
|
|
892
|
+
}, [
|
|
893
|
+
definition,
|
|
894
|
+
inputTargets,
|
|
895
|
+
mediaRevision,
|
|
896
|
+
registrationRevision,
|
|
897
|
+
registrations
|
|
898
|
+
]);
|
|
899
|
+
const commands = createCommandEntries(definition, registrations, () => document.activeElement, (media) => window.matchMedia(media).matches);
|
|
900
|
+
return /* @__PURE__ */ jsx(ShortcutInputTargetsContext.Provider, {
|
|
901
|
+
value: registerInputTarget,
|
|
902
|
+
children: /* @__PURE__ */ jsx(CommandsContext.Provider, {
|
|
903
|
+
value: commands,
|
|
904
|
+
children
|
|
905
|
+
})
|
|
906
|
+
});
|
|
907
|
+
};
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/internal/client/websocket.ts
|
|
910
|
+
const maxWebSocketBufferedBytes = 1048576;
|
|
911
|
+
const socketUrl = () => `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/api/socket`;
|
|
912
|
+
const operationId = () => {
|
|
913
|
+
const randomUUID = crypto.randomUUID?.();
|
|
914
|
+
if (randomUUID) return randomUUID;
|
|
915
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
916
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
917
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
918
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
919
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
920
|
+
};
|
|
921
|
+
const disconnected = () => /* @__PURE__ */ new Error("Overmux transport disconnected.");
|
|
922
|
+
const createWebSocketTransport = ({ createSocket = () => new WebSocket(socketUrl()) } = {}) => {
|
|
923
|
+
let active = false;
|
|
924
|
+
let disposed = false;
|
|
925
|
+
let reconnectDelay = 250;
|
|
926
|
+
let reconnectTimer;
|
|
927
|
+
let socket;
|
|
928
|
+
let status = "disconnected";
|
|
929
|
+
const diagnosticQueue = [];
|
|
930
|
+
const requests = /* @__PURE__ */ new Map();
|
|
931
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
932
|
+
const streams = /* @__PURE__ */ new Map();
|
|
933
|
+
const lifecycleListeners = /* @__PURE__ */ new Set();
|
|
934
|
+
const notificationListeners = /* @__PURE__ */ new Set();
|
|
935
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
936
|
+
const setStatus = (next) => {
|
|
937
|
+
if (status === next) return;
|
|
938
|
+
status = next;
|
|
939
|
+
statusListeners.forEach((listener) => listener(status));
|
|
940
|
+
};
|
|
941
|
+
const send = (message) => {
|
|
942
|
+
if (socket?.readyState !== WebSocket.OPEN) return false;
|
|
943
|
+
if (socket.bufferedAmount > maxWebSocketBufferedBytes) {
|
|
944
|
+
socket.close(1013, "client could not keep up");
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
socket.send(encodeProtocolMessage(clientProtocolMessageSchema.parse(message)));
|
|
948
|
+
return true;
|
|
949
|
+
};
|
|
950
|
+
const flushDiagnostics = () => {
|
|
951
|
+
const unsent = diagnosticQueue.filter((event) => !send(event));
|
|
952
|
+
diagnosticQueue.splice(0, diagnosticQueue.length, ...unsent);
|
|
953
|
+
};
|
|
954
|
+
const request = ({ input, resourceName }) => new Promise((resolve, reject) => {
|
|
955
|
+
const id = operationId();
|
|
956
|
+
requests.set(id, {
|
|
957
|
+
reject,
|
|
958
|
+
resolve
|
|
959
|
+
});
|
|
960
|
+
if (!send({
|
|
961
|
+
input,
|
|
962
|
+
operationId: id,
|
|
963
|
+
resourceName,
|
|
964
|
+
type: "resource-read"
|
|
965
|
+
})) {
|
|
966
|
+
requests.delete(id);
|
|
967
|
+
reject(disconnected());
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
const restore = () => {
|
|
971
|
+
subscriptions.forEach(({ input, onInvalidate, resourceName }, subscriptionId) => {
|
|
972
|
+
if (send({
|
|
973
|
+
input,
|
|
974
|
+
operationId: subscriptionId,
|
|
975
|
+
resourceName,
|
|
976
|
+
subscriptionId,
|
|
977
|
+
type: "resource-subscribe"
|
|
978
|
+
})) onInvalidate();
|
|
979
|
+
});
|
|
980
|
+
streams.forEach((stream, streamId) => {
|
|
981
|
+
if (stream.status === "failed") return;
|
|
982
|
+
stream.status = send({
|
|
983
|
+
input: stream.input,
|
|
984
|
+
operationId: streamId,
|
|
985
|
+
streamId,
|
|
986
|
+
streamName: stream.streamName,
|
|
987
|
+
type: "stream-open"
|
|
988
|
+
}) ? "opening" : "disconnected";
|
|
989
|
+
});
|
|
990
|
+
};
|
|
991
|
+
const rejectRequests = (error) => {
|
|
992
|
+
requests.forEach(({ reject }) => reject(error));
|
|
993
|
+
requests.clear();
|
|
994
|
+
};
|
|
995
|
+
const reportDisconnect = () => {
|
|
996
|
+
const error = disconnected();
|
|
997
|
+
rejectRequests(error);
|
|
998
|
+
subscriptions.forEach(({ onError }) => onError(error));
|
|
999
|
+
streams.forEach((stream) => {
|
|
1000
|
+
stream.pendingMessages.length = 0;
|
|
1001
|
+
if (stream.status !== "failed") stream.status = "disconnected";
|
|
1002
|
+
stream.onError(error);
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
const connect = () => {
|
|
1006
|
+
if (!active || disposed || socket) return;
|
|
1007
|
+
setStatus("connecting");
|
|
1008
|
+
const next = createSocket();
|
|
1009
|
+
next.binaryType = "arraybuffer";
|
|
1010
|
+
socket = next;
|
|
1011
|
+
next.addEventListener("open", () => {
|
|
1012
|
+
if (socket !== next) return;
|
|
1013
|
+
reconnectDelay = 250;
|
|
1014
|
+
setStatus("connected");
|
|
1015
|
+
flushDiagnostics();
|
|
1016
|
+
restore();
|
|
1017
|
+
});
|
|
1018
|
+
next.addEventListener("message", (event) => {
|
|
1019
|
+
if (socket !== next) return;
|
|
1020
|
+
let decoded;
|
|
1021
|
+
try {
|
|
1022
|
+
decoded = decodeProtocolFrame(event.data);
|
|
1023
|
+
} catch (cause) {
|
|
1024
|
+
reportDisconnect();
|
|
1025
|
+
console.error("Invalid Overmux transport message", cause);
|
|
1026
|
+
next.close(1002, "invalid protocol message");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const parsed = serverProtocolMessageSchema.safeParse(decoded);
|
|
1030
|
+
if (!parsed.success) {
|
|
1031
|
+
reportDisconnect();
|
|
1032
|
+
console.error("Invalid Overmux transport message", parsed.error);
|
|
1033
|
+
next.close(1002, "invalid protocol message");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const message = parsed.data;
|
|
1037
|
+
if (message.type === "update-available" || message.type === "restarting") lifecycleListeners.forEach((listener) => listener(message));
|
|
1038
|
+
if (message.type === "notification") notificationListeners.forEach((listener) => listener(message));
|
|
1039
|
+
if (message.type === "resource-invalidated") subscriptions.get(message.subscriptionId)?.onInvalidate();
|
|
1040
|
+
if (message.type === "stream-opened") {
|
|
1041
|
+
const stream = streams.get(message.streamId);
|
|
1042
|
+
if (stream?.status === "opening") {
|
|
1043
|
+
stream.status = "open";
|
|
1044
|
+
if (stream.pendingMessages.splice(0).every(send)) stream.onOpen();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (message.type === "stream-output") streams.get(message.streamId)?.onMessage(message.message);
|
|
1048
|
+
if (message.type === "stream-closed") {
|
|
1049
|
+
const stream = streams.get(message.streamId);
|
|
1050
|
+
if (stream) {
|
|
1051
|
+
stream.pendingMessages.length = 0;
|
|
1052
|
+
stream.onClose();
|
|
1053
|
+
streams.delete(message.streamId);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (message.type === "error") {
|
|
1057
|
+
if (message.operationId) requests.get(message.operationId)?.reject(new Error(message.message));
|
|
1058
|
+
const stream = streams.get(message.operationId ?? "");
|
|
1059
|
+
if (stream) {
|
|
1060
|
+
stream.pendingMessages.length = 0;
|
|
1061
|
+
stream.status = "failed";
|
|
1062
|
+
stream.onError(new Error(message.message));
|
|
1063
|
+
}
|
|
1064
|
+
subscriptions.get(message.operationId ?? "")?.onError(new Error(message.message));
|
|
1065
|
+
requests.delete(message.operationId ?? "");
|
|
1066
|
+
}
|
|
1067
|
+
if (message.type === "resource-result") {
|
|
1068
|
+
requests.get(message.operationId)?.resolve(message.output);
|
|
1069
|
+
requests.delete(message.operationId);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
next.addEventListener("close", (event) => {
|
|
1073
|
+
if (socket !== next || disposed) return;
|
|
1074
|
+
socket = void 0;
|
|
1075
|
+
reportDisconnect();
|
|
1076
|
+
if (event.code === webSocketCloseCode.authenticationRevoked) {
|
|
1077
|
+
active = false;
|
|
1078
|
+
setStatus("authentication-required");
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
setStatus("disconnected");
|
|
1082
|
+
reconnectTimer = setTimeout(() => {
|
|
1083
|
+
reconnectTimer = void 0;
|
|
1084
|
+
connect();
|
|
1085
|
+
}, reconnectDelay);
|
|
1086
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 5e3);
|
|
1087
|
+
});
|
|
1088
|
+
};
|
|
1089
|
+
return {
|
|
1090
|
+
activate: () => {
|
|
1091
|
+
active = true;
|
|
1092
|
+
connect();
|
|
1093
|
+
},
|
|
1094
|
+
dispose: () => {
|
|
1095
|
+
disposed = true;
|
|
1096
|
+
if (reconnectTimer !== void 0) clearTimeout(reconnectTimer);
|
|
1097
|
+
reportDisconnect();
|
|
1098
|
+
streams.forEach(({ onClose }) => onClose());
|
|
1099
|
+
streams.clear();
|
|
1100
|
+
subscriptions.clear();
|
|
1101
|
+
socket?.close();
|
|
1102
|
+
},
|
|
1103
|
+
openStream: ({ input, onClose, onError, onMessage, onOpen, streamName }) => {
|
|
1104
|
+
const streamId = operationId();
|
|
1105
|
+
const stream = {
|
|
1106
|
+
input,
|
|
1107
|
+
onClose,
|
|
1108
|
+
onError,
|
|
1109
|
+
onMessage,
|
|
1110
|
+
onOpen,
|
|
1111
|
+
pendingMessages: [],
|
|
1112
|
+
status: "disconnected",
|
|
1113
|
+
streamName
|
|
1114
|
+
};
|
|
1115
|
+
streams.set(streamId, stream);
|
|
1116
|
+
stream.status = send({
|
|
1117
|
+
input,
|
|
1118
|
+
operationId: streamId,
|
|
1119
|
+
streamId,
|
|
1120
|
+
streamName,
|
|
1121
|
+
type: "stream-open"
|
|
1122
|
+
}) ? "opening" : "disconnected";
|
|
1123
|
+
if (stream.status === "disconnected") onError(disconnected());
|
|
1124
|
+
return {
|
|
1125
|
+
close: () => {
|
|
1126
|
+
const activeStream = streams.get(streamId);
|
|
1127
|
+
if (activeStream) {
|
|
1128
|
+
activeStream.pendingMessages.length = 0;
|
|
1129
|
+
activeStream.onClose();
|
|
1130
|
+
streams.delete(streamId);
|
|
1131
|
+
}
|
|
1132
|
+
send({
|
|
1133
|
+
streamId,
|
|
1134
|
+
type: "stream-close"
|
|
1135
|
+
});
|
|
1136
|
+
},
|
|
1137
|
+
send: (message) => {
|
|
1138
|
+
const activeStream = streams.get(streamId);
|
|
1139
|
+
if (activeStream?.status !== "open" && activeStream?.status !== "opening") return false;
|
|
1140
|
+
const protocolMessage = streamMessageSchema.parse({
|
|
1141
|
+
message,
|
|
1142
|
+
streamId,
|
|
1143
|
+
type: "stream-message"
|
|
1144
|
+
});
|
|
1145
|
+
if (activeStream.status === "opening") {
|
|
1146
|
+
activeStream.pendingMessages.push(protocolMessage);
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
return send(protocolMessage);
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
},
|
|
1153
|
+
readResource: (input) => request(input),
|
|
1154
|
+
reportDiagnostic: (event) => {
|
|
1155
|
+
if (send(event)) return;
|
|
1156
|
+
diagnosticQueue.push(event);
|
|
1157
|
+
if (diagnosticQueue.length > 100) diagnosticQueue.shift();
|
|
1158
|
+
},
|
|
1159
|
+
subscribeResource: ({ input, onError, onInvalidate, resourceName }) => {
|
|
1160
|
+
const subscriptionId = operationId();
|
|
1161
|
+
subscriptions.set(subscriptionId, {
|
|
1162
|
+
input,
|
|
1163
|
+
onError,
|
|
1164
|
+
onInvalidate,
|
|
1165
|
+
resourceName
|
|
1166
|
+
});
|
|
1167
|
+
if (!send({
|
|
1168
|
+
input,
|
|
1169
|
+
operationId: subscriptionId,
|
|
1170
|
+
resourceName,
|
|
1171
|
+
subscriptionId,
|
|
1172
|
+
type: "resource-subscribe"
|
|
1173
|
+
})) onError(disconnected());
|
|
1174
|
+
return () => {
|
|
1175
|
+
subscriptions.delete(subscriptionId);
|
|
1176
|
+
send({
|
|
1177
|
+
subscriptionId,
|
|
1178
|
+
type: "resource-unsubscribe"
|
|
1179
|
+
});
|
|
1180
|
+
};
|
|
1181
|
+
},
|
|
1182
|
+
subscribeStatus: (listener) => {
|
|
1183
|
+
statusListeners.add(listener);
|
|
1184
|
+
listener(status);
|
|
1185
|
+
return () => statusListeners.delete(listener);
|
|
1186
|
+
},
|
|
1187
|
+
subscribeLifecycle: (listener) => {
|
|
1188
|
+
lifecycleListeners.add(listener);
|
|
1189
|
+
return () => lifecycleListeners.delete(listener);
|
|
1190
|
+
},
|
|
1191
|
+
subscribeNotification: (listener) => {
|
|
1192
|
+
notificationListeners.add(listener);
|
|
1193
|
+
return () => notificationListeners.delete(listener);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
};
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/internal/client/transport.ts
|
|
1199
|
+
const TransportContext = createContext(void 0);
|
|
1200
|
+
const operationError = async (response) => {
|
|
1201
|
+
const body = await response.text();
|
|
1202
|
+
try {
|
|
1203
|
+
const error = JSON.parse(body).error;
|
|
1204
|
+
if (typeof error === "string") return new Error(error);
|
|
1205
|
+
} catch {}
|
|
1206
|
+
return /* @__PURE__ */ new Error(`Operation request failed: ${response.status} ${response.statusText}`);
|
|
1207
|
+
};
|
|
1208
|
+
const createClientTransport = ({ createSocket, fetch: fetchRequest = globalThis.fetch } = {}) => ({
|
|
1209
|
+
...createWebSocketTransport({ createSocket }),
|
|
1210
|
+
invokeOperation: async ({ input, name, signal }) => {
|
|
1211
|
+
const response = await fetchRequest(`/api/operations/${encodeURIComponent(name)}`, {
|
|
1212
|
+
body: input === void 0 ? void 0 : JSON.stringify(input),
|
|
1213
|
+
headers: { "content-type": "application/json" },
|
|
1214
|
+
method: "POST",
|
|
1215
|
+
signal
|
|
1216
|
+
});
|
|
1217
|
+
if (!response.ok) throw await operationError(response);
|
|
1218
|
+
return response.status === 204 ? void 0 : response.json();
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
//#endregion
|
|
1222
|
+
//#region src/internal/client/host/hosted-pages/hosted-page.tsx
|
|
1223
|
+
const HostedPage = ({ children }) => /* @__PURE__ */ jsx("main", {
|
|
1224
|
+
"data-om-hosted-page": "",
|
|
1225
|
+
children: /* @__PURE__ */ jsx("article", {
|
|
1226
|
+
"data-om-hosted-panel": "",
|
|
1227
|
+
children
|
|
1228
|
+
})
|
|
1229
|
+
});
|
|
1230
|
+
//#endregion
|
|
1231
|
+
//#region src/internal/client/host/hosted-pages/logout-page.tsx
|
|
1232
|
+
const logoutRequests = /* @__PURE__ */ new WeakMap();
|
|
1233
|
+
const minimumLogoutDuration = 3e3;
|
|
1234
|
+
const requestLogout = () => {
|
|
1235
|
+
const existing = logoutRequests.get(document);
|
|
1236
|
+
if (existing) return existing;
|
|
1237
|
+
const request = fetch("/api/auth/logout", { method: "POST" }).then((response) => {
|
|
1238
|
+
if (!response.ok) throw new Error(`Logout failed: ${response.status}`);
|
|
1239
|
+
});
|
|
1240
|
+
logoutRequests.set(document, request);
|
|
1241
|
+
return request;
|
|
1242
|
+
};
|
|
1243
|
+
const minimumDelay = () => new Promise((resolve) => setTimeout(resolve, minimumLogoutDuration));
|
|
1244
|
+
const HostedLogoutPage = () => {
|
|
1245
|
+
const [attempt, setAttempt] = useState(0);
|
|
1246
|
+
const [error, setError] = useState(false);
|
|
1247
|
+
useEffect(() => {
|
|
1248
|
+
let active = true;
|
|
1249
|
+
setError(false);
|
|
1250
|
+
Promise.allSettled([requestLogout(), minimumDelay()]).then(([logout]) => {
|
|
1251
|
+
if (!active) return;
|
|
1252
|
+
if (logout?.status === "fulfilled") {
|
|
1253
|
+
location.replace("/login");
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
setError(true);
|
|
1257
|
+
});
|
|
1258
|
+
return () => {
|
|
1259
|
+
active = false;
|
|
1260
|
+
};
|
|
1261
|
+
}, [attempt]);
|
|
1262
|
+
return /* @__PURE__ */ jsxs(HostedPage, { children: [/* @__PURE__ */ jsx("h1", { children: error ? "Log out failed" : "Logging out…" }), error ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", {
|
|
1263
|
+
role: "alert",
|
|
1264
|
+
children: "Overmux could not log you out. Check your connection and try again."
|
|
1265
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1266
|
+
"data-om-hosted-retry": "",
|
|
1267
|
+
onClick: () => {
|
|
1268
|
+
logoutRequests.delete(document);
|
|
1269
|
+
setAttempt((current) => current + 1);
|
|
1270
|
+
},
|
|
1271
|
+
type: "button",
|
|
1272
|
+
children: "Retry"
|
|
1273
|
+
})] }) : /* @__PURE__ */ jsx("p", { children: "Please wait while Overmux ends your session." })] });
|
|
1274
|
+
};
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/internal/client/host/hosted-pages/browser-notification-settings.tsx
|
|
1277
|
+
const statusMessage = (status) => {
|
|
1278
|
+
return {
|
|
1279
|
+
checking: "Checking background notification status…",
|
|
1280
|
+
denied: "Background notifications are blocked in this browser's site settings.",
|
|
1281
|
+
disabled: "Background notifications are disabled for this browser.",
|
|
1282
|
+
enabled: "Background notifications are enabled for this browser.",
|
|
1283
|
+
error: "Background notification settings could not be updated. Try again.",
|
|
1284
|
+
"ios-home-screen": "On iPhone or iPad, add Overmux to the Home Screen before enabling background notifications.",
|
|
1285
|
+
unsupported: "Background notifications are not supported in this browser."
|
|
1286
|
+
}[status];
|
|
1287
|
+
};
|
|
1288
|
+
const BrowserNotificationSettings = () => {
|
|
1289
|
+
const [status, setStatus] = useState("checking");
|
|
1290
|
+
useEffect(() => {
|
|
1291
|
+
let active = true;
|
|
1292
|
+
const reconcile = () => {
|
|
1293
|
+
reconciledStatus().then((next) => {
|
|
1294
|
+
if (active) setStatus(next);
|
|
1295
|
+
}).catch(() => {
|
|
1296
|
+
if (active) setStatus("error");
|
|
1297
|
+
});
|
|
1298
|
+
};
|
|
1299
|
+
const onVisibilityChange = () => {
|
|
1300
|
+
if (document.visibilityState === "visible") reconcile();
|
|
1301
|
+
};
|
|
1302
|
+
reconcile();
|
|
1303
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1304
|
+
navigator.serviceWorker?.addEventListener("controllerchange", reconcile);
|
|
1305
|
+
return () => {
|
|
1306
|
+
active = false;
|
|
1307
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1308
|
+
navigator.serviceWorker?.removeEventListener("controllerchange", reconcile);
|
|
1309
|
+
};
|
|
1310
|
+
}, []);
|
|
1311
|
+
const enable = useCallback(async () => {
|
|
1312
|
+
try {
|
|
1313
|
+
setStatus("checking");
|
|
1314
|
+
const enabled = await enableBackgroundNotifications();
|
|
1315
|
+
setStatus(enabled ? "enabled" : Notification.permission === "denied" ? "denied" : "disabled");
|
|
1316
|
+
} catch {
|
|
1317
|
+
setStatus("error");
|
|
1318
|
+
}
|
|
1319
|
+
}, []);
|
|
1320
|
+
const disable = useCallback(async () => {
|
|
1321
|
+
try {
|
|
1322
|
+
setStatus("checking");
|
|
1323
|
+
await disableBackgroundNotifications();
|
|
1324
|
+
setStatus("disabled");
|
|
1325
|
+
} catch {
|
|
1326
|
+
setStatus("error");
|
|
1327
|
+
}
|
|
1328
|
+
}, []);
|
|
1329
|
+
const canEnable = status === "disabled" || status === "error";
|
|
1330
|
+
const canDisable = status === "enabled" || status === "error";
|
|
1331
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1332
|
+
"data-om-browser-notifications": "",
|
|
1333
|
+
children: [
|
|
1334
|
+
/* @__PURE__ */ jsx("h3", { children: "Browser notifications" }),
|
|
1335
|
+
/* @__PURE__ */ jsx("p", {
|
|
1336
|
+
"aria-live": "polite",
|
|
1337
|
+
"data-om-background-notifications-status": "",
|
|
1338
|
+
children: statusMessage(status)
|
|
1339
|
+
}),
|
|
1340
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1341
|
+
"data-om-hosted-actions": "",
|
|
1342
|
+
children: [canEnable ? /* @__PURE__ */ jsx("button", {
|
|
1343
|
+
"data-om-background-notifications-enable": "",
|
|
1344
|
+
onClick: enable,
|
|
1345
|
+
type: "button",
|
|
1346
|
+
children: "Enable"
|
|
1347
|
+
}) : null, canDisable ? /* @__PURE__ */ jsx("button", {
|
|
1348
|
+
"data-om-background-notifications-disable": "",
|
|
1349
|
+
onClick: disable,
|
|
1350
|
+
type: "button",
|
|
1351
|
+
children: "Disable"
|
|
1352
|
+
}) : null]
|
|
1353
|
+
})
|
|
1354
|
+
]
|
|
1355
|
+
});
|
|
1356
|
+
};
|
|
1357
|
+
//#endregion
|
|
1358
|
+
//#region src/internal/client/host/hosted-pages/settings-page.tsx
|
|
1359
|
+
const DesktopNotificationSettings = () => /* @__PURE__ */ jsxs("div", {
|
|
1360
|
+
"data-om-desktop-notifications": "",
|
|
1361
|
+
children: [/* @__PURE__ */ jsx("h3", { children: "Desktop notifications" }), /* @__PURE__ */ jsx("p", { children: "Notifications are delivered through the Overmux desktop app and managed by the OS. Change notification permissions and preferences in your operating system's settings." })]
|
|
1362
|
+
});
|
|
1363
|
+
const HostedSettingsPage = () => {
|
|
1364
|
+
const returnTo = resolveOvermuxReturnTo(new URLSearchParams(location.search).get("returnTo"));
|
|
1365
|
+
const desktopNotifications = Boolean(window.overmuxHost?.notifications);
|
|
1366
|
+
return /* @__PURE__ */ jsxs(HostedPage, { children: [
|
|
1367
|
+
/* @__PURE__ */ jsxs("header", {
|
|
1368
|
+
"data-om-hosted-header": "",
|
|
1369
|
+
children: [/* @__PURE__ */ jsx("h1", { children: "Overmux settings" }), /* @__PURE__ */ jsx("a", {
|
|
1370
|
+
href: returnTo,
|
|
1371
|
+
children: "Back to Overmux"
|
|
1372
|
+
})]
|
|
1373
|
+
}),
|
|
1374
|
+
/* @__PURE__ */ jsxs("section", {
|
|
1375
|
+
"data-om-hosted-section": "",
|
|
1376
|
+
children: [/* @__PURE__ */ jsx("h2", { children: "Notifications" }), desktopNotifications ? /* @__PURE__ */ jsx(DesktopNotificationSettings, {}) : /* @__PURE__ */ jsx(BrowserNotificationSettings, {})]
|
|
1377
|
+
}),
|
|
1378
|
+
/* @__PURE__ */ jsxs("section", {
|
|
1379
|
+
"data-om-hosted-section": "",
|
|
1380
|
+
children: [
|
|
1381
|
+
/* @__PURE__ */ jsx("h2", { children: "Session" }),
|
|
1382
|
+
/* @__PURE__ */ jsx("p", { children: "End this Overmux session on this device." }),
|
|
1383
|
+
/* @__PURE__ */ jsx("a", {
|
|
1384
|
+
"data-om-hosted-button": "",
|
|
1385
|
+
href: overmuxLogoutPath,
|
|
1386
|
+
children: "Log out"
|
|
1387
|
+
})
|
|
1388
|
+
]
|
|
1389
|
+
})
|
|
1390
|
+
] });
|
|
1391
|
+
};
|
|
1392
|
+
//#endregion
|
|
1393
|
+
//#region src/internal/client/host/recovery-screen.tsx
|
|
1394
|
+
const RecoveryScreen = ({ error }) => /* @__PURE__ */ jsx("main", {
|
|
1395
|
+
"data-om-recovery": "",
|
|
1396
|
+
children: /* @__PURE__ */ jsxs("section", {
|
|
1397
|
+
"data-om-recovery-panel": "",
|
|
1398
|
+
children: [
|
|
1399
|
+
/* @__PURE__ */ jsx("h1", {
|
|
1400
|
+
"data-om-recovery-title": "",
|
|
1401
|
+
children: "Overmux recovery"
|
|
1402
|
+
}),
|
|
1403
|
+
error ? /* @__PURE__ */ jsx("pre", {
|
|
1404
|
+
"data-om-recovery-error": "",
|
|
1405
|
+
children: error
|
|
1406
|
+
}) : null,
|
|
1407
|
+
/* @__PURE__ */ jsx("button", {
|
|
1408
|
+
"data-om-recovery-reload": "",
|
|
1409
|
+
onClick: () => location.reload(),
|
|
1410
|
+
type: "button",
|
|
1411
|
+
children: "Reload"
|
|
1412
|
+
})
|
|
1413
|
+
]
|
|
1414
|
+
})
|
|
1415
|
+
});
|
|
1416
|
+
//#endregion
|
|
1417
|
+
//#region ../lib/dist/index.js
|
|
1418
|
+
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/internal/client/host/update-popover.tsx
|
|
1421
|
+
const reduceUpdateState = (_state, action) => {
|
|
1422
|
+
if (action.type === "update-available") return { type: "available" };
|
|
1423
|
+
if (action.type === "restart-requested") return { type: "requesting" };
|
|
1424
|
+
if (action.type === "restart-failed") return {
|
|
1425
|
+
type: "available",
|
|
1426
|
+
error: action.error
|
|
1427
|
+
};
|
|
1428
|
+
return { type: "restarting" };
|
|
1429
|
+
};
|
|
1430
|
+
const pollUntilHealthy = async (signal) => {
|
|
1431
|
+
if (signal.aborted) return;
|
|
1432
|
+
try {
|
|
1433
|
+
if ((await fetch("/api/health", {
|
|
1434
|
+
cache: "no-store",
|
|
1435
|
+
signal
|
|
1436
|
+
})).ok) {
|
|
1437
|
+
location.reload();
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
} catch {}
|
|
1441
|
+
await delay(250);
|
|
1442
|
+
return pollUntilHealthy(signal);
|
|
1443
|
+
};
|
|
1444
|
+
const ReconnectingScreen = () => /* @__PURE__ */ jsx("main", {
|
|
1445
|
+
"aria-live": "polite",
|
|
1446
|
+
"data-om-reconnecting": "",
|
|
1447
|
+
role: "status",
|
|
1448
|
+
children: /* @__PURE__ */ jsxs("section", {
|
|
1449
|
+
"data-om-reconnecting-panel": "",
|
|
1450
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
1451
|
+
"data-om-reconnecting-title": "",
|
|
1452
|
+
children: "Reconnecting"
|
|
1453
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1454
|
+
"data-om-reconnecting-message": "",
|
|
1455
|
+
children: "Reconnecting to Overmux. Your workspace will resume shortly."
|
|
1456
|
+
})]
|
|
1457
|
+
})
|
|
1458
|
+
});
|
|
1459
|
+
const useUpdateState = (transport) => {
|
|
1460
|
+
const [state, dispatch] = useReducer(reduceUpdateState, { type: "current" });
|
|
1461
|
+
useEffect(() => transport.subscribeLifecycle((event) => dispatch(event)), [transport]);
|
|
1462
|
+
useEffect(() => {
|
|
1463
|
+
if (state.type !== "restarting") return;
|
|
1464
|
+
const controller = new AbortController();
|
|
1465
|
+
pollUntilHealthy(controller.signal);
|
|
1466
|
+
return () => controller.abort();
|
|
1467
|
+
}, [state.type]);
|
|
1468
|
+
return {
|
|
1469
|
+
requestRestart: useCallback(async () => {
|
|
1470
|
+
dispatch({ type: "restart-requested" });
|
|
1471
|
+
try {
|
|
1472
|
+
const response = await fetch("/api/restart", { method: "POST" });
|
|
1473
|
+
if (!response.ok) throw new Error(`Restart request failed: ${response.status}`);
|
|
1474
|
+
dispatch({ type: "restarting" });
|
|
1475
|
+
} catch (cause) {
|
|
1476
|
+
const error = cause instanceof Error ? cause.message : String(cause);
|
|
1477
|
+
dispatch({
|
|
1478
|
+
type: "restart-failed",
|
|
1479
|
+
error
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}, []),
|
|
1483
|
+
state
|
|
1484
|
+
};
|
|
1485
|
+
};
|
|
1486
|
+
const useConnectionState = (transport) => {
|
|
1487
|
+
const connected = useRef(false);
|
|
1488
|
+
const [connection, setConnection] = useState("initial");
|
|
1489
|
+
useEffect(() => transport.subscribeStatus((status) => {
|
|
1490
|
+
if (status === "connected") {
|
|
1491
|
+
connected.current = true;
|
|
1492
|
+
setConnection("connected");
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
if (!connected.current) return;
|
|
1496
|
+
connected.current = false;
|
|
1497
|
+
setConnection("reconnecting");
|
|
1498
|
+
}), [transport]);
|
|
1499
|
+
return connection;
|
|
1500
|
+
};
|
|
1501
|
+
const UpdatePopover = ({ transport }) => {
|
|
1502
|
+
const connection = useConnectionState(transport);
|
|
1503
|
+
const { requestRestart, state } = useUpdateState(transport);
|
|
1504
|
+
if (state.type === "restarting") return /* @__PURE__ */ jsx("main", {
|
|
1505
|
+
"aria-live": "polite",
|
|
1506
|
+
"data-om-restarting": "",
|
|
1507
|
+
role: "status",
|
|
1508
|
+
children: /* @__PURE__ */ jsxs("section", {
|
|
1509
|
+
"data-om-restarting-panel": "",
|
|
1510
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
1511
|
+
"data-om-restarting-title": "",
|
|
1512
|
+
children: "Restarting Overmux"
|
|
1513
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1514
|
+
"data-om-restarting-message": "",
|
|
1515
|
+
children: "Waiting for the new version."
|
|
1516
|
+
})]
|
|
1517
|
+
})
|
|
1518
|
+
});
|
|
1519
|
+
if (connection === "reconnecting") return /* @__PURE__ */ jsx(ReconnectingScreen, {});
|
|
1520
|
+
if (state.type === "current") return null;
|
|
1521
|
+
const error = state.type === "available" ? state.error : void 0;
|
|
1522
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
1523
|
+
"data-om-update": "",
|
|
1524
|
+
role: "status",
|
|
1525
|
+
children: [
|
|
1526
|
+
/* @__PURE__ */ jsx("p", { children: "A new version is available. Reload to update." }),
|
|
1527
|
+
/* @__PURE__ */ jsx("button", {
|
|
1528
|
+
"data-om-update-reload": "",
|
|
1529
|
+
disabled: state.type === "requesting",
|
|
1530
|
+
onClick: () => void requestRestart(),
|
|
1531
|
+
type: "button",
|
|
1532
|
+
children: "Reload"
|
|
1533
|
+
}),
|
|
1534
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
1535
|
+
"data-om-update-error": "",
|
|
1536
|
+
children: error
|
|
1537
|
+
}) : null
|
|
1538
|
+
]
|
|
1539
|
+
});
|
|
1540
|
+
};
|
|
1541
|
+
//#endregion
|
|
1542
|
+
//#region src/internal/client/host/overmux-host.tsx
|
|
1543
|
+
var RootErrorBoundary = class extends Component {
|
|
1544
|
+
state = {};
|
|
1545
|
+
static getDerivedStateFromError(error) {
|
|
1546
|
+
return { error };
|
|
1547
|
+
}
|
|
1548
|
+
render() {
|
|
1549
|
+
return this.state.error ? this.props.fallback(this.state.error) : this.props.children;
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
var AuthenticationRequiredError = class extends Error {};
|
|
1553
|
+
const redirectToLogin = () => location.replace("/login?reason=session-expired");
|
|
1554
|
+
const loadRuntimeManifest = async () => {
|
|
1555
|
+
const response = await fetch("/api/runtime-manifest", { cache: "no-store" });
|
|
1556
|
+
if (response.status === 401) throw new AuthenticationRequiredError("Authentication required");
|
|
1557
|
+
if (!response.ok) throw new Error(`Runtime manifest request failed: ${response.status}`);
|
|
1558
|
+
return runtimeManifestSchema.parse(await response.json());
|
|
1559
|
+
};
|
|
1560
|
+
const OvermuxRuntime = ({ definition }) => {
|
|
1561
|
+
const safeMode = new URLSearchParams(location.search).get("safe") === "1";
|
|
1562
|
+
const transport = useMemo(createClientTransport, []);
|
|
1563
|
+
const queryClient = useMemo(() => new QueryClient(), []);
|
|
1564
|
+
const logForwardingDisposer = useRef(void 0);
|
|
1565
|
+
const [manifest, setManifest] = useState();
|
|
1566
|
+
const [error, setError] = useState();
|
|
1567
|
+
useEffect(() => {
|
|
1568
|
+
let active = true;
|
|
1569
|
+
loadRuntimeManifest().then((next) => {
|
|
1570
|
+
if (!active) return;
|
|
1571
|
+
setManifest(next);
|
|
1572
|
+
transport.activate();
|
|
1573
|
+
}).catch((cause) => {
|
|
1574
|
+
if (cause instanceof AuthenticationRequiredError) {
|
|
1575
|
+
redirectToLogin();
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (active) setError(cause instanceof Error ? cause.message : String(cause));
|
|
1579
|
+
});
|
|
1580
|
+
return () => {
|
|
1581
|
+
active = false;
|
|
1582
|
+
};
|
|
1583
|
+
}, [transport]);
|
|
1584
|
+
const registrations = useMemo(() => /* @__PURE__ */ new Set(), []);
|
|
1585
|
+
const [registrationRevision, setRegistrationRevision] = useState(0);
|
|
1586
|
+
useEffect(() => {
|
|
1587
|
+
if (manifest?.debug && !logForwardingDisposer.current) logForwardingDisposer.current = installBrowserLogForwarding(transport.reportDiagnostic);
|
|
1588
|
+
}, [manifest?.debug, transport]);
|
|
1589
|
+
useEffect(() => transport.subscribeStatus((status) => {
|
|
1590
|
+
if (status === "authentication-required") redirectToLogin();
|
|
1591
|
+
}), [transport]);
|
|
1592
|
+
useEffect(() => () => {
|
|
1593
|
+
logForwardingDisposer.current?.();
|
|
1594
|
+
transport.dispose();
|
|
1595
|
+
}, [transport]);
|
|
1596
|
+
useEffect(() => installNotificationForwarding(transport), [transport]);
|
|
1597
|
+
const refreshCommands = useCallback(() => setRegistrationRevision((revision) => revision + 1), []);
|
|
1598
|
+
const registerCommand = useCallback((registration) => {
|
|
1599
|
+
registrations.add(registration);
|
|
1600
|
+
refreshCommands();
|
|
1601
|
+
return () => {
|
|
1602
|
+
registrations.delete(registration);
|
|
1603
|
+
refreshCommands();
|
|
1604
|
+
};
|
|
1605
|
+
}, [refreshCommands, registrations]);
|
|
1606
|
+
const overmuxServerApi = useMemo(() => manifest ? createOvermuxServerApi({
|
|
1607
|
+
manifest,
|
|
1608
|
+
transport
|
|
1609
|
+
}) : void 0, [manifest, transport]);
|
|
1610
|
+
if (safeMode || !manifest || !overmuxServerApi) return /* @__PURE__ */ jsx(RecoveryScreen, { error });
|
|
1611
|
+
const App = definition.component;
|
|
1612
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
1613
|
+
client: queryClient,
|
|
1614
|
+
children: /* @__PURE__ */ jsx(RuntimeContext.Provider, {
|
|
1615
|
+
value: {
|
|
1616
|
+
manifest,
|
|
1617
|
+
overmuxServerApi,
|
|
1618
|
+
refreshCommands,
|
|
1619
|
+
registerCommand
|
|
1620
|
+
},
|
|
1621
|
+
children: /* @__PURE__ */ jsx(TransportContext.Provider, {
|
|
1622
|
+
value: transport,
|
|
1623
|
+
children: /* @__PURE__ */ jsxs(ShortcutHost, {
|
|
1624
|
+
definition,
|
|
1625
|
+
registrations,
|
|
1626
|
+
registrationRevision,
|
|
1627
|
+
children: [/* @__PURE__ */ jsx(RootErrorBoundary, {
|
|
1628
|
+
fallback: (cause) => /* @__PURE__ */ jsx(RecoveryScreen, { error: cause.message }),
|
|
1629
|
+
children: createElement(App)
|
|
1630
|
+
}), /* @__PURE__ */ jsx(UpdatePopover, { transport })]
|
|
1631
|
+
})
|
|
1632
|
+
})
|
|
1633
|
+
})
|
|
1634
|
+
});
|
|
1635
|
+
};
|
|
1636
|
+
const OvermuxHost = ({ definition }) => /* @__PURE__ */ jsx(OvermuxThemeScope, {
|
|
1637
|
+
...definition.appearance,
|
|
1638
|
+
children: location.pathname === "/_overmux/settings" ? /* @__PURE__ */ jsx(HostedSettingsPage, {}) : location.pathname === "/_overmux/logout" ? /* @__PURE__ */ jsx(HostedLogoutPage, {}) : /* @__PURE__ */ jsx(OvermuxRuntime, { definition })
|
|
1639
|
+
});
|
|
1640
|
+
//#endregion
|
|
1641
|
+
export { OvermuxHost, OvermuxPortal, OvermuxThemeScope, chordPrefixSchema, commandBindingSchema, createOvermuxHooks, defineCommandRegistry, defineOvermuxClient, disableBackgroundNotifications, enableBackgroundNotifications, formatKeyBinding, formatShortcutBinding, keyBindingSchema, matchesKeyBinding, shortcutBindingSchema, skipToken, useCommand, useCommands, useShortcutInputTarget };
|
|
1642
|
+
|
|
1643
|
+
//# sourceMappingURL=client.js.map
|