pipane 0.1.9 → 0.1.11

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 (31) hide show
  1. package/README.md +1 -1
  2. package/dist/build-info.json +2 -2
  3. package/dist/client/assets/__vite-browser-external-DGfF_P2w.js +1 -0
  4. package/dist/client/assets/backend-landing-page-CF2aHs0U.js +1 -0
  5. package/dist/client/assets/{__vite-browser-external-DtLbhLGx.js → backend-protocol-BvNAsFvb.js} +1 -1
  6. package/dist/client/assets/device-identity-C_w2NbSk.js +4 -0
  7. package/dist/client/assets/index-Cc4cqaXn.js +2 -0
  8. package/dist/client/assets/index-DtrSEX3O.css +1 -0
  9. package/dist/client/assets/{main-DEfSB8wO.js → main-CtM4oO40.js} +531 -427
  10. package/dist/client/assets/pairing-page-CiEb8c3R.js +1 -0
  11. package/dist/client/assets/remote-backend-manager-BY79yja7.js +1 -0
  12. package/dist/client/assets/rendezvous-trust-api-xLnrhe-v.js +1 -0
  13. package/dist/client/assets/webrtc-frame-transport-Drpqhhdy.js +1 -0
  14. package/dist/client/assets/workspace-backend-client-C2DwlCed.js +1 -0
  15. package/dist/client/assets/ws-agent-adapter-P9Rtv9sl.js +6 -0
  16. package/dist/client/index.html +5 -17
  17. package/dist/server/server/frame-connection.js +157 -40
  18. package/dist/server/server/frame-router.js +2 -1
  19. package/dist/server/server/server.js +13 -10
  20. package/dist/server/server/ws-handler.js +44 -23
  21. package/dist/server/shared/data-channel-framing.js +31 -1
  22. package/docs/protocol.md +4 -2
  23. package/package.json +1 -1
  24. package/dist/client/assets/backend-landing-page-B7SbyIXQ.js +0 -1
  25. package/dist/client/assets/index-C7_1ODks.js +0 -2
  26. package/dist/client/assets/index-DgI_gkjg.css +0 -1
  27. package/dist/client/assets/pairing-page-C0YByC2D.js +0 -1
  28. package/dist/client/assets/remote-backend-manager-DCSdS38m.js +0 -1
  29. package/dist/client/assets/rendezvous-trust-api-C2Zob5i4.js +0 -4
  30. package/dist/client/assets/webrtc-frame-transport-DGX5EO_A.js +0 -1
  31. package/dist/client/assets/ws-agent-adapter-DpkkZGPl.js +0 -6
@@ -8,7 +8,7 @@
8
8
  * - Any number of clients can connect; each subscribes to one session at a time.
9
9
  * - Detached sessions are read from JSONL on demand.
10
10
  */
11
- import { FRAME_CONNECTION_OPEN } from "./frame-connection.js";
11
+ import { FRAME_CONNECTION_OPEN, WebSocketFrameConnection, } from "./frame-connection.js";
12
12
  import { copyFile } from "node:fs/promises";
13
13
  import { constants as fsConstants, existsSync } from "node:fs";
14
14
  import path from "node:path";
@@ -16,10 +16,13 @@ import { SessionJsonl, readSessionFromDisk, getSessionFileSize, serializeSession
16
16
  import { getSessionCwd } from "./session-cwd.js";
17
17
  import { checkCommandAvailable, installPiGlobal, isPiInstallable, makePiNotFoundMessage } from "./pi-runtime.js";
18
18
  import { modelsMatch, toCompactModelRef } from "../shared/thinking-levels.js";
19
+ import { computeSyncOp } from "../shared/jsonl-sync.js";
19
20
  import { COMPACT_RPC_TIMEOUT_MS } from "../shared/rpc-timeouts.js";
20
21
  import { assertNever, decodeClientCommand, encodeServerMessage, } from "../shared/ws-protocol.js";
21
22
  import { SessionPathError, SessionPathGuard } from "./session-path.js";
22
23
  import { extensionStatusSnapshot, isValidExtensionStatusKey, normalizeExtensionStatusText, providerForUsageStatus, PROVIDER_USAGE_STATUS_KEY, } from "./extension-status.js";
24
+ const DETACHED_SYNC_COALESCE_MS = 75;
25
+ const SESSION_SYNC_TRANSFER_KEY = "active-session-sync";
23
26
  let nextTurnId = 0;
24
27
  function makeTurnId() {
25
28
  return `turn_${Date.now()}_${++nextTurnId}`;
@@ -49,6 +52,8 @@ export class WsHandler {
49
52
  subscribedFileSizes = new Map();
50
53
  /** Last revision/hash published for each authoritative session state. */
51
54
  sessionRevisions = new Map();
55
+ /** Per-session trailing-edge disk refreshes for detached JSONL bursts. */
56
+ pendingDetachedSyncs = new Map();
52
57
  piAvailable;
53
58
  piInstalling = false;
54
59
  constructor(options) {
@@ -186,29 +191,36 @@ export class WsHandler {
186
191
  }
187
192
  /**
188
193
  * Called by the file watcher when a JSONL file changes on disk.
189
- * For detached sessions with subscribers, re-reads from disk and pushes a snapshot.
190
- * Ignores attached sessions (their state comes from streaming events).
194
+ * Detached writes are coalesced per session so a burst is read, diffed, and
195
+ * published once at its trailing edge. Attached actor events remain authoritative.
191
196
  */
192
197
  notifySessionFileChanged(sessionPath) {
193
- // If the session is attached, ignore — streaming events are authoritative
194
- if (this.registry.find(sessionPath)?.isAttached)
198
+ if (this.registry.find(sessionPath)?.isAttached || !this.hasSubscribers(sessionPath))
195
199
  return;
196
- // Check if any client is subscribed to this session
197
- let hasSubscribers = false;
198
- for (const [, client] of this.clients) {
199
- if (client.subscribedSession === sessionPath) {
200
- hasSubscribers = true;
201
- break;
202
- }
200
+ const pending = this.pendingDetachedSyncs.get(sessionPath);
201
+ if (pending)
202
+ clearTimeout(pending);
203
+ const timer = setTimeout(() => {
204
+ this.pendingDetachedSyncs.delete(sessionPath);
205
+ this.flushDetachedSessionSync(sessionPath);
206
+ }, DETACHED_SYNC_COALESCE_MS);
207
+ timer.unref?.();
208
+ this.pendingDetachedSyncs.set(sessionPath, timer);
209
+ }
210
+ hasSubscribers(sessionPath) {
211
+ for (const client of this.clients.values()) {
212
+ if (client.subscribedSession === sessionPath)
213
+ return true;
203
214
  }
204
- if (!hasSubscribers)
215
+ return false;
216
+ }
217
+ flushDetachedSessionSync(sessionPath) {
218
+ if (this.registry.find(sessionPath)?.isAttached || !this.hasSubscribers(sessionPath))
205
219
  return;
206
- // Check if the file actually changed
207
220
  const oldSize = this.subscribedFileSizes.get(sessionPath) ?? 0;
208
221
  const newSize = getSessionFileSize(sessionPath);
209
222
  if (newSize === oldSize)
210
223
  return;
211
- // File changed — read from disk and push snapshot to subscribers
212
224
  this.subscribedFileSizes.set(sessionPath, newSize);
213
225
  const { json, hash } = readSessionFromDisk(sessionPath);
214
226
  this.pushSnapshotToSubscribers(sessionPath, json, hash);
@@ -243,7 +255,7 @@ export class WsHandler {
243
255
  ws.close(1008, "Unauthorized");
244
256
  return;
245
257
  }
246
- this.acceptConnection(ws);
258
+ this.acceptConnection(new WebSocketFrameConnection(ws));
247
259
  });
248
260
  }
249
261
  acceptAuthenticatedConnection(connection) {
@@ -279,8 +291,11 @@ export class WsHandler {
279
291
  });
280
292
  }
281
293
  sendMessage(ws, payload) {
282
- if (ws.readyState === FRAME_CONNECTION_OPEN)
283
- ws.send(encodeServerMessage(payload));
294
+ if (ws.readyState !== FRAME_CONNECTION_OPEN)
295
+ return;
296
+ ws.send(encodeServerMessage(payload), payload.type === "session_sync"
297
+ ? { priority: "bulk", transferKey: SESSION_SYNC_TRANSFER_KEY }
298
+ : { priority: "control" });
284
299
  }
285
300
  sendSuccess(ws, id, command, data) {
286
301
  this.sendMessage(ws, {
@@ -417,6 +432,7 @@ export class WsHandler {
417
432
  return;
418
433
  const requestedPath = command.sessionPath;
419
434
  if (!requestedPath) {
435
+ ws.cancelTransfer?.(SESSION_SYNC_TRANSFER_KEY);
420
436
  client.subscribedSession = null;
421
437
  client.lastVersion = 0;
422
438
  client.lastJson = "";
@@ -438,7 +454,13 @@ export class WsHandler {
438
454
  throw error;
439
455
  sessionPath = pendingPath;
440
456
  }
457
+ // A newly selected session makes every queued chunk from the previous
458
+ // snapshot stale. The carrier emits a cancellation for a partial frame.
459
+ ws.cancelTransfer?.(SESSION_SYNC_TRANSFER_KEY);
441
460
  client.subscribedSession = sessionPath;
461
+ client.lastVersion = 0;
462
+ client.lastJson = "";
463
+ client.lastHash = "";
442
464
  // If the session is attached, send from its actor-owned in-memory state
443
465
  const attached = this.registry.find(sessionPath)?.session;
444
466
  if (attached) {
@@ -1083,17 +1105,16 @@ export class WsHandler {
1083
1105
  pushSnapshotToSubscribers(sessionPath, json, hash) {
1084
1106
  const revision = this.revisionForState(sessionPath, hash);
1085
1107
  for (const [ws, client] of this.clients) {
1086
- if (client.subscribedSession !== sessionPath)
1108
+ if (client.subscribedSession !== sessionPath || ws.readyState !== FRAME_CONNECTION_OPEN)
1087
1109
  continue;
1088
- if (ws.readyState !== FRAME_CONNECTION_OPEN)
1110
+ if (client.lastHash === hash)
1089
1111
  continue;
1112
+ const sync = computeSyncOp(client.lastJson, json, client.lastHash, hash);
1090
1113
  this.sendMessage(ws, {
1091
1114
  type: "session_sync",
1092
1115
  sessionPath,
1093
1116
  revision,
1094
- op: "full",
1095
- data: json,
1096
- hash,
1117
+ ...sync,
1097
1118
  });
1098
1119
  client.lastJson = json;
1099
1120
  client.lastHash = hash;
@@ -1,5 +1,7 @@
1
1
  const CHUNK_MARKER = 1;
2
+ const CANCEL_MARKER = 1;
2
3
  const CHUNK_PREFIX = `{"__pipaneDataChannelChunk":${CHUNK_MARKER},`;
4
+ const CANCEL_PREFIX = `{"__pipaneDataChannelCancel":${CANCEL_MARKER},`;
3
5
  /** Keep physical messages below conservative browser/libdatachannel SCTP limits. */
4
6
  export const DATA_CHANNEL_CHUNK_PAYLOAD_BYTES = 12_000;
5
7
  export const MAX_DATA_CHANNEL_MESSAGE_BYTES = 16 * 1024;
@@ -41,13 +43,23 @@ export function encodeDataChannelFrame(frame, id) {
41
43
  }
42
44
  return messages;
43
45
  }
46
+ /** Cancel one incomplete logical frame without exposing a carrier envelope upstream. */
47
+ export function encodeDataChannelFrameCancellation(id) {
48
+ if (!CHUNK_ID_PATTERN.test(id))
49
+ throw new Error("DataChannel frame id is invalid");
50
+ return JSON.stringify({ __pipaneDataChannelCancel: CANCEL_MARKER, id });
51
+ }
44
52
  /** Reassemble carrier chunks while passing ordinary application frames through unchanged. */
45
53
  export class DataChannelFrameDecoder {
46
54
  pending = new Map();
47
55
  accept(message) {
48
- if (!message.startsWith(CHUNK_PREFIX))
56
+ if (!message.startsWith(CHUNK_PREFIX) && !message.startsWith(CANCEL_PREFIX))
49
57
  return message;
50
58
  try {
59
+ if (message.startsWith(CANCEL_PREFIX)) {
60
+ this.pending.delete(parseCancel(message).id);
61
+ return undefined;
62
+ }
51
63
  const chunk = parseChunk(message);
52
64
  let pending = this.pending.get(chunk.id);
53
65
  if (!pending) {
@@ -90,6 +102,24 @@ export class DataChannelFrameDecoder {
90
102
  this.pending.clear();
91
103
  }
92
104
  }
105
+ function parseCancel(message) {
106
+ let value;
107
+ try {
108
+ value = JSON.parse(message);
109
+ }
110
+ catch {
111
+ throw new Error("DataChannel cancellation is not valid JSON");
112
+ }
113
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
114
+ throw new Error("DataChannel cancellation must be an object");
115
+ }
116
+ const cancellation = value;
117
+ if (cancellation.__pipaneDataChannelCancel !== CANCEL_MARKER
118
+ || typeof cancellation.id !== "string" || !CHUNK_ID_PATTERN.test(cancellation.id)) {
119
+ throw new Error("DataChannel cancellation envelope is invalid");
120
+ }
121
+ return cancellation;
122
+ }
93
123
  function parseChunk(message) {
94
124
  let value;
95
125
  try {
package/docs/protocol.md CHANGED
@@ -89,7 +89,9 @@ The answer-side implementation accepts only a reliable ordered DataChannel with
89
89
 
90
90
  Authenticated DataChannels carry the existing versioned v1 application frames through the same server connection boundary as local WebSockets. A frame router keeps those application frames isolated from semantic v2 responses on the same ordered channel. Revocation closes active rendezvous routes and matching backend peers, prevents new ticket issuance, and is retained centrally so an offline backend clears stale local ownership when it next registers.
91
91
 
92
- After the unfragmented authentication exchange, the carrier transparently splits logical frames larger than 12,000 UTF-8 bytes into ordered base64 chunk envelopes no larger than 16 KiB. Browser and backend reassemble at most 64 MiB per logical frame with bounded pending-frame and outgoing-queue memory. Application v1 and semantic v2 decoders therefore continue to receive exactly one complete JSON frame regardless of the negotiated SCTP message-size limit.
92
+ After the unfragmented authentication exchange, the carrier transparently splits logical frames larger than 12,000 UTF-8 bytes into ordered base64 chunk envelopes no larger than 16 KiB. The same transparent envelopes are used for large server-to-browser local WebSocket frames. Browser and backend reassemble at most 64 MiB per logical frame with bounded pending-frame and outgoing-queue memory. Application v1 and semantic v2 decoders therefore continue to receive exactly one complete JSON frame regardless of the negotiated SCTP message-size limit.
93
+
94
+ Server carriers schedule control and bulk lanes independently, so responses, status, steering, and abort traffic can overtake queued session-snapshot chunks. Selecting another session cancels the unsent portion of the stale snapshot with a reserved carrier envelope before the replacement begins. Detached JSONL watcher bursts are coalesced per session and hash-chained against each client's last state, producing a delta when it is smaller than a full snapshot and suppressing unchanged updates.
93
95
 
94
96
  ### Semantic backend protocol v2
95
97
 
@@ -112,7 +114,7 @@ The currently implemented semantic methods are:
112
114
 
113
115
  Every method has runtime-validated parameters, correlated responses, stable error codes, bounded concurrency, and a bounded device-scoped completed-request cache. File uploads use bounded, offset-addressed base64 chunks so arbitrary non-image attachments can cross either local HTTP or the authenticated DataChannel, land in a private temporary backend path, and be referenced in the agent prompt. Pending browser requests retain their id across a carrier reconnect, so an in-flight mutation is resumed or answered from the cache instead of being executed twice. The backend uses one `LocalBackendApi` implementation for both the legacy local HTTP facade and semantic DataChannel requests. Remote session results are scoped to a structured `{ backendId, path }` identity; paths remain backend-local identifiers rather than authorization.
114
116
 
115
- The browser's `RemoteBackendManager` maintains one client/store per active backend id, requests a fresh ticket whenever a WebRTC carrier reconnects, and never treats an arbitrary URL backend id as authorized until signed account discovery includes it. The product UI exposes reachable authorized backends and uses on-demand pairwise connections rather than a full mesh. Terminal `pipane pair` remains the no-email recovery path when browser storage is lost.
117
+ The browser's `RemoteBackendManager` maintains one client/store per authorized backend id, requests a fresh ticket whenever a WebRTC carrier reconnects, and never treats an arbitrary URL backend id as authorized until signed account discovery includes it. The account workspace opens a direct connection to each reachable authorized host so it can merge backend-scoped session metadata and live status in one sidebar; only the selected host/session drives the conversation renderer and receives a session subscription. Session metadata remains end-to-end between browser and backend and is not indexed by rendezvous. Terminal `pipane pair` remains the no-email recovery path when browser storage is lost.
116
118
 
117
119
  Application streaming, turn control, and session snapshots remain on validated v1 frames during parity migration. Semantic v2 is deployed beside v1 rather than changing v1's renderer-state contract in place.
118
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipane",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "A clean web interface for the pi coding agent — chat UI with real-time tool calls, streaming output, session management, and model picker",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- import{B as u}from"./__vite-browser-external-DtLbhLGx.js";import{R as x,a as b}from"./rendezvous-trust-api-C2Zob5i4.js";const y={loadIdentity:()=>b(),createTrustApi:()=>new x(window.location.origin),confirm:e=>window.confirm(e)};async function v(e=y){const l=document.getElementById("app")??document.body;l.replaceChildren();const r=document.createElement("main");r.dataset.testid="backend-landing",r.style.cssText="min-height:100vh;padding:40px 24px;font-family:ui-sans-serif,system-ui;color:var(--foreground,#111827);background:var(--background,#fff)";const i=document.createElement("section");i.style.cssText="width:min(760px,100%);margin:0 auto";const t=document.createElement("h1");t.textContent="Your pipane backends",t.style.cssText="margin:0 0 8px;font-size:28px";const s=document.createElement("p");s.textContent="Connections are opened directly and only when you choose a backend.",s.style.cssText="margin:0 0 24px;opacity:.72";const n=document.createElement("div");n.dataset.testid="backend-list",n.style.cssText="display:grid;gap:12px";const d=document.createElement("p");d.style.cssText="margin-top:24px;padding-top:18px;border-top:1px solid var(--border,#d1d5db);opacity:.78;line-height:1.5",d.textContent="Add a backend or recover this browser by running `pipane pair` in an owned backend terminal and scanning its QR code.",i.append(t,s,n,d),r.append(i),l.append(r);const c=await e.loadIdentity();if(!c){n.append(m("No paired browser key was found.","Use `pipane pair` on a backend to begin without creating an account."));return}const o=e.createTrustApi();let a;try{a=await o.listAuthorizedBackends(c)}catch(p){n.append(m("Backend access could not be recovered from this device key.",p instanceof Error?p.message:"Pair this browser again from an owned backend terminal."));return}if(a.length===0){n.append(m("No backends are paired.","Run `pipane pair` in a backend terminal to add one."));return}for(const p of a)n.append(f(p,c,o,e))}function f(e,l,r,i){const t=document.createElement("article");t.dataset.backendId=e.backendId,t.style.cssText="display:flex;align-items:center;gap:14px;border:1px solid var(--border,#d1d5db);border-radius:10px;padding:16px";const s=document.createElement("div");s.style.cssText="min-width:0;flex:1";const n=document.createElement("strong");n.textContent=e.name||`${e.backendId.slice(0,18)}…`,n.style.cssText="display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis";const d=document.createElement("small"),c=e.protocolVersions.includes(u);d.textContent=e.online?c?`Online${e.softwareVersion?` · v${e.softwareVersion}`:""}`:"Online · update required":"Offline",d.style.cssText=`color:${e.online&&c?"#15803d":"#a16207"}`,s.append(n,d);const o=document.createElement("a");o.textContent="Open",o.href=`/backend/${encodeURIComponent(e.backendId)}`,o.dataset.testid="open-backend",o.style.cssText="border-radius:8px;padding:8px 12px;background:#2563eb;color:white;text-decoration:none",(!e.online||!c)&&(o.removeAttribute("href"),o.setAttribute("aria-disabled","true"),o.style.opacity=".45");const a=document.createElement("button");return a.type="button",a.textContent="Remove",a.style.cssText="border:0;background:transparent;color:#dc2626;cursor:pointer",a.addEventListener("click",()=>{i.confirm(`Remove ${e.name||e.backendId} from this account?`)&&(a.disabled=!0,r.revokeBackend(l,e.backendId).then(()=>t.remove()).catch(p=>{a.disabled=!1,d.textContent=p instanceof Error?p.message:"Removal failed",d.style.color="#dc2626"}))}),t.append(s,o,a),t}function m(e,l){const r=document.createElement("div");r.style.cssText="border:1px solid var(--border,#d1d5db);border-radius:10px;padding:18px;line-height:1.5";const i=document.createElement("strong");i.textContent=e;const t=document.createElement("p");return t.textContent=l,t.style.cssText="margin:6px 0 0;opacity:.72",r.append(i,t),r}export{v as initializeBackendLandingPage};
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/pairing-page-C0YByC2D.js","assets/rendezvous-trust-api-C2Zob5i4.js","assets/webrtc-frame-transport-DGX5EO_A.js","assets/backend-landing-page-B7SbyIXQ.js","assets/__vite-browser-external-DtLbhLGx.js","assets/remote-backend-manager-DCSdS38m.js","assets/ws-agent-adapter-DpkkZGPl.js","assets/main-DEfSB8wO.js","assets/app-runtime-Rw-O-AwW.js"])))=>i.map(i=>d[i]);
2
- (function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))a(t);new MutationObserver(t=>{for(const r of t)if(r.type==="childList")for(const i of r.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&a(i)}).observe(document,{childList:!0,subtree:!0});function o(t){const r={};return t.integrity&&(r.integrity=t.integrity),t.referrerPolicy&&(r.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?r.credentials="include":t.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function a(t){if(t.ep)return;t.ep=!0;const r=o(t);fetch(t.href,r)}})();const w="modulepreload",y=function(n){return"/"+n},p={},f=function(e,o,a){let t=Promise.resolve();if(o&&o.length>0){let m=function(c){return Promise.all(c.map(l=>Promise.resolve(l).then(u=>({status:"fulfilled",value:u}),u=>({status:"rejected",reason:u}))))};document.getElementsByTagName("link");const i=document.querySelector("meta[property=csp-nonce]"),s=i?.nonce||i?.getAttribute("nonce");t=m(o.map(c=>{if(c=y(c),c in p)return;p[c]=!0;const l=c.endsWith(".css"),u=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${u}`))return;const d=document.createElement("link");if(d.rel=l?"stylesheet":w,l||(d.as="script"),d.crossOrigin="",d.href=c,s&&d.setAttribute("nonce",s),document.head.appendChild(d),l)return new Promise((h,g)=>{d.addEventListener("load",h),d.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function r(i){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=i,window.dispatchEvent(s),!s.defaultPrevented)throw i}return t.then(i=>{for(const s of i||[])s.status==="rejected"&&r(s.reason);return e().catch(r)})};"serviceWorker"in navigator&&navigator.serviceWorker.getRegistrations().then(n=>{for(const e of n)e.unregister()});v();async function v(){if(P(window.location.pathname)){const{initializePairingPage:e}=await f(async()=>{const{initializePairingPage:o}=await import("./pairing-page-C0YByC2D.js");return{initializePairingPage:o}},__vite__mapDeps([0,1,2]));await e();return}if(window.location.pathname==="/"&&await E()){const{initializeBackendLandingPage:e}=await f(async()=>{const{initializeBackendLandingPage:o}=await import("./backend-landing-page-B7SbyIXQ.js");return{initializeBackendLandingPage:o}},__vite__mapDeps([3,4,1]));await e();return}const n=b(window.location.pathname);if(n)try{const[{configureAppRuntime:e},{RemoteBackendManager:o}]=await Promise.all([f(()=>import("./app-runtime-Rw-O-AwW.js"),[]),f(()=>import("./remote-backend-manager-DCSdS38m.js"),__vite__mapDeps([5,4,1,2,6]))]),a=new o(window.location.origin),t=await a.initialize();e({client:a.getClient(n),remote:{backendId:n,backends:t,manager:a}})}catch(e){_(e);return}await f(()=>import("./main-DEfSB8wO.js"),__vite__mapDeps([7,6,4,8]))}async function E(){try{const n=await fetch("/health",{cache:"no-store"});if(!n.ok||!n.headers.get("content-type")?.includes("application/json"))return!1;const e=await n.json();return!!e&&typeof e=="object"&&e.ok===!0}catch{return!1}}function _(n){const e=document.getElementById("app")??document.body;e.replaceChildren();const o=document.createElement("main");o.style.cssText="min-height:100vh;display:grid;place-items:center;padding:24px;font-family:ui-sans-serif,system-ui;color:var(--foreground,#111827);background:var(--background,#fff)";const a=document.createElement("section");a.style.cssText="max-width:520px;border:1px solid var(--border,#d1d5db);border-radius:12px;padding:28px";const t=document.createElement("h1");t.textContent="Cannot open backend";const r=document.createElement("p");r.textContent=n instanceof Error?n.message:"This browser is not authorized for the backend.";const i=document.createElement("p");i.textContent="To restore access, run `pipane pair` in the backend terminal and scan the new QR code with this browser.",a.append(t,r,i),o.append(a),e.append(o)}function P(n){return/^\/pair\/[^/]+$/u.test(n)}function b(n){const e=/^\/backend\/([^/]+)$/u.exec(n);return e?.[1]?decodeURIComponent(e[1]):void 0}export{f as _};