@openchambery/web 2.1.9-beta.1 → 2.1.9-beta.3

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 (87) hide show
  1. package/dist/assets/{AgentAvatar-CJQwq8UM.js → AgentAvatar-qLxaEaDh.js} +1 -1
  2. package/dist/assets/{AssistantView-j9aI8OWI.js → AssistantView-3Lz_SADb.js} +3 -3
  3. package/dist/assets/{DiagramView-EsIuVPrU.js → DiagramView-BZP5Aj4S.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-BNVExwoL.js → FadeInOnReveal-CX8Bkq-l.js} +1 -1
  5. package/dist/assets/{Icon-B4xKSuz3.js → Icon-BCFeHDWx.js} +1 -1
  6. package/dist/assets/{ImageLoadingPlaceholder-xB8WeKks.js → ImageLoadingPlaceholder-DEpMJXzV.js} +1 -1
  7. package/dist/assets/{LoadingImage-CoT3GWhg.js → LoadingImage-BAT9D9wN.js} +1 -1
  8. package/dist/assets/{MarkdownRenderer-TmfHeWHu.js → MarkdownRenderer-DpB0hJ_E.js} +2 -2
  9. package/dist/assets/{MarkdownRendererImpl-vkv1QLTd.js → MarkdownRendererImpl-Hg1bbRlM.js} +1 -1
  10. package/dist/assets/{MarkstreamRendererImpl-BbnksqWU.js → MarkstreamRendererImpl-BfUZU3WC.js} +1 -1
  11. package/dist/assets/{MobileDetailNavigation-CtGncmBY.js → MobileDetailNavigation-DqGKTFzT.js} +1 -1
  12. package/dist/assets/{MobileShareBridge-pYQFFYmm.js → MobileShareBridge-DHHw00TT.js} +2 -2
  13. package/dist/assets/{MobileTabPageHeader-D7NAfatx.js → MobileTabPageHeader-jFeAz_Z2.js} +1 -1
  14. package/dist/assets/{MultiRunWindow-gZk-f5lA.js → MultiRunWindow-C92A1Qkc.js} +1 -1
  15. package/dist/assets/{OnboardingScreen-BiLurnX5.js → OnboardingScreen-CqcZEptL.js} +1 -1
  16. package/dist/assets/{OverlayScrollbar-DIsrS93m.js → OverlayScrollbar-BY5HaNrb.js} +1 -1
  17. package/dist/assets/{SettingsView-D7Ykm-CM.js → SettingsView-C2-Tug3M.js} +1 -1
  18. package/dist/assets/{SettingsWindow-CyPZw4xx.js → SettingsWindow-D_j-lLiY.js} +1 -1
  19. package/dist/assets/{TerminalView-D-X0mGO4.js → TerminalView-Veaa8xms.js} +1 -1
  20. package/dist/assets/{ToolOutputDialog-C4vQBg3z.js → ToolOutputDialog-CI32t1nx.js} +1 -1
  21. package/dist/assets/{appThemeRegistry-DUH4Fprj.js → appThemeRegistry-BhVqv80m.js} +1 -1
  22. package/dist/assets/{assistant-attachment-cache-Bzewqov4.js → assistant-attachment-cache-B2UAUHUZ.js} +1 -1
  23. package/dist/assets/{button-BupmKVUs.js → button-CWSs9Baz.js} +1 -1
  24. package/dist/assets/{collapsible-BAPWS6G-.js → collapsible-ngtoKADo.js} +1 -1
  25. package/dist/assets/{desktopHosts-BZ7iYE-l.js → desktopHosts-C0e7Nes2.js} +1 -1
  26. package/dist/assets/{desktopRelayRestore-S9wEiY2U.js → desktopRelayRestore-DckkHKsi.js} +1 -1
  27. package/dist/assets/{diagram-0a7vH45N.js → diagram-DmlTCkmV.js} +1 -1
  28. package/dist/assets/{dialog-BYLD4Cm3.js → dialog-BMa16qrs.js} +1 -1
  29. package/dist/assets/{dropdown-menu.styles-HOWSubFF.js → dropdown-menu.styles-BNwZmBRp.js} +1 -1
  30. package/dist/assets/{fileMentionTouchSelection-BcqQPEDT.js → fileMentionTouchSelection-ytHOa8vD.js} +1 -1
  31. package/dist/assets/{fileQueries-DZR-mK8h.js → fileQueries-DVpZC5ha.js} +1 -1
  32. package/dist/assets/{gitApi-BtJfaFQy.js → gitApi-DZSpp5_u.js} +1 -1
  33. package/dist/assets/{image-loading-CsxDGgek.js → image-loading-Bj11k4gM.js} +1 -1
  34. package/dist/assets/{input-DgrLdIys.js → input-CALwaZEB.js} +1 -1
  35. package/dist/assets/{insertionBoundaries-z2K9a6oY.js → insertionBoundaries-B3F2vgJn.js} +1 -1
  36. package/dist/assets/{main-DAHeBbJJ.js → main-HsqXFi70.js} +2 -2
  37. package/dist/assets/{main-BDCqUxWf.js → main-mdIF-V3Y.js} +4 -4
  38. package/dist/assets/{markdownHeightCache-Dyctmwqv.js → markdownHeightCache-DXM_5jTe.js} +1 -1
  39. package/dist/assets/{mcpQueries-BSwb38Pz.js → mcpQueries-BAU9FYFF.js} +1 -1
  40. package/dist/assets/miniChat-B7C-IBFC.js +2 -0
  41. package/dist/assets/mobile-CqWR9PBo.js +2 -0
  42. package/dist/assets/{multirun-DldZm-Aq.js → multirun-D3iS9HjC.js} +1 -1
  43. package/dist/assets/{persistence-B4Ni3Gmv.js → persistence-BKszKaoQ.js} +1 -1
  44. package/dist/assets/{platform-BQPsr8Zl.js → platform-CgwxwfA4.js} +1 -1
  45. package/dist/assets/{projectMeta-zESSNFVw.js → projectMeta-DlHKE_8C.js} +1 -1
  46. package/dist/assets/{relay-image-stream-qI2NTXbG.js → relay-image-stream-SV0lH6aG.js} +1 -1
  47. package/dist/assets/{renderElectronMiniChatApp-t1IsnPfc.js → renderElectronMiniChatApp-CVT0WyUz.js} +2 -2
  48. package/dist/assets/{renderMobileApp-BrC28EkU.js → renderMobileApp-hUJNFMKe.js} +4 -4
  49. package/dist/assets/runtime-auth-BpCQ-dPr.js +1 -0
  50. package/dist/assets/{runtime-fetch-QCWYlAx0.js → runtime-fetch-WgH0pROs.js} +1 -1
  51. package/dist/assets/{runtime-switch-3ad3yzM7.js → runtime-switch-CMiMO5Yj.js} +1 -1
  52. package/dist/assets/{runtimeConfig-5NAbSrPI.js → runtimeConfig-DAxmpV5p.js} +1 -1
  53. package/dist/assets/{runtimeEndpointReset-CxreCHBU.js → runtimeEndpointReset-BCf2ORY3.js} +2 -2
  54. package/dist/assets/{scheduledTaskNavigation-C1PrFEhV.js → scheduledTaskNavigation-DQcLqQ52.js} +1 -1
  55. package/dist/assets/{sessionChangeSummary-CUMpXSiU.js → sessionChangeSummary-BYoAdclB.js} +1 -1
  56. package/dist/assets/{sessionLookup-DD8YnthC.js → sessionLookup-DxWtYNX-.js} +1 -1
  57. package/dist/assets/{sortable-tabs-strip-DogWaIrG.js → sortable-tabs-strip-CscHgCaq.js} +1 -1
  58. package/dist/assets/{speechSettingsQueries-PsDvs5QG.js → speechSettingsQueries-CBrLvvue.js} +2 -2
  59. package/dist/assets/{terminalApi-kRMttlcP.js → terminalApi-rxsMWL0o.js} +1 -1
  60. package/dist/assets/{textarea-Boj9axpc.js → textarea-ClV5sMS-.js} +1 -1
  61. package/dist/assets/{tooltip-Dbgnq-Mh.js → tooltip-CV6dvLBp.js} +1 -1
  62. package/dist/assets/{url-Bd61gNgl.js → url-nBOUbtV_.js} +1 -1
  63. package/dist/assets/{useAppFontEffects-Ba6H7bLU.js → useAppFontEffects-BvJWL9Tq.js} +5 -5
  64. package/dist/assets/{useAssistantUIStore-Dlvb7P0q.js → useAssistantUIStore-OyPm_4M_.js} +1 -1
  65. package/dist/assets/{useDesktopWindowControlsLayout-CbeUwtDM.js → useDesktopWindowControlsLayout-6B2M3hOM.js} +1 -1
  66. package/dist/assets/{useEffectiveDirectory-wtPL_zdo.js → useEffectiveDirectory-fANUMttq.js} +1 -1
  67. package/dist/assets/{useFontPreferences-lAw0Cbza.js → useFontPreferences-DqEIKMEW.js} +1 -1
  68. package/dist/assets/{useMobileNavigationStore-DXpdkzw_.js → useMobileNavigationStore-DRpR2Oyd.js} +11 -11
  69. package/dist/assets/{useUIStore-CDMI_Yzu.js → useUIStore-CEqU9Ujv.js} +1 -1
  70. package/dist/assets/{useWorkerHighlightedLines-BXDSKlPX.js → useWorkerHighlightedLines-D1io7Q4t.js} +1 -1
  71. package/dist/index.html +11 -11
  72. package/dist/mini-chat.html +11 -11
  73. package/dist/mobile.html +11 -11
  74. package/package.json +1 -1
  75. package/server/lib/git/DOCUMENTATION.md +1 -1
  76. package/server/lib/git/branches.test.js +39 -0
  77. package/server/lib/git/service.js +3 -43
  78. package/server/lib/relay/DOCUMENTATION.md +20 -0
  79. package/server/lib/relay/relay-server.e2e.test.ts +35 -0
  80. package/server/lib/relay/tunnel-host.js +14 -4
  81. package/server/lib/relay/tunnel-host.test.js +37 -1
  82. package/server/lib/session-index/DOCUMENTATION.md +17 -0
  83. package/server/lib/session-index/sync-runtime.js +34 -14
  84. package/server/lib/session-index/sync-runtime.test.js +121 -0
  85. package/dist/assets/miniChat-DEIx3C4R.js +0 -2
  86. package/dist/assets/mobile-DmNNI21w.js +0 -2
  87. package/dist/assets/runtime-auth-C8w0nlKy.js +0 -1
@@ -75,6 +75,26 @@ See also `packages/web/server/lib/opencode/DOCUMENTATION.md` (“Catalog / OpenC
75
75
  4. **Handshake.** Over that connection pair, client and host run the E2EE handshake and derive a shared encrypted channel the relay cannot read.
76
76
  5. **Traffic.** All normal app traffic is multiplexed and encrypted through that channel. On the host, decrypted requests are dispatched to the local server over loopback; responses stream back encrypted. Reconnects re-establish a fresh channel and the app's existing retry machinery recovers.
77
77
 
78
+ ## Negotiated JSON response compression
79
+
80
+ Each HTTP request may advertise `acceptEncoding: 'gzip'` in its tunnel JSON head
81
+ when the client supports `DecompressionStream`. The Host may then set
82
+ `encoding: 'gzip'` on the response head and stream gzip bytes as ordinary HttpBody
83
+ frames **before encryption**. The client decompresses before exposing its Fetch
84
+ Response; HTTP headers describe the decoded body, with no compressed content-length
85
+ or content-encoding leaked to callers. Wire framing, batching and crypto counters
86
+ are unchanged. Old Hosts ignore the opt-in; old clients (including native Android
87
+ notifications) omit it and receive uncompressed bodies.
88
+
89
+ Only application/json and application/*+json bodies qualify. Known decoded bodies
90
+ under 1 KiB, HEAD/no-body responses, SSE and binary assets are not compressed.
91
+ Unknown-length JSON is streamed rather than buffered in full; loopback fetch's
92
+ automatic decompression is accounted for before checking length. Cancellation and
93
+ errors propagate through the stream pipeline. Host tests cover negotiation and
94
+ legacy/SSE/binary exclusions; client tests cover malformed gzip and cancellation;
95
+ the authenticated compiled-Relay integration test checks decoded snapshot fidelity
96
+ and encrypted wire-byte reduction on 4,000 session summaries.
97
+
78
98
  ## Candidate refresh (staying off the relay when direct works)
79
99
 
80
100
  Pairing-payload transport candidates are a snapshot: when DHCP hands the host
@@ -36,6 +36,9 @@ import { requestSpeechSession } from '../../../../ui/src/lib/voice/direct-sessio
36
36
 
37
37
  const timeoutMs = 10_000;
38
38
  const previewPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aD1sAAAAASUVORK5CYII=', 'base64');
39
+ const initializationSnapshot = { sessions: Array.from({ length: 4000 }, (_, id) => ({
40
+ id: `session-${id}`, title: `Session ${id}`, directory: '/repo/project', time: { created: 1000, updated: 2000 },
41
+ })) };
39
42
  const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
40
43
 
41
44
  const eventually = async (predicate: () => boolean | Promise<boolean>, message: string, timeout = timeoutMs) => {
@@ -215,6 +218,10 @@ const startOrigin = async () => {
215
218
  });
216
219
  return;
217
220
  }
221
+ if (pathname === '/api/e2e/initialization' && req.method === 'GET') {
222
+ await uiAuth.requireAuth(req, res, () => res.json(initializationSnapshot));
223
+ return;
224
+ }
218
225
  if (pathname === '/api/agent' && req.method === 'GET') {
219
226
  await uiAuth.requireAuth(req, res, () => {
220
227
  const directory = new URL(req.url ?? '/', 'http://origin').searchParams.get('location[directory]');
@@ -401,10 +408,32 @@ describe('private relay compiled-process boundary', () => {
401
408
  origin = await startOrigin();
402
409
  const identity = await buildIdentity();
403
410
  host = startRelayHost({ relayUrl: relay.url, identity, getLocalPort: () => origin.port, onStatus: () => {}, logger: { warn: () => {}, info: () => {} } });
411
+ let receivedBytes = 0;
404
412
  client = createRelayTunnelClient({
405
413
  relayUrl: relay.url, serverId: identity.serverId, hostEncPubJwk: identity.hostEncPubJwk,
406
414
  helloRetryMs: 50, helloTimeoutMs: 2_000, pingIntervalMs: 500, pingTimeoutMs: 250,
407
415
  reconnectBaseDelayMs: 50, reconnectMaxDelayMs: 250, batchWindowMs: 5,
416
+ createWireSocket: (url) => {
417
+ const socket = new globalThis.WebSocket(url);
418
+ socket.binaryType = 'arraybuffer';
419
+ const wire = {
420
+ get readyState() { return socket.readyState; },
421
+ send: (data: string | ArrayBuffer | Uint8Array) => socket.send(data as string | ArrayBuffer),
422
+ close: (code?: number, reason?: string) => socket.close(code, reason),
423
+ onopen: null as (() => void) | null,
424
+ onmessage: null as ((event: { data: unknown }) => void) | null,
425
+ onclose: null as ((event: { code: number; reason: string }) => void) | null,
426
+ onerror: null as (() => void) | null,
427
+ };
428
+ socket.onopen = () => wire.onopen?.();
429
+ socket.onmessage = (event) => {
430
+ receivedBytes += typeof event.data === 'string' ? Buffer.byteLength(event.data) : event.data.byteLength;
431
+ wire.onmessage?.({ data: event.data });
432
+ };
433
+ socket.onclose = (event) => wire.onclose?.({ code: event.code, reason: event.reason });
434
+ socket.onerror = () => wire.onerror?.();
435
+ return wire;
436
+ },
408
437
  });
409
438
 
410
439
  await eventually(() => host!.getStatus().state === 'connected' && client!.getStatus().state === 'connected', 'host and TypeScript client did not connect');
@@ -415,6 +444,12 @@ describe('private relay compiled-process boundary', () => {
415
444
  expect(httpResponse.status).toBe(200);
416
445
  expect(await httpResponse.json()).toEqual({ body: 'stream-body', hasRelayConnection: true });
417
446
 
447
+ const beforeSnapshot = receivedBytes;
448
+ const compressed = await client.fetch('/api/e2e/initialization', { headers: { authorization: 'Bearer client-token' } });
449
+ expect(compressed.status).toBe(200);
450
+ expect(await compressed.json()).toEqual(initializationSnapshot);
451
+ expect(receivedBytes - beforeSnapshot).toBeLessThan(Buffer.byteLength(JSON.stringify(initializationSnapshot)) / 4);
452
+
418
453
  const sseResponse = await client.fetch('/api/e2e/sse', { headers: { authorization: 'Bearer client-token' } });
419
454
  const reader = sseResponse.body!.getReader();
420
455
  const chunks: Uint8Array[] = [];
@@ -258,11 +258,21 @@ export const createTunnelHost = ({
258
258
  if (STRIPPED_RESPONSE_HEADERS.has(name)) continue;
259
259
  responseHeaders[name] = value;
260
260
  }
261
- await sendJson(TunnelFrameType.HttpResponse, streamId, { status: response.status, headers: responseHeaders });
262
-
263
261
  try {
264
- if (response.body) {
265
- for await (const chunk of response.body) {
262
+ const contentType = (response.headers.get('content-type') ?? '').split(';', 1)[0].trim().toLowerCase();
263
+ const contentLength = response.headers.has('content-encoding') ? null : response.headers.get('content-length');
264
+ // Compress finite JSON responses before encryption, never SSE or binary assets.
265
+ // Old clients omit acceptEncoding and old hosts ignore it, preserving v1 framing.
266
+ const gzip = request.acceptEncoding === 'gzip' && typeof CompressionStream === 'function'
267
+ && method !== 'HEAD' && response.body !== null
268
+ && (contentType === 'application/json' || /^application\/[^/]+\+json$/.test(contentType))
269
+ && (contentLength === null || Number(contentLength) >= 1024);
270
+ const body = gzip ? response.body.pipeThrough(new CompressionStream('gzip')) : response.body;
271
+ await sendJson(TunnelFrameType.HttpResponse, streamId, {
272
+ status: response.status, headers: responseHeaders, ...(gzip ? { encoding: 'gzip' } : {}),
273
+ });
274
+ if (body) {
275
+ for await (const chunk of body) {
266
276
  if (closed || stream.abort.signal.aborted) return;
267
277
  const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
268
278
  for (const piece of chunkPayload(bytes, MAX_TUNNEL_PAYLOAD_BYTES)) {
@@ -7,7 +7,7 @@
7
7
  // corrupted payload. This test must stay red until send serialization is fixed.
8
8
  // Production send path is intentionally not changed here.
9
9
 
10
- import { describe, expect, it } from 'bun:test';
10
+ import { describe, expect, it, vi } from 'vitest';
11
11
 
12
12
  import {
13
13
  MAX_TUNNEL_PAYLOAD_BYTES,
@@ -68,6 +68,42 @@ const waitFor = async (predicate, { timeoutMs = 2000, intervalMs = 5 } = {}) =>
68
68
  throw new Error('waitFor timed out');
69
69
  };
70
70
 
71
+ describe('negotiated JSON response compression', () => {
72
+ it.each([
73
+ { acceptEncoding: 'gzip', contentType: 'application/json', compressed: true },
74
+ { acceptEncoding: 'gzip', contentType: 'application/problem+json', compressed: true },
75
+ { acceptEncoding: undefined, contentType: 'application/json', compressed: false },
76
+ { acceptEncoding: 'gzip', contentType: 'text/event-stream', compressed: false },
77
+ { acceptEncoding: 'gzip', contentType: 'image/png', compressed: false },
78
+ { acceptEncoding: 'gzip', contentType: 'application/json', small: true, compressed: false },
79
+ ])('preserves body and legacy/SSE behavior for $contentType / $acceptEncoding / small=$small', async ({ acceptEncoding, contentType, small, compressed }) => {
80
+ const text = JSON.stringify({ rows: Array.from({ length: small ? 1 : 4000 }, (_, id) => ({ id, title: 'Session summary', directory: '/repo/project' })) });
81
+ const raw = textEncoder.encode(text);
82
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(raw, {
83
+ headers: { 'content-type': contentType, 'content-length': String(raw.byteLength) },
84
+ }));
85
+ const frames = [];
86
+ const host = createTunnelHost({ connectionId: 'compression-test', getLocalPort: () => 9,
87
+ sendFrame: (frame) => { frames.push(decodeTunnelFrame(frame)); }, getBufferedAmount: () => 0 });
88
+ try {
89
+ await host.handleFrame(encodeTunnelFrame(TunnelFrameType.HttpRequest, 1, encodeJsonPayload({
90
+ method: 'GET', path: '/api/test', query: '', headers: {}, ...(acceptEncoding ? { acceptEncoding } : {}),
91
+ })));
92
+ await waitFor(() => frames.some((frame) => frame.frameType === TunnelFrameType.StreamEnd));
93
+ const head = JSON.parse(textDecoder.decode(frames.find((frame) => frame.frameType === TunnelFrameType.HttpResponse).payload));
94
+ expect(head.encoding).toBe(compressed ? 'gzip' : undefined);
95
+ expect(head.headers['content-length']).toBeUndefined();
96
+ const body = new Blob(frames.filter((frame) => frame.frameType === TunnelFrameType.HttpBody).map((frame) => frame.payload));
97
+ if (compressed) {
98
+ expect(body.size).toBeLessThan(raw.byteLength / 4);
99
+ expect(await new Response(body.stream().pipeThrough(new DecompressionStream('gzip'))).text()).toBe(text);
100
+ } else {
101
+ expect(await body.text()).toBe(text);
102
+ }
103
+ } finally { host.close(); fetchMock.mockRestore(); }
104
+ });
105
+ });
106
+
71
107
  describe('isTunnelWsPathAllowed', () => {
72
108
  it('allows exact terminal/dictation/event sockets and preview proxy prefix', () => {
73
109
  expect(isTunnelWsPathAllowed('/api/terminal/ws')).toBe(true);
@@ -121,6 +121,23 @@ observer active after startup refresh work becomes idle, and successful
121
121
  event-driven index writes with a semantic snapshot change publish a new
122
122
  revision tip immediately.
123
123
 
124
+ Background directory and child-enrichment progress share a fixed 500ms trailing
125
+ tip window: the first pending change arms the injected timer, later changes do
126
+ not reset it, and delivery reads the latest revision and sync flags. New-batch
127
+ enqueue, idle (`active=false` and `enriching=false`), `publishChange()` (including
128
+ pin/rename and event-driven writes), and stop bypass the window and cancel its
129
+ pending delivery. These immediate edges are exceptions to the 500ms rate bound.
130
+ Stop publishes idle with interrupted directories marked failed; late worker
131
+ cleanup cannot emit another revision or leave a pending tip timer. Internal
132
+ revision increments and `waitForChange` delivery remain immediate, including the
133
+ final state. Full snapshots are materialized for waiting readers, not for each
134
+ tip. No persisted revision, failure, or runtime-isolation semantics change.
135
+
136
+ The shared renderer independently throttles busy tips to a fixed 1000ms window
137
+ for compatibility with older Hosts. Idle/legacy tips use 100ms debounce and
138
+ shorten a pending busy deadline without extending it. Stream-ready and safety
139
+ timeout repair retain their existing behavior; GET remains authoritative.
140
+
124
141
  The OpenCode proxy calls `noteInteractiveRequest()` for selected-session reads
125
142
  and mutations. That aborts the current background list, yields for one second,
126
143
  then resumes the same directory. Consequently a startup index refresh cannot
@@ -7,6 +7,7 @@ const FULL_RECONCILE_INTERVAL_MS = 24 * 60 * 60 * 1000;
7
7
  const FETCH_TIMEOUT_MS = 6_000;
8
8
  const INTERACTIVE_YIELD_MS = 1_000;
9
9
  const LONG_POLL_MAX_MS = 25_000;
10
+ const REVISION_TIP_THROTTLE_MS = 500;
10
11
 
11
12
  const normalizeDirectory = (value) => {
12
13
  if (typeof value !== 'string') return null;
@@ -64,7 +65,7 @@ export const createSessionIndexSyncRuntime = ({
64
65
  buildOpenCodeUrl,
65
66
  getOpenCodeAuthHeaders,
66
67
  waitForOpenCodeReady,
67
- /** Optional tip sink: ({ revision, sync }) after each revision bump. */
68
+ /** Optional tip sink: ({ revision, sync }); background progress is coalesced. */
68
69
  onRevisionTip = null,
69
70
  /**
70
71
  * Optional Host authority projection applied to each upstream list page
@@ -96,10 +97,11 @@ export const createSessionIndexSyncRuntime = ({
96
97
  const failedDirectories = new Set();
97
98
  const waiters = new Set();
98
99
  let batchDirectories = new Set();
100
+ let revisionTipTimer;
99
101
 
100
102
  const syncState = () => ({
101
- active: running || queue.length > 0,
102
- enriching: enrichingChildFlags || childFlagQueue.length > 0,
103
+ active: !stopped && (running || queue.length > 0),
104
+ enriching: !stopped && (enrichingChildFlags || childFlagQueue.length > 0),
103
105
  completed: completedDirectories.size,
104
106
  total: batchDirectories.size,
105
107
  pendingDirectories: [...queuedDirectories],
@@ -113,17 +115,17 @@ export const createSessionIndexSyncRuntime = ({
113
115
  ...sessionIndexService.snapshot(),
114
116
  });
115
117
 
116
- const notify = () => {
117
- revision += 1;
118
- const value = snapshot();
119
- for (const resolve of [...waiters]) resolve(value);
118
+ const broadcastRevisionTip = () => {
119
+ if (revisionTipTimer !== undefined) clearTimer(revisionTipTimer);
120
+ revisionTipTimer = undefined;
121
+ const sync = syncState();
120
122
  if (typeof onRevisionTip === 'function') {
121
123
  try {
122
124
  onRevisionTip({
123
- revision: value.revision,
125
+ revision,
124
126
  sync: {
125
- active: value.sync.active === true,
126
- enriching: value.sync.enriching === true,
127
+ active: sync.active,
128
+ enriching: sync.enriching,
127
129
  },
128
130
  occurredAt: now(),
129
131
  });
@@ -133,6 +135,21 @@ export const createSessionIndexSyncRuntime = ({
133
135
  }
134
136
  };
135
137
 
138
+ const notify = (immediate = false) => {
139
+ if (stopped && !immediate) return;
140
+ revision += 1;
141
+ if (waiters.size > 0) {
142
+ const value = snapshot();
143
+ for (const resolve of [...waiters]) resolve(value);
144
+ }
145
+ const sync = syncState();
146
+ if (immediate || (!sync.active && !sync.enriching)) {
147
+ broadcastRevisionTip();
148
+ } else if (revisionTipTimer === undefined && typeof onRevisionTip === 'function') {
149
+ revisionTipTimer = setTimer(broadcastRevisionTip, REVISION_TIP_THROTTLE_MS);
150
+ }
151
+ };
152
+
136
153
  const delay = (ms, signal) => new Promise((resolve) => {
137
154
  if (ms <= 0 || signal?.aborted) {
138
155
  resolve();
@@ -414,6 +431,7 @@ export const createSessionIndexSyncRuntime = ({
414
431
  };
415
432
 
416
433
  const enqueue = (directories) => {
434
+ if (stopped) return snapshot();
417
435
  const nextRuntimeKey = sessionIndexService.getRuntimeKey();
418
436
  if (nextRuntimeKey !== runtimeKey) {
419
437
  runtimeKey = nextRuntimeKey;
@@ -427,7 +445,8 @@ export const createSessionIndexSyncRuntime = ({
427
445
  failedDirectories.clear();
428
446
  batchDirectories = new Set();
429
447
  }
430
- if (!running && queue.length === 0) {
448
+ const newBatch = !running && queue.length === 0;
449
+ if (newBatch) {
431
450
  completedDirectories.clear();
432
451
  failedDirectories.clear();
433
452
  batchDirectories = new Set();
@@ -443,7 +462,7 @@ export const createSessionIndexSyncRuntime = ({
443
462
  currentWasPreempted = true;
444
463
  currentController.abort();
445
464
  }
446
- notify();
465
+ notify(newBatch);
447
466
  void run();
448
467
  return snapshot();
449
468
  };
@@ -458,7 +477,7 @@ export const createSessionIndexSyncRuntime = ({
458
477
 
459
478
  const publishChange = () => {
460
479
  if (stopped) return;
461
- notify();
480
+ notify(true);
462
481
  };
463
482
 
464
483
  const waitForChange = (since, { signal, timeoutMs = LONG_POLL_MAX_MS } = {}) => {
@@ -481,6 +500,7 @@ export const createSessionIndexSyncRuntime = ({
481
500
  };
482
501
 
483
502
  const stop = () => {
503
+ if (stopped) return;
484
504
  stopped = true;
485
505
  currentController?.abort();
486
506
  const interruptedDirectories = [...queuedDirectories];
@@ -489,7 +509,7 @@ export const createSessionIndexSyncRuntime = ({
489
509
  queuedDirectories.clear();
490
510
  queuedChildFlagKeys.clear();
491
511
  for (const directory of interruptedDirectories) failedDirectories.add(directory);
492
- notify();
512
+ notify(true);
493
513
  };
494
514
 
495
515
  return { enqueue, noteInteractiveRequest, publishChange, snapshot, waitForChange, stop };
@@ -41,6 +41,127 @@ const isFullyIdle = (runtime) => (
41
41
  );
42
42
 
43
43
  describe('session index background sync runtime', () => {
44
+ it('coalesces hundreds of directory/child checks and immediately publishes the final revision', async () => {
45
+ vi.useFakeTimers();
46
+ try {
47
+ const service = createService();
48
+ const revisionsAtDelivery = [];
49
+ const tips = vi.fn(() => { revisionsAtDelivery.push(runtime.snapshot().revision); });
50
+ const runtime = createSessionIndexSyncRuntime({
51
+ sessionIndexService: service,
52
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
53
+ getOpenCodeAuthHeaders: () => ({}),
54
+ onRevisionTip: tips,
55
+ setTimer: setTimeout,
56
+ clearTimer: clearTimeout,
57
+ fetchFn: async (url) => {
58
+ await new Promise((resolve) => setTimeout(resolve, 10));
59
+ const directory = url.searchParams.get('directory');
60
+ return new Response(JSON.stringify({ data: url.searchParams.has('parentID') ? []
61
+ : Array.from({ length: 20 }, (_, i) => session(`${directory}-${i}`, 100 - i, directory)) }));
62
+ },
63
+ });
64
+ runtime.enqueue(Array.from({ length: 15 }, (_, i) => `/repo/${i}`));
65
+ expect(tips).toHaveBeenCalledTimes(1);
66
+ expect(tips.mock.calls[0][0]).toMatchObject({ revision: 1, sync: { active: true } });
67
+ await vi.advanceTimersByTimeAsync(499);
68
+ expect(tips).toHaveBeenCalledTimes(1);
69
+ await vi.advanceTimersByTimeAsync(1);
70
+ expect(tips).toHaveBeenCalledTimes(2);
71
+ const progress = runtime.waitForChange(runtime.snapshot().revision);
72
+ await vi.advanceTimersByTimeAsync(10);
73
+ expect((await progress).revision).toBe(runtime.snapshot().revision);
74
+ const final = (async () => {
75
+ let value = runtime.snapshot();
76
+ while (value.sync.active || value.sync.enriching) value = await runtime.waitForChange(value.revision);
77
+ return value;
78
+ })();
79
+ await vi.advanceTimersByTimeAsync(2640);
80
+ expect(service.replaceDirectory).toHaveBeenCalledTimes(15);
81
+ expect(service.replaceChildSessions).toHaveBeenCalledTimes(300);
82
+ expect(isFullyIdle(runtime)).toBe(true);
83
+ expect(tips).toHaveBeenCalledTimes(8);
84
+ expect(tips.mock.calls.map(([tip]) => tip.revision)).toEqual(revisionsAtDelivery);
85
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision: runtime.snapshot().revision, sync: { active: false, enriching: false } });
86
+ expect((await final).revision).toBe(runtime.snapshot().revision);
87
+ expect(vi.getTimerCount()).toBe(0);
88
+ await vi.advanceTimersByTimeAsync(500);
89
+ expect(tips).toHaveBeenCalledTimes(8);
90
+ runtime.stop();
91
+ } finally {
92
+ vi.useRealTimers();
93
+ }
94
+ });
95
+
96
+ it('publishes external changes immediately while busy and stop cancels the pending tip', async () => {
97
+ vi.useFakeTimers();
98
+ try {
99
+ const tips = vi.fn();
100
+ let releaseReady;
101
+ const runtime = createSessionIndexSyncRuntime({
102
+ sessionIndexService: createService(),
103
+ onRevisionTip: tips,
104
+ waitForOpenCodeReady: () => new Promise((resolve) => { releaseReady = resolve; }),
105
+ setTimer: setTimeout,
106
+ clearTimer: clearTimeout,
107
+ });
108
+ runtime.enqueue(['/repo']);
109
+ expect(vi.getTimerCount()).toBe(1);
110
+ runtime.publishChange();
111
+ expect(tips).toHaveBeenCalledTimes(2);
112
+ expect(tips.mock.lastCall[0].revision).toBe(runtime.snapshot().revision);
113
+ expect(vi.getTimerCount()).toBe(0);
114
+ runtime.enqueue(['/repo/second']);
115
+ expect(vi.getTimerCount()).toBe(1);
116
+ const final = runtime.waitForChange(runtime.snapshot().revision);
117
+ runtime.stop();
118
+ const stoppedRevision = runtime.snapshot().revision;
119
+ expect(tips).toHaveBeenCalledTimes(3);
120
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision: stoppedRevision, sync: { active: false, enriching: false } });
121
+ await expect(final).resolves.toMatchObject({ revision: stoppedRevision, sync: { active: false, failedDirectories: ['/repo', '/repo/second'] } });
122
+ expect(vi.getTimerCount()).toBe(0);
123
+ releaseReady(true);
124
+ await vi.advanceTimersByTimeAsync(1000);
125
+ expect(runtime.snapshot().revision).toBe(stoppedRevision);
126
+ expect(tips).toHaveBeenCalledTimes(3);
127
+ } finally {
128
+ vi.useRealTimers();
129
+ }
130
+ });
131
+
132
+ it('stopping during enrichment flushes idle and fences late worker notifications', async () => {
133
+ vi.useFakeTimers();
134
+ try {
135
+ const tips = vi.fn();
136
+ const runtime = createSessionIndexSyncRuntime({
137
+ sessionIndexService: createService(),
138
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
139
+ getOpenCodeAuthHeaders: () => ({}),
140
+ onRevisionTip: tips,
141
+ fetchFn: async (url, { signal }) => {
142
+ if (!url.searchParams.has('parentID')) return new Response(JSON.stringify([session('root', 1)]));
143
+ return new Promise((_resolve, reject) => {
144
+ signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true });
145
+ });
146
+ },
147
+ });
148
+ runtime.enqueue(['/repo']);
149
+ await vi.advanceTimersByTimeAsync(0);
150
+ expect(runtime.snapshot().sync.enriching).toBe(true);
151
+ const final = runtime.waitForChange(runtime.snapshot().revision);
152
+ runtime.stop();
153
+ const revision = runtime.snapshot().revision;
154
+ await expect(final).resolves.toMatchObject({ revision, sync: { active: false, enriching: false } });
155
+ await vi.advanceTimersByTimeAsync(1000);
156
+ expect(tips).toHaveBeenCalledTimes(2);
157
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision, sync: { active: false, enriching: false } });
158
+ expect(runtime.snapshot().revision).toBe(revision);
159
+ expect(vi.getTimerCount()).toBe(0);
160
+ } finally {
161
+ vi.useRealTimers();
162
+ }
163
+ });
164
+
44
165
  it.each([1, 20, 21])('only advertises more when terminal pages contain over 20 visible roots (%i roots)', async (rootCount) => {
45
166
  const service = createService();
46
167
  const rows = Array.from({ length: Math.max(20, rootCount) }, (_, i) => ({
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/renderElectronMiniChatApp-t1IsnPfc.js","assets/rolldown-runtime-hePW80VL.js","assets/persistence-B4Ni3Gmv.js","assets/runtime-auth-C8w0nlKy.js","assets/runtime-switch-3ad3yzM7.js","assets/runtime-fetch-QCWYlAx0.js","assets/vendor-zustand-o69pgTEK.js","assets/vendor-base-ui-CdzJ6My_.js","assets/useUIStore-CDMI_Yzu.js","assets/vendor-react-BP2cRVqX.js","assets/vendor-pathe-vBmvQE8f.js","assets/vendor-tanstack-query-core-BWz0ose6.js","assets/platform-BQPsr8Zl.js","assets/vendor-opencode-client-MF2GBrFx.js","assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js","assets/vendor-reactuses-core-BpfIZ1tg.js","assets/vendor-lodash-es-DfLbG5Af.js","assets/vendor-js-cookie-hSOiFTtX.js","assets/button-BupmKVUs.js","assets/store-CG7Ytsg9.js","assets/vendor-class-variance-authority-BLUaYARZ.js","assets/vendor-tailwind-merge-BicNWLTg.js","assets/useMobileNavigationStore-DXpdkzw_.js","assets/vendor-tanstack-react-query-CdqCAt40.js","assets/vendor-sonner-ZS8XIOVl.js","assets/useAssistantUIStore-Dlvb7P0q.js","assets/vendor-zod-Dzk5kdUU.js","assets/vendor-fuse.js-DB5wDJbI.js","assets/Icon-B4xKSuz3.js","assets/useAppFontEffects-Ba6H7bLU.js","assets/desktopHosts-BZ7iYE-l.js","assets/desktopRelayRestore-S9wEiY2U.js","assets/vendor-pierre-diffs-DPBtNDJ5.js","assets/vendor-hast-util-to-html-Bw_SgZ1t.js","assets/vendor-character-entities-legacy-Wut6vMuj.js","assets/vendor-character-entities-html4-DlN_yAFd.js","assets/vendor-ccount-CyGsA0u7.js","assets/vendor-comma-separated-tokens-B9ouxNNS.js","assets/vendor-oniguruma-to-es-CrHzjjSU.js","assets/vendor-oniguruma-parser-D2DIvhSP.js","assets/vendor-diff-Dq7CZNim.js","assets/vendor-lru_map-BMOrLc9L.js","assets/rasterizePng-CpK-xCkB.js","assets/dualLimitLru-D8Sphgua.js","assets/AgentAvatar-CJQwq8UM.js","assets/vendor-dicebear-core-CV95S5Pg.js","assets/vendor-dicebear-styles-7lEG-JsF.js","assets/useThemeSystem-8jhE4Zyk.js","assets/dialog-BYLD4Cm3.js","assets/gitApi-BtJfaFQy.js","assets/vendor-simplewebauthn-browser-Dwhcdg_5.js","assets/projectMeta-zESSNFVw.js","assets/input-DgrLdIys.js","assets/tooltip-Dbgnq-Mh.js","assets/OverlayScrollbar-DIsrS93m.js","assets/textarea-Boj9axpc.js","assets/vendor-framer-motion-D-oiwsxL.js","assets/vendor-dnd-kit-core-5pNnzVdw.js","assets/vendor-dnd-kit-accessibility-COSwgjFs.js","assets/vendor-dnd-kit-sortable-Dm50I0m7.js","assets/speechSettingsQueries-PsDvs5QG.js","assets/dropdown-menu.styles-HOWSubFF.js","assets/image-loading-CsxDGgek.js","assets/fileQueries-DZR-mK8h.js","assets/useRuntimeAPIs-Cx8S1HLz.js","assets/relay-image-stream-qI2NTXbG.js","assets/image-loading-CObM4tcX.css","assets/sessionChangeSummary-CUMpXSiU.js","assets/LoadingImage-CoT3GWhg.js","assets/ImageLoadingPlaceholder-xB8WeKks.js","assets/sessionChangeSummary-vC84qM3v.css","assets/insertionBoundaries-z2K9a6oY.js","assets/fileMentionTouchSelection-BcqQPEDT.js","assets/appThemeRegistry-DUH4Fprj.js","assets/MarkdownRenderer-TmfHeWHu.js","assets/markdownHeightCache-Dyctmwqv.js","assets/url-Bd61gNgl.js","assets/markdown-worker-DFNZ3KJ_.js","assets/useEffectiveDirectory-wtPL_zdo.js","assets/FadeInOnReveal-BNVExwoL.js","assets/markdownTheme-CV7_0QAL.js","assets/useWorkerHighlightedLines-BXDSKlPX.js","assets/vendor-tanstack-react-virtual-B_EO--9t.js","assets/vendor-cmdk-RDxC-9Iz.js","assets/vendor-aria-hidden-BbL3cEyI.js","assets/sortable-tabs-strip-DogWaIrG.js","assets/vendor-motion-88WXBOBf.js","assets/vendor-legendapp-list-CNxU_ALy.js","assets/useFontPreferences-lAw0Cbza.js","assets/terminalApi-kRMttlcP.js","assets/useDesktopWindowControlsLayout-CbeUwtDM.js","assets/src-CdGit3gf.css"])))=>i.map(i=>d[i]);
2
- import{t as e}from"./runtimeConfig-5NAbSrPI.js";import{o as t}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";/* empty css */window.__OPENCHAMBER_RUNTIME_APIS__=e(),t(async()=>{let{renderElectronMiniChatApp:e}=await import(`./renderElectronMiniChatApp-t1IsnPfc.js`);return{renderElectronMiniChatApp:e}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91])).then(({renderElectronMiniChatApp:t})=>{t(window.__OPENCHAMBER_RUNTIME_APIS__??e())});
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/renderMobileApp-BrC28EkU.js","assets/rolldown-runtime-hePW80VL.js","assets/runtime-auth-C8w0nlKy.js","assets/runtime-switch-3ad3yzM7.js","assets/runtime-fetch-QCWYlAx0.js","assets/persistence-B4Ni3Gmv.js","assets/vendor-zustand-o69pgTEK.js","assets/vendor-base-ui-CdzJ6My_.js","assets/useUIStore-CDMI_Yzu.js","assets/vendor-react-BP2cRVqX.js","assets/vendor-pathe-vBmvQE8f.js","assets/vendor-tanstack-query-core-BWz0ose6.js","assets/platform-BQPsr8Zl.js","assets/vendor-opencode-client-MF2GBrFx.js","assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js","assets/capgoAdapter-KNLHDj7G.js","assets/types-BuiqMmh_.js","assets/vendor-pierre-diffs-DPBtNDJ5.js","assets/vendor-hast-util-to-html-Bw_SgZ1t.js","assets/vendor-character-entities-legacy-Wut6vMuj.js","assets/vendor-character-entities-html4-DlN_yAFd.js","assets/vendor-ccount-CyGsA0u7.js","assets/vendor-comma-separated-tokens-B9ouxNNS.js","assets/vendor-oniguruma-to-es-CrHzjjSU.js","assets/vendor-oniguruma-parser-D2DIvhSP.js","assets/vendor-diff-Dq7CZNim.js","assets/vendor-lru_map-BMOrLc9L.js","assets/vendor-sonner-ZS8XIOVl.js","assets/vendor-reactuses-core-BpfIZ1tg.js","assets/vendor-lodash-es-DfLbG5Af.js","assets/vendor-js-cookie-hSOiFTtX.js","assets/button-BupmKVUs.js","assets/store-CG7Ytsg9.js","assets/vendor-class-variance-authority-BLUaYARZ.js","assets/vendor-tailwind-merge-BicNWLTg.js","assets/MobileShareBridge-pYQFFYmm.js","assets/MobileDetailNavigation-CtGncmBY.js","assets/vendor-tanstack-react-query-CdqCAt40.js","assets/useMobileNavigationStore-DXpdkzw_.js","assets/useAssistantUIStore-Dlvb7P0q.js","assets/vendor-zod-Dzk5kdUU.js","assets/vendor-fuse.js-DB5wDJbI.js","assets/Icon-B4xKSuz3.js","assets/rasterizePng-CpK-xCkB.js","assets/AgentAvatar-CJQwq8UM.js","assets/vendor-dicebear-core-CV95S5Pg.js","assets/vendor-dicebear-styles-7lEG-JsF.js","assets/dualLimitLru-D8Sphgua.js","assets/useThemeSystem-8jhE4Zyk.js","assets/dialog-BYLD4Cm3.js","assets/gitApi-BtJfaFQy.js","assets/useAppFontEffects-Ba6H7bLU.js","assets/desktopHosts-BZ7iYE-l.js","assets/desktopRelayRestore-S9wEiY2U.js","assets/vendor-simplewebauthn-browser-Dwhcdg_5.js","assets/projectMeta-zESSNFVw.js","assets/input-DgrLdIys.js","assets/tooltip-Dbgnq-Mh.js","assets/OverlayScrollbar-DIsrS93m.js","assets/textarea-Boj9axpc.js","assets/vendor-framer-motion-D-oiwsxL.js","assets/vendor-dnd-kit-core-5pNnzVdw.js","assets/vendor-dnd-kit-accessibility-COSwgjFs.js","assets/vendor-dnd-kit-sortable-Dm50I0m7.js","assets/speechSettingsQueries-PsDvs5QG.js","assets/dropdown-menu.styles-HOWSubFF.js","assets/image-loading-CsxDGgek.js","assets/fileQueries-DZR-mK8h.js","assets/useRuntimeAPIs-Cx8S1HLz.js","assets/relay-image-stream-qI2NTXbG.js","assets/image-loading-CObM4tcX.css","assets/sessionChangeSummary-CUMpXSiU.js","assets/LoadingImage-CoT3GWhg.js","assets/ImageLoadingPlaceholder-xB8WeKks.js","assets/sessionChangeSummary-vC84qM3v.css","assets/insertionBoundaries-z2K9a6oY.js","assets/fileMentionTouchSelection-BcqQPEDT.js","assets/appThemeRegistry-DUH4Fprj.js","assets/MarkdownRenderer-TmfHeWHu.js","assets/markdownHeightCache-Dyctmwqv.js","assets/url-Bd61gNgl.js","assets/markdown-worker-DFNZ3KJ_.js","assets/useEffectiveDirectory-wtPL_zdo.js","assets/FadeInOnReveal-BNVExwoL.js","assets/markdownTheme-CV7_0QAL.js","assets/useWorkerHighlightedLines-BXDSKlPX.js","assets/vendor-tanstack-react-virtual-B_EO--9t.js","assets/vendor-cmdk-RDxC-9Iz.js","assets/vendor-aria-hidden-BbL3cEyI.js","assets/sortable-tabs-strip-DogWaIrG.js","assets/vendor-motion-88WXBOBf.js","assets/vendor-legendapp-list-CNxU_ALy.js","assets/useFontPreferences-lAw0Cbza.js","assets/terminalApi-kRMttlcP.js","assets/MobileTabPageHeader-D7NAfatx.js","assets/vendor-codemirror-autocomplete-7c2L_4dM.js","assets/vendor-codemirror-lang-angular-DXOgWEVj.js","assets/vendor-codemirror-lang-json-mLulNbwB.js","assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js","assets/vendor-codemirror-lang-python-YM2WVLHF.js","assets/vendor-codemirror-legacy-modes-DwltVFbT.js","assets/mcpQueries-BSwb38Pz.js","assets/collapsible-BAPWS6G-.js","assets/vendor-codemirror-commands-B8KrBQ9B.js","assets/vendor-codemirror-search-ZqtrPLjU.js","assets/vendor-replit-codemirror-vim-DDj0dSVY.js","assets/vendor-codemirror-language-data-Ctd4WxgW.js","assets/projectActions-CWvNGKfA.js","assets/runtimeEndpointReset-CxreCHBU.js","assets/scheduledTaskNavigation-C1PrFEhV.js","assets/sessionLookup-DD8YnthC.js","assets/vendor-cron-parser-BmO80h0C.js","assets/runtimeEndpointReset-ByXtaeWD.css","assets/SettingsView-D7Ykm-CM.js","assets/vendor-yaml-C9KlJAlj.js","assets/vendor-qrcode-D7jBCyl-.js","assets/vendor-dijkstrajs-BAzPDGZM.js","assets/AssistantView-j9aI8OWI.js","assets/assistant-attachment-cache-Bzewqov4.js","assets/src-CdGit3gf.css"])))=>i.map(i=>d[i]);
2
- import{t as e}from"./runtimeConfig-5NAbSrPI.js";import{o as t}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";/* empty css */window.__OPENCHAMBER_RUNTIME_APIS__=e(),t(async()=>{let{renderMobileApp:e}=await import(`./renderMobileApp-BrC28EkU.js`);return{renderMobileApp:e}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119])).then(({renderMobileApp:t})=>{t(window.__OPENCHAMBER_RUNTIME_APIS__??e())});
@@ -1 +0,0 @@
1
- import{n as e}from"./rolldown-runtime-hePW80VL.js";var t=`openchamber-relay-v1`,n=65526,r={HttpRequest:1,HttpBody:2,HttpResponse:3,StreamEnd:4,StreamAbort:5,WsOpen:6,WsOpened:7,WsText:8,WsBinary:9,WsClose:10,Ping:11,Pong:12},i=new Set(Object.values(r)),a=e=>i.has(e),o={ControlReplaced:4001,DuplicateClient:4002,StuckControlReset:4003,HostUnavailable:4008,AuthFailed:4010,LimitExceeded:4029,HostWentAway:1012,RekeyMismatch:1008,ChannelFailure:1011},s=globalThis.crypto.subtle,c={name:`ECDH`,namedCurve:`P-256`},l=16,u=32,d=16,f=4,p=8,m=class extends Error{constructor(e){super(e),this.name=`RelayCryptoError`}},h=()=>s.generateKey(c,!0,[`deriveBits`]),g=async e=>{let t=await s.exportKey(`jwk`,e);return{kty:t.kty,crv:t.crv,x:t.x,y:t.y}},_=async e=>{if(e.kty!==`EC`||e.crv!==`P-256`||typeof e.x!=`string`||typeof e.y!=`string`)throw new m(`invalid ECDH public key JWK`);try{return await s.importKey(`jwk`,{kty:e.kty,crv:e.crv,x:e.x,y:e.y,ext:!0},c,!0,[])}catch{throw new m(`invalid ECDH public key JWK`)}},v=()=>{let e=new Uint8Array(l);return globalThis.crypto.getRandomValues(e),e},y=async(e,n,r)=>{if(r.length!==l)throw new m(`invalid handshake nonce length`);let i=await s.deriveBits({name:`ECDH`,public:n},e,256),a=await s.importKey(`raw`,i,`HKDF`,!1,[`deriveBits`]),o=new Uint8Array(await s.deriveBits({name:`HKDF`,hash:`SHA-256`,salt:r,info:new TextEncoder().encode(t)},a,512)),c=(e,t)=>s.importKey(`raw`,e,{name:`AES-GCM`},!1,t);return{clientToHost:await c(o.slice(0,u),[`encrypt`,`decrypt`]),hostToClient:await c(o.slice(u),[`encrypt`,`decrypt`])}},b=(e,t,n)=>{for(let r=7;r>=0;--r)e[t+r]=Number(n&255n),n>>=8n},x=(e,t)=>{let n=0n;for(let r=0;r<p;r+=1)n=n<<8n|BigInt(e[t+r]);return n},S=e=>{let t=new Uint8Array(f);globalThis.crypto.getRandomValues(t);let n=0n;return{async encrypt(r){if(r.length>65536)throw new m(`plaintext frame exceeds maximum size`);n+=1n;let i=new Uint8Array(12);i.set(t,0),b(i,f,n);let a=new Uint8Array(await s.encrypt({name:`AES-GCM`,iv:i},e,r)),o=new Uint8Array(13+a.length);return o[0]=1,o.set(i,1),o.set(a,13),o}}},C=e=>{let t=0n;return{async decrypt(n){if(n.length<13+d)throw new m(`encrypted frame too short`);if(n[0]!==1)throw new m(`unsupported encrypted frame version`);let r=n.slice(1,13),i=x(r,f);if(i<=t)throw new m(`frame counter regression`);let a;try{a=await s.decrypt({name:`AES-GCM`,iv:r},e,n.slice(13))}catch{throw new m(`frame decryption failed`)}return t=i,new Uint8Array(a)}}},w=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`,ee=e=>{let t=``;for(let n=0;n<e.length;n+=3){let r=e[n],i=n+1<e.length?e[n+1]:void 0,a=n+2<e.length?e[n+2]:void 0;t+=w[r>>2],t+=w[(r&3)<<4|(i??0)>>4],i!==void 0&&(t+=w[(i&15)<<2|(a??0)>>6]),a!==void 0&&(t+=w[a&63])}return t},te=e=>{let t;try{t=JSON.parse(e)}catch{return null}if(typeof t!=`object`||!t)return null;let n=t;if(n.v!==1)return null;let r=n.batch===!0;return n.t===`ready`?{t:`ready`,v:1,batch:r}:n.t===`hello`&&typeof n.nonce==`string`&&typeof n.clientPubJwk==`object`&&n.clientPubJwk!==null?{t:`hello`,v:1,clientPubJwk:n.clientPubJwk,nonce:n.nonce,batch:r}:null},ne=e=>({type:`fail`,closeCode:o.ChannelFailure,reason:e}),re=async(e,t={})=>{let n=t.batch!==!1,r=await _(e),i=await h(),a=v(),o={t:`hello`,v:1,clientPubJwk:await g(i.publicKey),nonce:ee(a),...n?{batch:!0}:{}},s=!1;return{helloText:JSON.stringify(o),get established(){return s},async handleText(e){if(s)return te(e)?.t===`ready`?{type:`ignore`}:ne(`plaintext frame on established channel`);let t=te(e);if(t?.t!==`ready`)return{type:`ignore`};let o=await y(i.privateKey,r,a);return s=!0,{type:`established`,batch:n&&t.batch===!0,channel:{encryptor:S(o.clientToHost),decryptor:C(o.hostToClient)}}}}},T=4294967295,E=class extends Error{constructor(e){super(e),this.name=`TunnelCodecError`}},D=(e,t,n,r=!1)=>{if(!Number.isInteger(t)||t<0||t>T)throw new E(`invalid stream id`);if(n.length>65526)throw new E(`tunnel payload exceeds maximum size`);let i=new Uint8Array(5+n.length);return i[0]=r?e|128:e,i[1]=t>>>24&255,i[2]=t>>>16&255,i[3]=t>>>8&255,i[4]=t&255,i.set(n,5),i},ie=e=>{if(e.length<5)throw new E(`tunnel frame too short`);let t=e[0],n=!!(t&128),r=t&-129;if(!a(r))throw new E(`unknown tunnel frame type ${r}`);return{frameType:r,streamId:(e[1]<<24|e[2]<<16|e[3]<<8|e[4])>>>0,payload:e.slice(5),hasMoreFragments:n}},O=new TextEncoder,k=new TextDecoder,A=e=>O.encode(JSON.stringify(e)),j=(e,t)=>{let n;try{n=JSON.parse(k.decode(e))}catch{throw new E(`malformed JSON tunnel payload`)}if(!t(n))throw new E(`unexpected JSON tunnel payload shape`);return n},M=(e,t=n)=>{if(t<=0||t>65526)throw new E(`invalid chunk size`);if(e.length===0)return[new Uint8Array];let r=[];for(let n=0;n<e.length;n+=t)r.push(e.slice(n,n+t));return r},N=(e,t,n)=>{let r=M(n);return r.map((n,i)=>D(e,t,n,i<r.length-1))},ae=(e=16777216)=>{let t=new Map;return{push(n){let r=`${n.streamId}:${n.frameType}`,i=t.get(r);if(!n.hasMoreFragments&&!i)return n.payload;let a=i?.chunks??[],o=(i?.totalBytes??0)+n.payload.length;if(o>e)throw t.delete(r),new E(`fragmented message exceeds maximum size`);if(a.push(n.payload),n.hasMoreFragments)return t.set(r,{chunks:a,totalBytes:o}),null;t.delete(r);let s=new Uint8Array(o),c=0;for(let e of a)s.set(e,c),c+=e.length;return s},dropStream(e){for(let n of t.keys())n.startsWith(`${e}:`)&&t.delete(n)}}},oe=e=>{if(e.length===0)throw new E(`cannot encode an empty frame batch`);if(e.length===1){let t=e[0],n=new Uint8Array(1+t.length);if(n[0]=0,n.set(t,1),n.length>65536)throw new E(`frame batch exceeds maximum plaintext size`);return n}let t=1;for(let n of e)t+=4+n.length;if(t>65536)throw new E(`frame batch exceeds maximum plaintext size`);let n=new Uint8Array(t);n[0]=1;let r=1;for(let t of e)n[r]=t.length>>>24&255,n[r+1]=t.length>>>16&255,n[r+2]=t.length>>>8&255,n[r+3]=t.length&255,r+=4,n.set(t,r),r+=t.length;return n},se=e=>{if(e.length<1)throw new E(`empty batch plaintext`);let t=e[0];if(t===0)return[e.slice(1)];if(t!==1)throw new E(`unknown batch container tag ${t}`);let n=[],r=1;for(;r<e.length;){if(r+4>e.length)throw new E(`truncated batch frame length`);let t=(e[r]<<24|e[r+1]<<16|e[r+2]<<8|e[r+3])>>>0;if(r+=4,r+t>e.length)throw new E(`truncated batch frame body`);n.push(e.slice(r,r+t)),r+=t}if(n.length===0)throw new E(`empty frame batch`);return n},ce=new Set([r.HttpBody,r.WsText,r.WsBinary]),le=e=>{let t=e.windowMs??150,n=e.maxBatchBytes??24576,r=e.maxBatchFrames??32,i=e.now??(()=>Date.now()),a=e.setTimer??((e,t)=>setTimeout(e,t)),o=e.clearTimer??(e=>clearTimeout(e)),s=[],c=0,l=null,u=0,d=!1,f=()=>{l!==null&&(o(l),l=null)},p=()=>{if(f(),s.length===0)return;let t=s;s=[],c=0,u=i(),e.sendBatch(oe(t))};return{enqueue:e=>{if(d)return;let o=e[0]&-129;if(!ce.has(o)){s.push(e),p();return}let f=i();if(s.length===0&&f-u>=t){s.push(e),p();return}let m=4+e.length;if(s.length>0&&1+c+m>65536&&p(),s.push(e),c+=m,c>=n||s.length>=r){p();return}l===null&&(l=a(p,t))},flush:p,dispose(){d=!0,f(),s=[],c=0}}},ue=()=>{let e=1;return{next(){if(e>T)throw new E(`stream id space exhausted`);let t=e;return e+=2,t}}},P=e=>typeof e==`object`&&!!e&&!Array.isArray(e),F=e=>P(e)&&Object.values(e).every(e=>typeof e==`string`),de=e=>P(e)&&typeof e.status==`number`&&F(e.headers),I=e=>P(e)&&typeof e.reason==`string`,fe=e=>P(e)&&typeof e.code==`number`&&typeof e.reason==`string`,pe=/^[a-z][a-z\d+.-]*:\/\//i,L=`http://tunnel.invalid`,me=e=>{try{let t=pe.test(e)?new URL(e):new URL(e,L);return`${t.pathname}${t.search}`}catch{return e}},R=e=>({async*[Symbol.asyncIterator](){yield e}}),he=e=>({async*[Symbol.asyncIterator](){let t=e.getReader();try{for(;;){let{done:e,value:n}=await t.read();if(e)return;n&&(yield n)}}finally{t.releaseLock()}}}),ge=e=>{let t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)),t},_e=async e=>{if(e==null)return{body:null};if(e instanceof ReadableStream)return{body:he(e)};if(typeof e==`string`)return{body:R(new TextEncoder().encode(e))};if(e instanceof ArrayBuffer)return{body:R(new Uint8Array(e.slice(0)))};if(ArrayBuffer.isView(e))return{body:R(ge(e))};let t=new Response(e),n=t.headers.get(`content-type`)??void 0;return{body:R(new Uint8Array(await t.arrayBuffer())),contentType:n}},ve=async(e,t)=>{let n,r=new Headers,i=`GET`,a=null,o;e instanceof Request?(n=e.url,i=e.method,e.headers.forEach((e,t)=>r.set(t,e)),o=e.signal,a=e.body):n=e.toString(),t&&(t.method&&(i=t.method),t.headers&&new Headers(t.headers).forEach((e,t)=>r.set(t,e)),t.body!==void 0&&(a=t.body),t.signal&&(o=t.signal));let s=pe.test(n)?new URL(n):new URL(n,L),{body:c,contentType:l}=await _e(a);l&&!r.has(`content-type`)&&r.set(`content-type`,l);let u={};return r.forEach((e,t)=>{u[t]=e}),{method:i.toUpperCase(),path:s.pathname,query:s.search.startsWith(`?`)?s.search.slice(1):s.search,headers:u,body:c,signal:o}},ye=new Uint8Array,be=new TextEncoder,xe=new TextDecoder,z=e=>e instanceof Error?e:Error(String(e)),B=()=>new DOMException(`The operation was aborted.`,`AbortError`),Se=e=>{e.binaryType=`arraybuffer`;let t={get readyState(){return e.readyState},send(t){e.send(t)},close(t,n){e.close(t,n)},onopen:null,onmessage:null,onclose:null,onerror:null};return e.onopen=()=>t.onopen?.(),e.onmessage=e=>t.onmessage?.({data:e.data}),e.onclose=e=>t.onclose?.({code:e.code,reason:e.reason}),e.onerror=()=>t.onerror?.(),t},Ce=e=>{let t={get readyState(){return e.readyState},get binaryType(){return e.binaryType},set binaryType(t){t&&(e.binaryType=t)},onopen:null,onmessage:null,onerror:null,onclose:null,send(t){e.send(t)},close(t,n){e.close(t,n)}};return e.onopen=()=>t.onopen?.(),e.onmessage=e=>{let n=e.data;(typeof n==`string`||n instanceof ArrayBuffer)&&t.onmessage?.({data:n})},e.onerror=()=>t.onerror?.(),e.onclose=e=>t.onclose?.({code:e.code,reason:e.reason}),t},we=0,Te=1,Ee=2,De=3,Oe=new Set([o.AuthFailed,o.DuplicateClient,o.LimitExceeded]),ke={[o.AuthFailed]:`relay authentication failed`,[o.DuplicateClient]:`relay connection replaced by another client`,[o.LimitExceeded]:`relay connection limit reached`},Ae=()=>{let e=typeof navigator<`u`&&navigator.onLine===!1,t=typeof document<`u`&&document.visibilityState===`hidden`;return e||t},je=e=>{let t=e.helloRetryMs??1e3,n=e.helloTimeoutMs??3e4,i=e.pingIntervalMs??3e4,a=e.pingTimeoutMs??15e3,o=e.batchWindowMs??150,s=e.batch!==!1,c=e.reconnectBaseDelayMs??1e3,l=e.reconnectMaxDelayMs??3e4,u=e.hiddenOrOfflineMaxDelayMs??6e4,d=e.createWireSocket??(e=>Se(new WebSocket(e))),f=!1,p={state:`idle`},m=new Set,h=null,g=null,_=null,v=0,y=0,b=null,x=[],S=!1,C=e=>{if(p.state!==e.state||p.lastError!==e.lastError){p=e;for(let e of m)try{e(p)}catch{}}},w=e=>{let t=x;x=[];for(let n of t)n.reject(e)},ee=e=>{let t=x;x=[];for(let n of t)n.resolve(e)},te=(e,t)=>{e.dead=!0;let n=Array.from(e.streams.values());e.streams.clear();for(let e of n)try{e.fail(t)}catch{}},ne=()=>{b!==null&&(clearTimeout(b),b=null)},T=()=>{f||b===null||Ae()||(ne(),k(),P())},E=()=>{typeof document<`u`&&document.visibilityState===`visible`&&T()},O=()=>{S||typeof window>`u`||(S=!0,window.addEventListener(`online`,T),typeof document<`u`&&document.addEventListener(`visibilitychange`,E))},k=()=>{!S||typeof window>`u`||(S=!1,window.removeEventListener(`online`,T),typeof document<`u`&&document.removeEventListener(`visibilitychange`,E))},oe=()=>{if(f||b!==null)return;let e=Math.max(0,y-1),t=c*2**Math.min(e,10),n=Ae()?u:l,r=Math.min(n,t);O(),b=setTimeout(()=>{b=null,k(),P()},r)},ce=()=>{let t=new URL(e.relayUrl);return t.searchParams.set(`v`,`1`),t.searchParams.set(`role`,`client`),t.searchParams.set(`serverId`,e.serverId),e.grant&&t.searchParams.set(`grant`,e.grant),t.toString()},P=async()=>{if(f)return;ne(),v+=1;let c=v;C({state:y>0?`reconnecting`:`connecting`,lastError:p.lastError});let l;try{l=await re(e.hostEncPubJwk,{batch:s})}catch(e){if(c!==v||f)return;fe(c,z(e),!0);return}if(c!==v||f)return;let u;try{u=d(ce())}catch(e){fe(c,z(e));return}g=u;let m=!1,b=null,x=null,S=null,T=null,E=Promise.resolve(),O=null,k=null,A=!1,j=null,M=Date.now(),N=()=>{b!==null&&(clearInterval(b),b=null),x!==null&&(clearTimeout(x),x=null),S!==null&&(clearInterval(S),S=null),T!==null&&(clearTimeout(T),T=null),j!==null&&(j.dispose(),j=null)};_=N;function P(e,t=!1,n=!1){if(!(m||c!==v)){m=!0,N(),O&&(h=null,te(O,Error(`relay tunnel reset: ${e.message}`))),w(e);try{u.close()}catch{}if(g===u&&(g=null),!f){if(y+=1,n){C({state:`error`,lastError:e.message});return}C({state:t?`error`:`reconnecting`,lastError:e.message}),oe()}}}let F=()=>{try{u.send(l.helloText)}catch{}},de=(e,t)=>{k=e,A=t,b!==null&&(clearInterval(b),b=null),x!==null&&(clearTimeout(x),x=null);let n=new Map,s=ue(),c=ae(),l=Promise.resolve(),d=t=>{l=l.then(async()=>{if(p.dead)return;let n=await e.encryptor.encrypt(t);u.send(n)}).catch(()=>{})},f=t?le({windowMs:o,sendBatch:d}):null;j=f;let p={streams:n,assembler:c,nextStreamId:()=>s.next(),dead:!1,send(e){if(p.dead)return;let t=e[0]&-129;t!==r.Ping&&t!==r.Pong&&(M=Date.now()),f?f.enqueue(e):d(e)}};O=p,h=p,y=0,M=Date.now(),C({state:`connected`}),ee(p),S=setInterval(()=>{Date.now()-M<i||(p.send(D(r.Ping,0,ye)),T===null&&(T=setTimeout(()=>{T=null,P(Error(`relay keepalive timeout`))},a)))},i)},I=(e,t)=>{let n;try{n=ie(t)}catch(e){P(z(e));return}if(T!==null&&(clearTimeout(T),T=null),n.frameType===r.Ping){e.send(D(r.Pong,n.streamId,ye));return}if(n.frameType===r.Pong)return;M=Date.now();let i=n.payload;if(n.frameType===r.WsText||n.frameType===r.WsBinary){let t;try{t=e.assembler.push(n)}catch(e){P(z(e));return}if(t===null)return;i=t}else if(n.hasMoreFragments){P(Error(`unexpected fragmented tunnel frame`));return}let a=e.streams.get(n.streamId);a&&a.handleFrame(n.frameType,i)};u.onopen=()=>{m||c!==v||(F(),b=setInterval(F,t))},u.onmessage=e=>{if(m||c!==v)return;let t=e.data;if(typeof t==`string`){E=E.then(async()=>{if(m||c!==v)return;let e=await l.handleText(t);if(e.type===`established`){if(k)return;de(e.channel,e.batch)}else e.type===`fail`&&P(Error(`relay handshake failed: ${e.reason}`))}).catch(e=>{P(z(e))});return}let n=t instanceof ArrayBuffer?new Uint8Array(t):t instanceof Uint8Array?t:null;n&&(E=E.then(async()=>{if(m||c!==v)return;let e=O,t=k;if(!e||!t){P(Error(`encrypted frame before handshake completed`));return}let r;try{r=await t.decryptor.decrypt(n)}catch(e){P(z(e));return}if(A){let t;try{t=se(r)}catch(e){P(z(e));return}for(let n of t){if(m||c!==v||e.dead)return;I(e,n)}return}I(e,r)}).catch(e=>{P(z(e))}))},u.onclose=e=>{let t=Oe.has(e.code);P(Error(ke[e.code]??`relay socket closed (code ${e.code})`),t,t)},u.onerror=()=>{},x=setTimeout(()=>{x=null,P(Error(`relay handshake timeout`),!0)},n);function fe(e,t,n=!1){e!==v||f||(w(t),y+=1,C({state:n?`error`:`reconnecting`,lastError:t.message}),oe())}},F=e=>f?Promise.reject(Error(`relay tunnel closed`)):e?.aborted?Promise.reject(B()):h&&!h.dead?Promise.resolve(h):new Promise((t,n)=>{let r=null,i={resolve(n){r&&e&&e.removeEventListener(`abort`,r),t(n)},reject(t){r&&e&&e.removeEventListener(`abort`,r),n(t)}};e&&(r=()=>{x=x.filter(e=>e!==i),n(B())},e.addEventListener(`abort`,r,{once:!0})),x.push(i)}),pe=async(e,t)=>{let n=await ve(e,t),i=n.signal;if(i?.aborted)throw B();let a=await F(i),o=a.nextStreamId();return await new Promise((e,t)=>{let s=!1,c=!1,l=null,u=null,d=()=>{a.streams.delete(o),a.assembler.dropStream(o),u&&i&&i.removeEventListener(`abort`,u)},f=e=>{if(!c){if(c=!0,d(),!s){t(e);return}try{l?.error(e)}catch{}}},p=e=>{a.dead||a.send(D(r.StreamAbort,o,A({reason:e})))};u=()=>{p(`aborted`),f(B())},a.streams.set(o,{handleFrame(t,n){if(t===r.HttpResponse){if(s||c)return;let t;try{t=j(n,de)}catch(e){p(`malformed response head`),f(z(e));return}let r=t.status===204||t.status===205||t.status===304,i=null;r||(i=new ReadableStream({start(e){l=e},cancel(){c||(c=!0,d(),p(`response body cancelled`))}})),s=!0,e(new Response(i,{status:t.status,headers:t.headers})),r&&(c=!0,d());return}if(t===r.HttpBody){if(!s||c)return;try{l?.enqueue(n)}catch{}return}if(t===r.StreamEnd){if(c)return;if(!s){f(Error(`tunnel stream ended before response head`));return}c=!0,d();try{l?.close()}catch{}return}if(t===r.StreamAbort){let e=`stream aborted by host`;try{e=j(n,I).reason}catch{}f(Error(e))}},fail(e){f(e)}}),i&&i.addEventListener(`abort`,u,{once:!0});let m={method:n.method,path:n.path,query:n.query,headers:n.headers};a.send(D(r.HttpRequest,o,A(m))),(async()=>{try{if(n.body)for await(let e of n.body){if(c||a.dead)return;for(let t of M(e))a.send(D(r.HttpBody,o,t))}!c&&!a.dead&&a.send(D(r.StreamEnd,o,ye))}catch(e){p(`request body failed`),f(z(e))}})()})},L=e=>{let t=e.indexOf(`?`);return t===-1?{path:e,query:``}:{path:e.slice(0,t),query:e.slice(t+1)}};return P(),{fetch:pe,openWebSocket:(e,t,n)=>{let i=we,a=null,o=0,s=!1,c={get readyState(){return i},onopen:null,onmessage:null,onerror:null,onclose:null,send(e){if(i!==Te||!a||a.dead)throw Error(`relay tunnel socket is not open`);if(typeof e==`string`){for(let t of N(r.WsText,o,be.encode(e)))a.send(t);return}let t=e instanceof ArrayBuffer?new Uint8Array(e.slice(0)):(()=>{let t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)),t})();for(let e of N(r.WsBinary,o,t))a.send(e)},close(e=1e3,t=``){i!==De&&i!==Ee&&(i===Te&&a&&!a.dead&&(i=Ee,a.send(D(r.WsClose,o,A({code:e,reason:t})))),l(e,t))}},l=(e,t,n=!1)=>{if(!s){if(s=!0,a&&(a.streams.delete(o),a.assembler.dropStream(o)),i=De,n)try{c.onerror?.()}catch{}try{c.onclose?.({code:e,reason:t})}catch{}}};return(async()=>{let u;try{u=await F()}catch(e){l(1006,z(e).message,!0);return}if(s)return;a=u,o=u.nextStreamId(),u.streams.set(o,{handleFrame(e,t){if(e===r.WsOpened){if(i===we){i=Te;try{c.onopen?.()}catch{}}return}if(e===r.WsText){try{c.onmessage?.({data:xe.decode(t)})}catch{}return}if(e===r.WsBinary){let e=new ArrayBuffer(t.byteLength);new Uint8Array(e).set(t);try{c.onmessage?.({data:e})}catch{}return}if(e===r.WsClose){let e=1e3,n=``;try{let r=j(t,fe);e=r.code,n=r.reason}catch{}l(e,n);return}if(e===r.StreamAbort){let e=`stream aborted`;try{e=j(t,I).reason}catch{}l(1006,e,!0)}},fail(e){l(1012,e.message,!0)}});let{path:d,query:f}=L(e),p={path:d,query:f,...t&&t.length>0?{protocols:t}:{},...n&&Object.keys(n).length>0?{headers:n}:{}};u.send(D(r.WsOpen,o,A(p)))})(),c},getStatus:()=>p,subscribeStatus(e){return m.add(e),()=>{m.delete(e)}},close:()=>{if(f)return;f=!0,v+=1,ne(),k(),_?.(),_=null;let e=h;h=null;let t=Error(`relay tunnel closed`);e&&te(e,t),w(t);try{g?.close()}catch{}g=null,C({state:`idle`})}}},V=null,H=null,Me=(e,t)=>e.relayUrl===t.relayUrl&&e.serverId===t.serverId&&e.grant===t.grant&&JSON.stringify(e.hostEncPubJwk)===JSON.stringify(t.hostEncPubJwk),Ne=()=>V,Pe=()=>H,Fe=()=>V!==null,Ie=()=>V?.getStatus().state===`connected`,Le=e=>V&&H&&Me(H,e)?V:(V?.close(),H=e,V=je(e),V),Re=(e,t)=>{V!==t&&(V?.close(),H=e,V=t)},ze=()=>{V?.close(),V=null,H=null},Be=e({acquireRuntimeUrlAuthToken:()=>kt,beginRuntimeAuthEndpointSwitch:()=>ot,buildRuntimeAuthHeaders:()=>jt,clearRuntimeUrlAuthToken:()=>X,endRuntimeAuthEndpointSwitch:()=>st,getLocalRuntimeUrlAuthTokenSync:()=>_t,getRuntimeAuthGeneration:()=>Ge,getRuntimeBearerTokenSync:()=>Z,getRuntimeExtraHeadersSync:()=>ft,getRuntimeUrlAuthTokenSync:()=>gt,invalidateRuntimeAuthSession:()=>lt,refreshLocalRuntimeUrlAuthToken:()=>St,refreshRuntimeUrlAuthToken:()=>xt,setRuntimeBearerToken:()=>ut,setRuntimeExtraHeaders:()=>dt,setRuntimeUrlAuthToken:()=>pt,subscribeRuntimeAuthGeneration:()=>ct,subscribeRuntimeUrlAuthToken:()=>At}),Ve=()=>null,He=``,Ue={},U=``,W=0,G=null,K=``,q=0,J=null,Y=0,We=1e4,Ge=()=>Y,Ke=e=>e.toLowerCase()===`authorization`,qe=e=>{let t={};for(let[n,r]of Object.entries(e||{})){let e=n.trim(),i=r.trim();e&&i&&!Ke(e)&&(t[e]=i)}return t},Je=(e,t)=>{let n=Object.entries(e);return n.length===Object.keys(t).length&&n.every(([e,n])=>t[e]===n)},Ye=e=>typeof e==`string`?e.trim():``,Xe=()=>{if(typeof window>`u`)return``;let e=window.__OPENCHAMBER_CLIENT_TOKEN__;return Ye(e)},Ze=()=>{if(typeof window>`u`)return``;let e=window.__OPENCHAMBER_API_BASE_URL__;return typeof e==`string`?e.trim():``},Qe=(e,t)=>{let n=typeof e==`string`&&e.trim()?e.trim():Ze();if(!n)return t;try{return new URL(t,`${n.replace(/\/+$/,``)}/`).toString()}catch{return t}},X=()=>{U=``,W=0,K=``,q=0},$e=new Set,et=null,tt=0,nt=async e=>{let t=new TextEncoder().encode(e),n=await crypto.subtle.digest(`SHA-256`,t);return[...new Uint8Array(n)].map(e=>e.toString(16).padStart(2,`0`)).join(``)},rt=e=>{for(let t of $e)try{t(e)}catch{}},it=e=>e||(tt>0?`endpoint-switch`:`credential`),at=(e={})=>{let t=e.previousBearerDigest===void 0?et??void 0:e.previousBearerDigest||void 0;Y+=1,G=null,X(),Ot(),rt({generation:Y,reason:it(e.reason),...t?{previousBearerDigest:t}:{}})},ot=()=>{tt+=1},st=()=>{tt=Math.max(0,tt-1)},ct=e=>($e.add(e),()=>{$e.delete(e)}),lt=()=>{at({reason:`invalidate`})},ut=e=>{let t=Ye(e),n=t===Z(),r=et;He=t,Ve=()=>t?{type:`bearer`,token:t}:null,!n&&(et=null,at({previousBearerDigest:r}),t&&nt(t).then(e=>{Z()===t&&(et=e)}).catch(()=>void 0))},dt=e=>{let t=qe(e);Je(Ue,t)||(Ue=t,at({previousBearerDigest:null}))},ft=()=>{if(Object.keys(Ue).length>0)return Ue;if(typeof window>`u`)return{};let e=window.__OPENCHAMBER_RUNTIME_HEADERS__;return e&&typeof e==`object`?qe(e):{}},Z=()=>He||Xe(),pt=(e,t)=>{let n=Ye(e);if(!n||typeof t!=`number`||!Number.isFinite(t)){X();return}let r=U;U=n,W=t,r&&r!==n&&Et()},mt=()=>!U||W<=Date.now()+We?(X(),``):U,ht=()=>!K||q<=Date.now()+We?(K=``,q=0,``):K,gt=()=>{let e=mt();return!e&&(Z()||typeof window<`u`)&&xt().catch(()=>{}),e},_t=e=>{let t=ht();return!t&&e&&typeof window<`u`&&St(e).catch(()=>{}),t},vt=async()=>{let e=await Ve(),t=e?.type===`bearer`?Ye(e.token):Z();return t?{type:`bearer`,token:t}:null},yt=e=>{if(G)return G;let t=Y,n=(async()=>{let n=await vt(),r=new Headers;for(let[e,t]of Object.entries(ft()))r.set(e,t);n?.type===`bearer`&&r.set(`Authorization`,`Bearer ${n.token}`);let i=Ne(),a=i?await i.fetch(`/auth/url-token`,{method:`POST`,headers:r}):await fetch(Qe(e,`/auth/url-token`),{method:`POST`,headers:r,credentials:`include`});if(!a.ok)throw t===Y&&X(),Error(`Failed to mint runtime URL auth token (${a.status})`);let o=await a.json().catch(()=>null),s=typeof o?.token==`string`?o.token.trim():``,c=typeof o?.expiresAt==`number`?o.expiresAt:0;if(t!==Y)throw Error(`Runtime URL auth token response is stale`);if(pt(s,c),!U)throw Error(`Runtime URL auth token response was invalid`);return U})().finally(()=>{G===n&&(G=null)});return G=n,G},bt=e=>{if(J)return J;let t=(async()=>{let t=await fetch(Qe(e,`/auth/url-token`),{method:`POST`,credentials:`include`});if(!t.ok)throw K=``,q=0,Error(`Failed to mint local runtime URL auth token (${t.status})`);let n=await t.json().catch(()=>null),r=typeof n?.token==`string`?n.token.trim():``,i=typeof n?.expiresAt==`number`?n.expiresAt:0;if(!r||!Number.isFinite(i))throw Error(`Local runtime URL auth token response was invalid`);return K=r,q=i,r})().finally(()=>{J===t&&(J=null)});return J=t,J},xt=async e=>mt()||yt(e),St=async e=>ht()||bt(e),Q=0,$=null,Ct=null,wt=new Set,Tt=5e3,Et=()=>{for(let e of wt)try{e()}catch{}},Dt=()=>{$!==null&&(clearTimeout($),$=null)},Ot=()=>{if(Dt(),Q<=0||typeof window>`u`)return;let e=W-We-Tt,t=W>0?Math.max(0,e-Date.now()):0;$=setTimeout(()=>{$=null,!(Q<=0)&&yt(Ct).catch(()=>{}).finally(()=>{Ot()})},t)},kt=e=>{typeof e==`string`&&e.trim()&&(Ct=e.trim()),Q+=1,Ot();let t=!1;return()=>{t||(t=!0,Q=Math.max(0,Q-1),Q===0&&Dt())}},At=e=>(wt.add(e),()=>{wt.delete(e)}),jt=async e=>{let t=new Headers(e);for(let[e,n]of Object.entries(ft()))t.has(e)||t.set(e,n);if(t.has(`Authorization`))return t;let n=await vt();return n?.type===`bearer`&&t.set(`Authorization`,`Bearer ${n.token}`),t};export{Ne as C,Ce as D,je as E,L as O,Pe as S,Ie as T,ct as _,st as a,Re as b,Z as c,lt as d,St as f,dt as g,ut as h,X as i,me as k,ft as l,Be as m,ot as n,_t as o,xt as p,jt as r,Ge as s,kt as t,gt as u,At as v,Fe as w,ze as x,Le as y};