claude-code-session-manager 0.21.1 → 0.21.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 (54) hide show
  1. package/bin/cli.cjs +5 -0
  2. package/dist/assets/{TiptapBody-C46DacIO.js → TiptapBody-PdmsfUCQ.js} +2 -2
  3. package/dist/assets/cssMode-DfqZGMQs.js +1 -0
  4. package/dist/assets/{freemarker2-BxIPNQn-.js → freemarker2-XTPYh37h.js} +1 -1
  5. package/dist/assets/handlebars-DKUF5VyH.js +1 -0
  6. package/dist/assets/html-uqoqsIeI.js +1 -0
  7. package/dist/assets/htmlMode-aMTQs1su.js +1 -0
  8. package/dist/assets/index-DO3ROR11.js +3525 -0
  9. package/dist/assets/index-DeQI4oVI.css +32 -0
  10. package/dist/assets/javascript-BVxRZMds.js +1 -0
  11. package/dist/assets/{jsonMode-1FAJaHiX.js → jsonMode-D04xP2s5.js} +4 -4
  12. package/dist/assets/liquid-BkQHTH2P.js +1 -0
  13. package/dist/assets/lspLanguageFeatures-By9uLznH.js +4 -0
  14. package/dist/assets/mdx-Du1IlbjV.js +1 -0
  15. package/dist/assets/{index-oGyPFfYZ.css → monaco-editor-BTnBOi8r.css} +1 -32
  16. package/dist/assets/monaco-editor-BW5C4Iv1.js +908 -0
  17. package/dist/assets/python-DSlImqXd.js +1 -0
  18. package/dist/assets/razor-BmUVyvSK.js +1 -0
  19. package/dist/assets/{tsMode-CLQIVays.js → tsMode-Btj0TTH7.js} +1 -1
  20. package/dist/assets/typescript-Bzelq9vO.js +1 -0
  21. package/dist/assets/xml-Whd9EaSd.js +1 -0
  22. package/dist/assets/yaml-QYf0-IN8.js +1 -0
  23. package/dist/index.html +4 -2
  24. package/package.json +1 -1
  25. package/src/main/__tests__/runVerify.test.cjs +101 -0
  26. package/src/main/config.cjs +36 -4
  27. package/src/main/historyAggregator.cjs +400 -149
  28. package/src/main/index.cjs +8 -0
  29. package/src/main/ipcSchemas.cjs +42 -13
  30. package/src/main/kg.cjs +87 -30
  31. package/src/main/lib/credentials.cjs +7 -0
  32. package/src/main/lib/e2eStateMachine.cjs +39 -0
  33. package/src/main/runVerify.cjs +28 -5
  34. package/src/main/scheduler/prdParser.cjs +16 -1
  35. package/src/main/scheduler.cjs +97 -13
  36. package/src/main/transcripts.cjs +141 -19
  37. package/src/main/usageMatrix.cjs +7 -3
  38. package/src/main/webRemote.cjs +190 -29
  39. package/src/preload/api.d.ts +40 -0
  40. package/src/preload/index.cjs +7 -0
  41. package/dist/assets/cssMode-CauFS5Bp.js +0 -1
  42. package/dist/assets/handlebars-DnEVFUsu.js +0 -1
  43. package/dist/assets/html-S8NXUTqc.js +0 -1
  44. package/dist/assets/htmlMode-rSEyII9x.js +0 -1
  45. package/dist/assets/index-DMobTczM.js +0 -4431
  46. package/dist/assets/javascript-BiWR68QP.js +0 -1
  47. package/dist/assets/liquid-CEtOkbwI.js +0 -1
  48. package/dist/assets/lspLanguageFeatures-CRF3U0x3.js +0 -4
  49. package/dist/assets/mdx-C7C95Bzt.js +0 -1
  50. package/dist/assets/python-CXvKcjLk.js +0 -1
  51. package/dist/assets/razor-tzZHfRy2.js +0 -1
  52. package/dist/assets/typescript-LxhyM9W2.js +0 -1
  53. package/dist/assets/xml-VS_m20VE.js +0 -1
  54. package/dist/assets/yaml-BsjggdVD.js +0 -1
@@ -7,7 +7,7 @@
7
7
  * - Outbound-only: opens a WS TO the relay, never listens.
8
8
  * - OFF by default: remoteEnabled must be explicitly true in web-remote.json.
9
9
  * - Kill switch: synchronous — drops socket + refuses all commands instantly.
10
- * - Strict allowlist: 15 enumerated command types; unknown → silent drop.
10
+ * - Strict allowlist: enumerated command types; unknown → opaque rejected response.
11
11
  * - Zod validation: every payload parsed before any dispatch.
12
12
  * - Path safety: cwd/path fields go through validatePath (home-dir boundary).
13
13
  * - Token at rest: web-remote.json written at 0600 via writeTextAtomic.
@@ -28,6 +28,7 @@ const { writeTextAtomic, validatePath } = require('./config.cjs');
28
28
  const logs = require('./logs.cjs');
29
29
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
30
30
  const { schemas } = require('./ipcSchemas.cjs');
31
+ const { makeState, confirmSas: confirmSasLogic } = require('./lib/e2eStateMachine.cjs');
31
32
 
32
33
  // ─── Constants ───────────────────────────────────────────────────────────────
33
34
 
@@ -66,7 +67,7 @@ const MSG_MAX_BYTES = 256 * 1024;
66
67
 
67
68
  // Single source of truth lives in ipcSchemas.cjs — imported here so the test
68
69
  // can verify the same Set without depending on Electron-linked modules.
69
- const { ALLOWED_COMMANDS } = require('./ipcSchemas.cjs');
70
+ const { ALLOWED_COMMANDS, MUTATE_COMMANDS, SAS_GATED_READS } = require('./ipcSchemas.cjs');
70
71
 
71
72
  // ─── E2E encryption helpers (P-256 ECDH + AES-256-GCM, ADR §5.2) ────────────
72
73
 
@@ -111,6 +112,30 @@ function deriveSessionKey(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
111
112
  return Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 32));
112
113
  }
113
114
 
115
+ /**
116
+ * Derive a 6-digit Short Authentication String from the ECDH shared secret.
117
+ * Uses a separate HKDF info label ('sm-sas-v1') so the SAS is independent of
118
+ * the session key. Both sides compute the same value; user confirms they match.
119
+ */
120
+ function deriveSas(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
121
+ const myPrivKey = crypto.createPrivateKey({
122
+ key: Buffer.from(myPrivateKeyB64, 'base64url'),
123
+ format: 'der',
124
+ type: 'pkcs8',
125
+ });
126
+ const peerPubKey = crypto.createPublicKey({
127
+ key: Buffer.from(peerPublicKeyB64, 'base64url'),
128
+ format: 'der',
129
+ type: 'spki',
130
+ });
131
+ const sharedSecret = crypto.diffieHellman({ privateKey: myPrivKey, publicKey: peerPubKey });
132
+ const salt = Buffer.from(deviceId, 'utf8');
133
+ const info = Buffer.from('sm-sas-v1', 'utf8');
134
+ const sasBytes = Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 3));
135
+ const sasNum = ((sasBytes[0] << 16) | (sasBytes[1] << 8) | sasBytes[2]) % 1_000_000;
136
+ return sasNum.toString().padStart(6, '0');
137
+ }
138
+
114
139
  /**
115
140
  * Encrypt a plaintext JSON string into an AES-256-GCM box.
116
141
  * @returns { nonce: base64url, ciphertext: base64url } — the nonce is 12 random bytes
@@ -161,12 +186,13 @@ let _configCacheAt = 0;
161
186
  let _destroyed = false; // set at app shutdown to stop reconnect loops
162
187
 
163
188
  // E2E session state — reset on each new WS connection.
164
- let _e2eSessionKey = null; // Buffer | null
189
+ // .state: 'idle' | 'pending_sas' | 'authenticated' | 'failed'
190
+ let _e2e = makeState();
165
191
 
166
192
  // ─── Config helpers ───────────────────────────────────────────────────────────
167
193
 
168
194
  function defaultConfig() {
169
- return { remoteEnabled: false, devices: [] };
195
+ return { remoteEnabled: false, remoteControlEnabled: false, devices: [] };
170
196
  }
171
197
 
172
198
  function loadConfigSync() {
@@ -219,7 +245,8 @@ async function auditLog(ts, type, deviceId, msgId, result) {
219
245
  try {
220
246
  const ymd = ts.slice(0, 10);
221
247
  const logPath = path.join(AUDIT_LOG_DIR, `remote-audit-${ymd}.log`);
222
- const line = `${ts} ${type} deviceId=${deviceId || '-'} msgId=${msgId || '-'} result=${result}\n`;
248
+ const clean = (s) => String(s ?? '').replace(/[\r\n\t]+/g, ' ').slice(0, 200);
249
+ const line = `${ts} ${clean(type)} deviceId=${clean(deviceId) || '-'} msgId=${clean(msgId) || '-'} result=${result}\n`;
223
250
  const handle = await fsp.open(logPath, 'a', 0o600);
224
251
  try {
225
252
  await handle.write(line);
@@ -285,6 +312,12 @@ async function getDeviceTicket(deviceToken) {
285
312
  return result.ticket;
286
313
  }
287
314
 
315
+ // ─── E2E state helpers ────────────────────────────────────────────────────────
316
+
317
+ function resetE2e(state = 'idle') {
318
+ _e2e = makeState(state);
319
+ }
320
+
288
321
  // ─── WebSocket lifecycle ──────────────────────────────────────────────────────
289
322
 
290
323
  function broadcastStatus() {
@@ -293,8 +326,12 @@ function broadcastStatus() {
293
326
  const connected = _ws !== null && _ws.readyState === WebSocket.OPEN;
294
327
  sendIfAlive(_window, 'webRemote:status', {
295
328
  enabled: cfg.remoteEnabled,
329
+ remoteControlEnabled: cfg.remoteControlEnabled ?? false,
296
330
  connected,
297
- e2eActive: connected && _e2eSessionKey !== null,
331
+ e2eActive: connected && _e2e.sessionKey !== null,
332
+ e2eAuthenticated: connected && _e2e.state === 'authenticated',
333
+ e2eState: connected ? _e2e.state : 'idle',
334
+ pendingSas: connected && _e2e.state === 'pending_sas' ? _e2e.pendingSas : null,
298
335
  devices: (cfg.devices || []).map(({ deviceId, deviceName, issuedAt, lastConnectedAt }) => ({
299
336
  deviceId, deviceName, issuedAt, lastConnectedAt,
300
337
  })),
@@ -358,7 +395,7 @@ function scheduleReconnect() {
358
395
  async function disconnect() {
359
396
  cancelReconnect();
360
397
  stopHeartbeat();
361
- _e2eSessionKey = null;
398
+ resetE2e();
362
399
  if (_ws) {
363
400
  const ws = _ws;
364
401
  _ws = null;
@@ -407,7 +444,7 @@ async function connect() {
407
444
 
408
445
  _ws = ws;
409
446
  _missedPongs = 0;
410
- _e2eSessionKey = null; // reset session key on new connection
447
+ resetE2e(); // reset E2E state on new connection
411
448
 
412
449
  ws.on('open', () => {
413
450
  logs.writeLine({ scope: 'webRemote', level: 'info', message: 'connected to relay' });
@@ -426,6 +463,8 @@ async function connect() {
426
463
  }).catch(() => {});
427
464
  broadcastStatus();
428
465
  // v2: begin pushing the live session list once connected.
466
+ // Reset diff-guard so the new client always receives a full session-list push.
467
+ _lastSessionListJson = null;
429
468
  startSessionListPush();
430
469
  });
431
470
 
@@ -442,7 +481,7 @@ async function connect() {
442
481
  ws.on('close', (code) => {
443
482
  stopHeartbeat();
444
483
  stopAllSessionWatches();
445
- _e2eSessionKey = null;
484
+ resetE2e();
446
485
  if (_ws === ws) _ws = null;
447
486
  logs.writeLine({ scope: 'webRemote', level: 'info', message: 'ws closed', meta: { code } });
448
487
  broadcastStatus();
@@ -483,14 +522,15 @@ const SESSION_INIT_TAIL_BYTES = 512 * 1024; // bound the initial read
483
522
 
484
523
  const _sessionWatchers = new Map(); // tabId → watcher
485
524
  let _sessionListTimer = null;
525
+ let _lastSessionListJson = null; // diff-guard: skip push when snapshot unchanged
486
526
 
487
527
  /** Push an unsolicited event to the browser(s). Encrypts when an E2E key is active. */
488
528
  function pushEvent(type, payload) {
489
529
  if (!_ws || _ws.readyState !== WebSocket.OPEN) return;
490
530
  const inner = { type, id: crypto.randomUUID(), payload, ts: Date.now() };
491
531
  try {
492
- if (_e2eSessionKey) {
493
- const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2eSessionKey);
532
+ if (_e2e.sessionKey) {
533
+ const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2e.sessionKey);
494
534
  _ws.send(JSON.stringify({ type: 'e2e:box', id: inner.id, payload: { nonce, ciphertext }, ts: Date.now() }));
495
535
  } else {
496
536
  _ws.send(JSON.stringify(inner));
@@ -536,12 +576,21 @@ async function tailLines(filePath, fromOffset) {
536
576
  if (stat.size <= start) return { lines: [], size: stat.size, inode: stat.ino };
537
577
  const fd = await fsp.open(filePath, 'r');
538
578
  try {
579
+ // Drop the first fragment only when offset genuinely landed mid-line —
580
+ // i.e. the byte immediately before `start` is not '\n'. When `start`
581
+ // sits right on a newline boundary (previous poll ended at a complete
582
+ // line) the first split-part is already a full line and must be kept.
583
+ let dropFirst = false;
584
+ if (start > 0) {
585
+ const prev = Buffer.alloc(1);
586
+ await fd.read(prev, 0, 1, start - 1);
587
+ dropFirst = prev[0] !== 0x0a; // 0x0a = '\n'
588
+ }
539
589
  const len = stat.size - start;
540
590
  const buf = Buffer.alloc(len);
541
591
  await fd.read(buf, 0, len, start);
542
592
  const parts = buf.toString('utf8').split('\n').filter(Boolean);
543
- // If we started mid-file, the first fragment may be a partial line — drop it.
544
- if (start > 0 && parts.length) parts.shift();
593
+ if (dropFirst && parts.length) parts.shift();
545
594
  return { lines: parts, size: stat.size, inode: stat.ino };
546
595
  } finally {
547
596
  await fd.close();
@@ -580,7 +629,10 @@ async function pollSessionWatcher(w) {
580
629
 
581
630
  if (nextState && nextState !== w.state) {
582
631
  w.state = nextState;
583
- pushEvent('event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
632
+ // Guard: don't push state if SAS not yet confirmed (watcher may outlive an auth reset).
633
+ if (_e2e.state === 'authenticated') {
634
+ pushEvent('event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
635
+ }
584
636
  }
585
637
  if (newAssistantText && newMsgId !== w.lastMsgId) {
586
638
  w.lastAssistantText = newAssistantText;
@@ -638,6 +690,11 @@ async function pushSessionList() {
638
690
  // this stops the unsolicited background push too.
639
691
  const cfg = await loadConfig();
640
692
  if (!cfg.remoteEnabled) return;
693
+ // Don't push before SAS is confirmed — session cwds/titles are sensitive user data.
694
+ // A relay that completes e2e:hello before the user confirms the SAS would otherwise
695
+ // receive the full session list immediately (same threat SAS_GATED_READS blocks for
696
+ // cmd:sessions:load). Guard here so _lastSessionListJson is not poisoned either.
697
+ if (_e2e.state !== 'authenticated') return;
641
698
  const sessionsStore = require('./sessionsStore.cjs');
642
699
  const data = await sessionsStore.load();
643
700
  // Normalize persisted tabs → SessionMeta. tabId === claudeSessionId so it
@@ -648,7 +705,11 @@ async function pushSessionList() {
648
705
  title: t.label || t.cwd,
649
706
  state: _sessionWatchers.get(t.claudeSessionId)?.state ?? null,
650
707
  }));
651
- pushEvent('event:session:list', { sessions, activeTabId: data?.activeTabId ?? null });
708
+ const payload = { sessions, activeTabId: data?.activeTabId ?? null };
709
+ const json = JSON.stringify(payload);
710
+ if (json === _lastSessionListJson) return;
711
+ _lastSessionListJson = json;
712
+ pushEvent('event:session:list', payload);
652
713
  } catch (e) {
653
714
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'pushSessionList failed', meta: { error: e?.message } });
654
715
  }
@@ -734,6 +795,8 @@ function anthropicSummarize(apiKey, text) {
734
795
  * completed turn per subscribed tab (~$1/$5 per 1M tokens).
735
796
  */
736
797
  async function maybeSummarize(w) {
798
+ // Guard: don't push summaries (session transcript content) before SAS confirmed.
799
+ if (_e2e.state !== 'authenticated') return;
737
800
  const text = w.lastAssistantText;
738
801
  if (!text) return;
739
802
  const ofMessageId = w.lastMsgId;
@@ -822,28 +885,69 @@ async function handleMessage(raw, device) {
822
885
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello but no device private key — skipping E2E' });
823
886
  return;
824
887
  }
888
+ // Explicit P-256 curve validation — do not rely on Node's implicit throw.
889
+ // Rejects wrong-curve keys (e.g. P-384), malformed DER, and the all-zero
890
+ // identity point. Must happen before deriveSessionKey so a bad key drops
891
+ // the session here, not silently in a crypto catch-all below.
825
892
  try {
826
- _e2eSessionKey = deriveSessionKey(device.e2ePrivateKey, browserPubKey, device.deviceId);
827
- logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session key established' });
893
+ const derBytes = Buffer.from(browserPubKey, 'base64url');
894
+ const importedPub = crypto.createPublicKey({ key: derBytes, format: 'der', type: 'spki' });
895
+ if (importedPub.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {
896
+ throw new Error(`wrong curve: ${importedPub.asymmetricKeyDetails?.namedCurve}`);
897
+ }
898
+ // P-256 SPKI DER is always 91 bytes; raw EC point (04 || x || y) starts at offset 26.
899
+ // Defense-in-depth: reject the identity point (x=0, y=0) explicitly even though
900
+ // Node's ECDH would also reject it — P-256 is a prime-order group, so the identity
901
+ // is the only low-order point.
902
+ if (derBytes.length === 91) {
903
+ const x = derBytes.subarray(27, 59);
904
+ const y = derBytes.subarray(59, 91);
905
+ if (x.every((b) => b === 0) && y.every((b) => b === 0)) {
906
+ throw new Error('identity point rejected');
907
+ }
908
+ }
909
+ } catch (e) {
910
+ logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello peer key validation failed — session dropped', meta: { error: e?.message } });
911
+ await auditLog(new Date().toISOString(), 'e2e:hello', device.deviceId, undefined, 'error:invalid_peer_key');
912
+ resetE2e('failed'); // surface key-validation failure to UI — user must reconnect
913
+ broadcastStatus();
914
+ return;
915
+ }
916
+ try {
917
+ const sessionKey = deriveSessionKey(device.e2ePrivateKey, browserPubKey, device.deviceId);
918
+ let pendingSas;
919
+ try {
920
+ pendingSas = deriveSas(device.e2ePrivateKey, browserPubKey, device.deviceId);
921
+ } catch (sasErr) {
922
+ // SAS derivation failed — session cannot be verified by the user.
923
+ // Mark failed so confirm-sas returns ok:false and the UI prompts retry.
924
+ resetE2e('failed');
925
+ logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E SAS derivation failed — session marked failed', meta: { error: sasErr?.message } });
926
+ broadcastStatus();
927
+ return;
928
+ }
929
+ _e2e = makeState('pending_sas', sessionKey, pendingSas);
930
+ logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session key established — SAS pending confirmation' });
828
931
  broadcastStatus();
829
932
  // Acknowledge with e2e:ready (unencrypted — session just started)
830
933
  respond(id, undefined, 'e2e:ready');
831
934
  } catch (e) {
832
935
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E key derivation failed', meta: { error: e?.message } });
833
- _e2eSessionKey = null;
936
+ resetE2e('failed');
937
+ broadcastStatus();
834
938
  }
835
939
  return;
836
940
  }
837
941
 
838
942
  // ── Decrypt e2e:box messages ──────────────────────────────────────────────
839
943
  if (type === 'e2e:box') {
840
- if (!_e2eSessionKey) {
944
+ if (!_e2e.sessionKey) {
841
945
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box received but no session key — dropping' });
842
946
  return;
843
947
  }
844
948
  const { nonce, ciphertext } = payload || {};
845
949
  if (!nonce || !ciphertext) return;
846
- const plaintext = decryptBox(nonce, ciphertext, _e2eSessionKey);
950
+ const plaintext = decryptBox(nonce, ciphertext, _e2e.sessionKey);
847
951
  if (!plaintext) {
848
952
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box decryption failed (auth tag mismatch)' });
849
953
  return;
@@ -864,7 +968,7 @@ async function handleMessage(raw, device) {
864
968
  if (!type.startsWith('cmd:')) return;
865
969
  // After E2E is established, reject plaintext commands — a malicious relay cannot
866
970
  // silently downgrade the session by stripping e2e:hello (PENTEST.md §H1).
867
- if (_e2eSessionKey) {
971
+ if (_e2e.sessionKey) {
868
972
  logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'plaintext cmd rejected — e2e session active' });
869
973
  return;
870
974
  }
@@ -881,13 +985,31 @@ async function dispatchEnvelope(envelope, device) {
881
985
  const cfg = await loadConfig();
882
986
  if (!cfg.remoteEnabled) {
883
987
  await auditLog(ts, type, device.deviceId, id, 'error:disabled');
884
- respond(id, { error: 'disabled' });
988
+ respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
885
989
  return;
886
990
  }
887
991
 
888
- // Allowlist check — unknown types are dropped without error feedback (ADR §6.2)
992
+ // Allowlist check — reject unknown cmd:* types with opaque error (oracle prevention)
889
993
  if (!ALLOWED_COMMANDS.has(type)) {
890
994
  await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
995
+ respond(id, { error: 'rejected' });
996
+ return;
997
+ }
998
+
999
+ // MUTATE tier gate — write/exec commands require remoteControlEnabled=true (default false)
1000
+ if (MUTATE_COMMANDS.has(type) && !cfg.remoteControlEnabled) {
1001
+ await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
1002
+ respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
1003
+ return;
1004
+ }
1005
+
1006
+ // E2E auth gate — MUTATE and sensitive READ commands are blocked until the
1007
+ // user confirms the SAS on the desktop. This prevents a compromised relay
1008
+ // from exfiltrating session lists, PRDs, run logs, or transcript summaries
1009
+ // by completing the ECDH handshake without the user's knowledge.
1010
+ if ((MUTATE_COMMANDS.has(type) || SAS_GATED_READS.has(type)) && _e2e.state !== 'authenticated') {
1011
+ await auditLog(ts, type, device.deviceId, id, 'error:e2e_not_authenticated');
1012
+ respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
891
1013
  return;
892
1014
  }
893
1015
 
@@ -899,7 +1021,7 @@ async function dispatchEnvelope(envelope, device) {
899
1021
  } catch (e) {
900
1022
  const code = e?.name === 'ZodError' ? 'schema_invalid' : 'dispatch_error';
901
1023
  await auditLog(ts, type, device.deviceId, id, `error:${code}`);
902
- result = { error: code }; // never leak internal error messages to the remote caller
1024
+ result = { error: 'rejected' }; // opaque on-wire reason stays in audit log only
903
1025
  }
904
1026
 
905
1027
  respond(id, result);
@@ -919,8 +1041,8 @@ function respond(msgId, payload, typeOverride) {
919
1041
 
920
1042
  try {
921
1043
  // Encrypt the response if a session key is active.
922
- if (_e2eSessionKey && !typeOverride) {
923
- const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2eSessionKey);
1044
+ if (_e2e.sessionKey && !typeOverride) {
1045
+ const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2e.sessionKey);
924
1046
  _ws.send(JSON.stringify({
925
1047
  type: 'e2e:box',
926
1048
  id: msgId,
@@ -1139,16 +1261,41 @@ function registerRemoteHandlers() {
1139
1261
  // Returns current status without tokens — safe to expose to renderer.
1140
1262
  ipcMain.handle('webRemote:get-status', async () => {
1141
1263
  const cfg = await loadConfig();
1264
+ const connected = _ws !== null && _ws.readyState === WebSocket.OPEN;
1142
1265
  return {
1143
1266
  enabled: cfg.remoteEnabled,
1144
- connected: _ws !== null && _ws.readyState === WebSocket.OPEN,
1145
- e2eActive: _ws !== null && _ws.readyState === WebSocket.OPEN && _e2eSessionKey !== null,
1267
+ remoteControlEnabled: cfg.remoteControlEnabled ?? false,
1268
+ connected,
1269
+ e2eActive: connected && _e2e.sessionKey !== null,
1270
+ e2eAuthenticated: connected && _e2e.state === 'authenticated',
1271
+ e2eState: connected ? _e2e.state : 'idle',
1272
+ pendingSas: connected && _e2e.state === 'pending_sas' ? _e2e.pendingSas : null,
1146
1273
  devices: (cfg.devices || []).map(({ deviceId, deviceName, issuedAt, lastConnectedAt }) => ({
1147
1274
  deviceId, deviceName, issuedAt, lastConnectedAt,
1148
1275
  })),
1149
1276
  };
1150
1277
  });
1151
1278
 
1279
+ // User confirmed that the SAS shown on both desktop and browser match.
1280
+ // Returns ok:false if the session is not in the pending_sas state (e.g. key
1281
+ // missing, already failed, already authenticated, or reconnected).
1282
+ ipcMain.handle('webRemote:confirm-sas', async () => {
1283
+ const { ok, error, next } = confirmSasLogic(_e2e);
1284
+ if (!ok) {
1285
+ logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'confirm-sas rejected — wrong state', meta: { state: _e2e.state, error } });
1286
+ return { ok: false, error };
1287
+ }
1288
+ _e2e = next;
1289
+ logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session authenticated — SAS confirmed by user' });
1290
+ broadcastStatus();
1291
+ // Flush the session list immediately — the push loop was suppressed while
1292
+ // state !== 'authenticated', so the mobile app would otherwise wait up to
1293
+ // SESSION_LIST_PUSH_MS for the first useful data.
1294
+ _lastSessionListJson = null;
1295
+ pushSessionList().catch(() => {});
1296
+ return { ok: true };
1297
+ });
1298
+
1152
1299
  ipcMain.handle('webRemote:enable', async () => {
1153
1300
  const cfg = await loadConfig();
1154
1301
  await saveConfig({ ...cfg, remoteEnabled: true });
@@ -1165,6 +1312,20 @@ function registerRemoteHandlers() {
1165
1312
  return { ok: true };
1166
1313
  });
1167
1314
 
1315
+ ipcMain.handle('webRemote:enable-control', async () => {
1316
+ const cfg = await loadConfig();
1317
+ await saveConfig({ ...cfg, remoteControlEnabled: true });
1318
+ broadcastStatus();
1319
+ return { ok: true };
1320
+ });
1321
+
1322
+ ipcMain.handle('webRemote:disable-control', async () => {
1323
+ const cfg = await loadConfig();
1324
+ await saveConfig({ ...cfg, remoteControlEnabled: false });
1325
+ broadcastStatus();
1326
+ return { ok: true };
1327
+ });
1328
+
1168
1329
  ipcMain.handle('webRemote:pair', validated(schemas.webRemotePair, async ({ otp }) => {
1169
1330
  return pair(otp);
1170
1331
  }));
@@ -1212,7 +1373,7 @@ async function init() {
1212
1373
  function destroy() {
1213
1374
  _destroyed = true;
1214
1375
  cancelReconnect();
1215
- _e2eSessionKey = null;
1376
+ resetE2e();
1216
1377
  if (_ws) {
1217
1378
  try { _ws.terminate(); } catch { /* */ }
1218
1379
  _ws = null;
@@ -393,6 +393,13 @@ export interface SupervisorLogEntry {
393
393
  costUsd: number | null;
394
394
  }
395
395
 
396
+ export interface SchedulePollHealth {
397
+ lastPollAt: number | null;
398
+ lastPollOk: boolean;
399
+ consecutiveFailures: number;
400
+ lastFailureKind: string | null;
401
+ }
402
+
396
403
  export interface ScheduleStateSnapshot {
397
404
  config: ScheduleConfig & { supervisor?: SupervisorConfig };
398
405
  jobs: ScheduleJob[];
@@ -403,6 +410,8 @@ export interface ScheduleStateSnapshot {
403
410
  paused: SchedulePauseInfo | null;
404
411
  /** Latest five_hour utilization percent (0–100) cached from billing.fetchUsage. null if unknown. */
405
412
  utilization: number | null;
413
+ /** Poll health — last billing poll result; used to detect stale utilization. */
414
+ pollHealth?: SchedulePollHealth;
406
415
  /** Returned only by the initial state() call, not the broadcast event. */
407
416
  paths?: SchedulePaths;
408
417
  }
@@ -452,6 +461,19 @@ export interface ListConversationsResult {
452
461
  scannedMs: number;
453
462
  }
454
463
 
464
+ export interface SessionScanEntry {
465
+ sessionId: string;
466
+ projectEncoded: string;
467
+ path: string;
468
+ mtimeMs: number;
469
+ sizeBytes: number;
470
+ }
471
+
472
+ export interface SessionScanResult {
473
+ sessions: SessionScanEntry[];
474
+ scannedMs: number;
475
+ }
476
+
455
477
 
456
478
  export interface FileEntry {
457
479
  name: string;
@@ -793,8 +815,15 @@ export interface WebRemoteDevice {
793
815
 
794
816
  export interface WebRemoteStatus {
795
817
  enabled: boolean;
818
+ remoteControlEnabled: boolean;
796
819
  connected: boolean;
797
820
  e2eActive: boolean;
821
+ /** True once the user has confirmed the SAS on the desktop. */
822
+ e2eAuthenticated: boolean;
823
+ /** Explicit E2E session state for UI rendering. */
824
+ e2eState: 'idle' | 'pending_sas' | 'authenticated' | 'failed';
825
+ /** 6-digit Short Authentication String pending user confirmation, or null. */
826
+ pendingSas: string | null;
798
827
  devices: WebRemoteDevice[];
799
828
  }
800
829
 
@@ -858,7 +887,10 @@ export interface SessionManagerAPI {
858
887
  };
859
888
  transcripts: {
860
889
  subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
890
+ /** Release the sub back to the LRU cache (view-switch). Does not destroy the watcher. */
861
891
  unsubscribe: (tabId: string) => Promise<{ ok: boolean }>;
892
+ /** Permanently destroy the sub (genuine tab close). */
893
+ closeTab: (tabId: string) => Promise<{ ok: boolean }>;
862
894
  buffer: (tabId: string) => Promise<TranscriptEvent[]>;
863
895
  pathFor: (cwd: string, sessionUuid: string) => Promise<string>;
864
896
  onEvent: (tabId: string, handler: (ev: TranscriptEvent) => void) => () => void;
@@ -949,6 +981,7 @@ export interface SessionManagerAPI {
949
981
  history: {
950
982
  aggregate: (req?: HistoryAggregateRequest) => Promise<HistoryAggregateResult>;
951
983
  listConversations: () => Promise<ListConversationsResult>;
984
+ scanProjects: () => Promise<SessionScanResult>;
952
985
  };
953
986
  schedule: {
954
987
  state: () => Promise<ScheduleStateSnapshot>;
@@ -1062,6 +1095,10 @@ export interface SessionManagerAPI {
1062
1095
  getStatus: () => Promise<WebRemoteStatus>;
1063
1096
  enable: () => Promise<WebRemoteMutationResult>;
1064
1097
  disable: () => Promise<WebRemoteMutationResult>;
1098
+ /** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
1099
+ enableControl: () => Promise<WebRemoteMutationResult>;
1100
+ /** Block MUTATE-tier commands — mobile becomes read-only mirror. */
1101
+ disableControl: () => Promise<WebRemoteMutationResult>;
1065
1102
  pair: (otp: string) => Promise<WebRemotePairResult>;
1066
1103
  revokeDevice: (deviceId: string) => Promise<WebRemoteMutationResult>;
1067
1104
  auditTail: (lines?: number) => Promise<WebRemoteAuditTailResult>;
@@ -1071,6 +1108,9 @@ export interface SessionManagerAPI {
1071
1108
  revokeAll: () => Promise<WebRemoteMutationResult>;
1072
1109
  /** Subscribe to the completion event broadcast after revokeAll. */
1073
1110
  onRevokedAll: (handler: (ev: { revokedCount: number }) => void) => () => void;
1111
+ /** Confirm that the SAS shown on the desktop matches the browser — marks the
1112
+ * E2E session as authenticated and unblocks MUTATE-tier commands. */
1113
+ confirmSas: () => Promise<WebRemoteMutationResult>;
1074
1114
  };
1075
1115
  kg: {
1076
1116
  /** Distilled knowledge graph + ingest status for ONE project (`cwd`).
@@ -59,6 +59,7 @@ contextBridge.exposeInMainWorld('api', {
59
59
  transcripts: {
60
60
  subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
61
61
  unsubscribe: (tabId) => ipcRenderer.invoke('transcript:unsubscribe', { tabId }),
62
+ closeTab: (tabId) => ipcRenderer.invoke('transcript:close', { tabId }),
62
63
  buffer: (tabId) => ipcRenderer.invoke('transcript:buffer', { tabId }),
63
64
  pathFor: (cwd, sessionUuid) =>
64
65
  ipcRenderer.invoke('transcript:path', { cwd, sessionUuid }),
@@ -155,6 +156,7 @@ contextBridge.exposeInMainWorld('api', {
155
156
  history: {
156
157
  aggregate: (req) => ipcRenderer.invoke('history:aggregate', req),
157
158
  listConversations: () => ipcRenderer.invoke('history:list-conversations'),
159
+ scanProjects: () => ipcRenderer.invoke('history:scan-projects'),
158
160
  },
159
161
  files: {
160
162
  list: (path, showHidden) => ipcRenderer.invoke('files:list', { path, showHidden }),
@@ -283,6 +285,10 @@ contextBridge.exposeInMainWorld('api', {
283
285
  enable: () => ipcRenderer.invoke('webRemote:enable'),
284
286
  /** Turn remote control off. Immediately drops relay connection. */
285
287
  disable: () => ipcRenderer.invoke('webRemote:disable'),
288
+ /** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
289
+ enableControl: () => ipcRenderer.invoke('webRemote:enable-control'),
290
+ /** Block MUTATE-tier commands — mobile becomes read-only mirror. */
291
+ disableControl: () => ipcRenderer.invoke('webRemote:disable-control'),
286
292
  /** Pair a new device using the 8-character OTP shown in the web UI. */
287
293
  pair: (otp) => ipcRenderer.invoke('webRemote:pair', { otp }),
288
294
  /** Revoke a paired device by its deviceId. */
@@ -303,6 +309,7 @@ contextBridge.exposeInMainWorld('api', {
303
309
  },
304
310
  /** Revoke ALL paired devices and tear down every session immediately. */
305
311
  revokeAll: () => ipcRenderer.invoke('webRemote:revoke-all'),
312
+ confirmSas: () => ipcRenderer.invoke('webRemote:confirm-sas'),
306
313
  /** Push event when revokeAll completes (main broadcasts webRemote:revoked-all). */
307
314
  onRevokedAll: (handler) => {
308
315
  const listener = (_e, payload) => handler(payload);
@@ -1 +0,0 @@
1
- import{d as h,l as s}from"./index-DMobTczM.js";import{C as c,H as u,d as p,D as m,R as f,g as _,h as w,b as k,F as v,a as D,S as P,c as R,f as I}from"./lspLanguageFeatures-CRF3U0x3.js";import{e as b,i as H,j as U,t as y,k as T}from"./lspLanguageFeatures-CRF3U0x3.js";const C=120*1e3;class A{constructor(o){this._defaults=o,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>C&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=h({moduleId:"vs/language/css/cssWorker",createWorker:()=>new Worker(new URL(""+new URL("css.worker-B4z49cGk.js",import.meta.url).href,import.meta.url),{type:"module"}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...o){let e;return this._getClient().then(a=>{e=a}).then(a=>{if(this._worker)return this._worker.withSyncedResources(o)}).then(a=>e)}}function F(n){const o=[],e=[],a=new A(n);o.push(a);const r=(...t)=>a.getLanguageServiceWorker(...t);function l(){const{languageId:t,modeConfiguration:i}=n;g(e),i.completionItems&&e.push(s.registerCompletionItemProvider(t,new c(r,["/","-",":"]))),i.hovers&&e.push(s.registerHoverProvider(t,new u(r))),i.documentHighlights&&e.push(s.registerDocumentHighlightProvider(t,new p(r))),i.definitions&&e.push(s.registerDefinitionProvider(t,new m(r))),i.references&&e.push(s.registerReferenceProvider(t,new f(r))),i.documentSymbols&&e.push(s.registerDocumentSymbolProvider(t,new _(r))),i.rename&&e.push(s.registerRenameProvider(t,new w(r))),i.colors&&e.push(s.registerColorProvider(t,new k(r))),i.foldingRanges&&e.push(s.registerFoldingRangeProvider(t,new v(r))),i.diagnostics&&e.push(new D(t,r,n.onDidChange)),i.selectionRanges&&e.push(s.registerSelectionRangeProvider(t,new P(r))),i.documentFormattingEdits&&e.push(s.registerDocumentFormattingEditProvider(t,new R(r))),i.documentRangeFormattingEdits&&e.push(s.registerDocumentRangeFormattingEditProvider(t,new I(r)))}return l(),o.push(d(e)),d(o)}function d(n){return{dispose:()=>g(n)}}function g(n){for(;n.length;)n.pop().dispose()}export{c as CompletionAdapter,m as DefinitionAdapter,D as DiagnosticsAdapter,k as DocumentColorAdapter,R as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,b as DocumentLinkAdapter,I as DocumentRangeFormattingEditProvider,_ as DocumentSymbolAdapter,v as FoldingRangeAdapter,u as HoverAdapter,f as ReferenceAdapter,w as RenameAdapter,P as SelectionRangeAdapter,A as WorkerManager,H as fromPosition,U as fromRange,F as setupMode,y as toRange,T as toTextEdit};
@@ -1 +0,0 @@
1
- import{l as e}from"./index-DMobTczM.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],a={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["{{!--","--}}"]},brackets:[["<!--","-->"],["<",">"],["{{","}}"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"<",close:">"},{open:'"',close:'"'},{open:"'",close:"'"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/(\w[\w\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}]},m={defaultToken:"",tokenPostfix:"",tokenizer:{root:[[/\{\{!--/,"comment.block.start.handlebars","@commentBlock"],[/\{\{!/,"comment.start.handlebars","@comment"],[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.root"}],[/<!DOCTYPE/,"metatag.html","@doctype"],[/<!--/,"comment.html","@commentHtml"],[/(<)(\w+)(\/>)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)([:\w]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)(\w+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/</,"delimiter.html"],[/\{/,"delimiter.html"],[/[^<{]+/]],doctype:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/[^>]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/\}\}/,"comment.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentBlock:[[/--\}\}/,"comment.block.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentHtml:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],handlebarsInSimpleState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3"}],{include:"handlebarsRoot"}],handlebarsInEmbeddedState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"handlebarsRoot"}],handlebarsRoot:[[/"[^"]*"/,"string.handlebars"],[/[#/][^\s}]+/,"keyword.helper.handlebars"],[/else\b/,"keyword.helper.handlebars"],[/[\s]+/],[/[^}]/,"variable.parameter.handlebars"]]}};export{a as conf,m as language};
@@ -1 +0,0 @@
1
- import{l as e}from"./index-DMobTczM.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],i={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["<!--","-->"]},brackets:[["<!--","-->"],["<",">"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}],folding:{markers:{start:new RegExp("^\\s*<!--\\s*#region\\b.*-->"),end:new RegExp("^\\s*<!--\\s*#endregion\\b.*-->")}}},r={defaultToken:"",tokenPostfix:".html",ignoreCase:!0,tokenizer:{root:[[/<!DOCTYPE/,"metatag","@doctype"],[/<!--/,"comment","@comment"],[/(<)((?:[\w\-]+:)?[\w\-]+)(\s*)(\/>)/,["delimiter","tag","","delimiter"]],[/(<)(script)/,["delimiter",{token:"tag",next:"@script"}]],[/(<)(style)/,["delimiter",{token:"tag",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/</,"delimiter"],[/[^<]+/]],doctype:[[/[^>]+/,"metatag.content"],[/>/,"metatag","@pop"]],comment:[[/-->/,"comment","@pop"],[/[^-]+/,"comment.content"],[/./,"comment.content"]],otherTag:[[/\/?>/,"delimiter","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"module"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/'module'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}};export{i as conf,r as language};
@@ -1 +0,0 @@
1
- import{d as D,l as t}from"./index-DMobTczM.js";import{H as d,d as l,e as u,F as c,g as h,S as m,h as p,c as w,f as _,C as R}from"./lspLanguageFeatures-CRF3U0x3.js";import{D as E,a as H,b,R as y,i as U,j as T,t as x,k as M}from"./lspLanguageFeatures-CRF3U0x3.js";const I=120*1e3;class f{constructor(n){this._defaults=n,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>I&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=D({moduleId:"vs/language/html/htmlWorker",createWorker:()=>new Worker(new URL(""+new URL("html.worker-DtiGdgqp.js",import.meta.url).href,import.meta.url),{type:"module"}),createData:{languageSettings:this._defaults.options,languageId:this._defaults.languageId},label:this._defaults.languageId}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...n){let e;return this._getClient().then(r=>{e=r}).then(r=>{if(this._worker)return this._worker.withSyncedResources(n)}).then(r=>e)}}class v extends R{constructor(n){super(n,[".",":","<",'"',"=","/"])}}function C(i){const n=new f(i),e=(...o)=>n.getLanguageServiceWorker(...o);let r=i.languageId;t.registerCompletionItemProvider(r,new v(e)),t.registerHoverProvider(r,new d(e)),t.registerDocumentHighlightProvider(r,new l(e)),t.registerLinkProvider(r,new u(e)),t.registerFoldingRangeProvider(r,new c(e)),t.registerDocumentSymbolProvider(r,new h(e)),t.registerSelectionRangeProvider(r,new m(e)),t.registerRenameProvider(r,new p(e)),r==="html"&&(t.registerDocumentFormattingEditProvider(r,new w(e)),t.registerDocumentRangeFormattingEditProvider(r,new _(e)))}function L(i){const n=[],e=[],r=new f(i);n.push(r);const o=(...s)=>r.getLanguageServiceWorker(...s);function P(){const{languageId:s,modeConfiguration:a}=i;k(e),a.completionItems&&e.push(t.registerCompletionItemProvider(s,new v(o))),a.hovers&&e.push(t.registerHoverProvider(s,new d(o))),a.documentHighlights&&e.push(t.registerDocumentHighlightProvider(s,new l(o))),a.links&&e.push(t.registerLinkProvider(s,new u(o))),a.documentSymbols&&e.push(t.registerDocumentSymbolProvider(s,new h(o))),a.rename&&e.push(t.registerRenameProvider(s,new p(o))),a.foldingRanges&&e.push(t.registerFoldingRangeProvider(s,new c(o))),a.selectionRanges&&e.push(t.registerSelectionRangeProvider(s,new m(o))),a.documentFormattingEdits&&e.push(t.registerDocumentFormattingEditProvider(s,new w(o))),a.documentRangeFormattingEdits&&e.push(t.registerDocumentRangeFormattingEditProvider(s,new _(o)))}return P(),n.push(g(e)),g(n)}function g(i){return{dispose:()=>k(i)}}function k(i){for(;i.length;)i.pop().dispose()}export{R as CompletionAdapter,E as DefinitionAdapter,H as DiagnosticsAdapter,b as DocumentColorAdapter,w as DocumentFormattingEditProvider,l as DocumentHighlightAdapter,u as DocumentLinkAdapter,_ as DocumentRangeFormattingEditProvider,h as DocumentSymbolAdapter,c as FoldingRangeAdapter,d as HoverAdapter,y as ReferenceAdapter,p as RenameAdapter,m as SelectionRangeAdapter,f as WorkerManager,U as fromPosition,T as fromRange,L as setupMode,C as setupMode1,x as toRange,M as toTextEdit};