instar 1.3.1592 → 1.3.1594

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 (162) hide show
  1. package/dashboard/console/console.css +1 -0
  2. package/dashboard/console/sessions.js +1 -1
  3. package/dashboard/desktop.css +1 -0
  4. package/dashboard/desktop.html +7 -0
  5. package/dashboard/desktop.js +205 -6
  6. package/dist/commands/server.d.ts.map +1 -1
  7. package/dist/commands/server.js +3 -0
  8. package/dist/commands/server.js.map +1 -1
  9. package/dist/core/AppConversationService.d.ts +38 -0
  10. package/dist/core/AppConversationService.d.ts.map +1 -1
  11. package/dist/core/AppConversationService.js +145 -4
  12. package/dist/core/AppConversationService.js.map +1 -1
  13. package/dist/core/AppConversationStore.d.ts +60 -2
  14. package/dist/core/AppConversationStore.d.ts.map +1 -1
  15. package/dist/core/AppConversationStore.js +142 -7
  16. package/dist/core/AppConversationStore.js.map +1 -1
  17. package/dist/core/AppConversationsReplicatedStore.js +1 -1
  18. package/dist/core/AppConversationsReplicatedStore.js.map +1 -1
  19. package/dist/core/AppMediaStore.d.ts +186 -0
  20. package/dist/core/AppMediaStore.d.ts.map +1 -0
  21. package/dist/core/AppMediaStore.js +805 -0
  22. package/dist/core/AppMediaStore.js.map +1 -0
  23. package/dist/core/AppRecordSignatures.d.ts +11 -0
  24. package/dist/core/AppRecordSignatures.d.ts.map +1 -1
  25. package/dist/core/AppRecordSignatures.js +33 -3
  26. package/dist/core/AppRecordSignatures.js.map +1 -1
  27. package/dist/core/AppReplyRelayInstaller.d.ts +4 -0
  28. package/dist/core/AppReplyRelayInstaller.d.ts.map +1 -1
  29. package/dist/core/AppReplyRelayInstaller.js +17 -3
  30. package/dist/core/AppReplyRelayInstaller.js.map +1 -1
  31. package/dist/core/AutonomousStartAdmission.d.ts +10 -4
  32. package/dist/core/AutonomousStartAdmission.d.ts.map +1 -1
  33. package/dist/core/AutonomousStartAdmission.js +14 -9
  34. package/dist/core/AutonomousStartAdmission.js.map +1 -1
  35. package/dist/core/DesktopHost.d.ts +30 -0
  36. package/dist/core/DesktopHost.d.ts.map +1 -1
  37. package/dist/core/DesktopHost.js +24 -0
  38. package/dist/core/DesktopHost.js.map +1 -1
  39. package/dist/core/DirectDesktopAccess.d.ts +11 -1
  40. package/dist/core/DirectDesktopAccess.d.ts.map +1 -1
  41. package/dist/core/DirectDesktopAccess.js +46 -0
  42. package/dist/core/DirectDesktopAccess.js.map +1 -1
  43. package/dist/core/FileClassifier.d.ts.map +1 -1
  44. package/dist/core/FileClassifier.js +1 -0
  45. package/dist/core/FileClassifier.js.map +1 -1
  46. package/dist/core/InstarWorktreeManager.js +1 -1
  47. package/dist/core/InstarWorktreeManager.js.map +1 -1
  48. package/dist/core/MacDesktopHost.d.ts +12 -1
  49. package/dist/core/MacDesktopHost.d.ts.map +1 -1
  50. package/dist/core/MacDesktopHost.js +70 -6
  51. package/dist/core/MacDesktopHost.js.map +1 -1
  52. package/dist/core/MobileChatRequests.d.ts.map +1 -1
  53. package/dist/core/MobileChatRequests.js +3 -0
  54. package/dist/core/MobileChatRequests.js.map +1 -1
  55. package/dist/core/MobileDeviceKeys.d.ts +8 -2
  56. package/dist/core/MobileDeviceKeys.d.ts.map +1 -1
  57. package/dist/core/MobileDeviceKeys.js +10 -4
  58. package/dist/core/MobileDeviceKeys.js.map +1 -1
  59. package/dist/core/PostUpdateMigrator.d.ts +6 -0
  60. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  61. package/dist/core/PostUpdateMigrator.js +34 -1
  62. package/dist/core/PostUpdateMigrator.js.map +1 -1
  63. package/dist/core/WindowRunCadenceExecutor.d.ts +24 -4
  64. package/dist/core/WindowRunCadenceExecutor.d.ts.map +1 -1
  65. package/dist/core/WindowRunCadenceExecutor.js +138 -18
  66. package/dist/core/WindowRunCadenceExecutor.js.map +1 -1
  67. package/dist/core/WindowRunLivenessAuthority.d.ts +48 -4
  68. package/dist/core/WindowRunLivenessAuthority.d.ts.map +1 -1
  69. package/dist/core/WindowRunLivenessAuthority.js +186 -30
  70. package/dist/core/WindowRunLivenessAuthority.js.map +1 -1
  71. package/dist/core/WindowsDesktopHost.d.ts +9 -1
  72. package/dist/core/WindowsDesktopHost.d.ts.map +1 -1
  73. package/dist/core/WindowsDesktopHost.js +43 -4
  74. package/dist/core/WindowsDesktopHost.js.map +1 -1
  75. package/dist/core/WindowsDesktopLocalTransport.js +1 -1
  76. package/dist/core/WindowsDesktopLocalTransport.js.map +1 -1
  77. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  78. package/dist/core/WriteDomainRegistry.js +11 -0
  79. package/dist/core/WriteDomainRegistry.js.map +1 -1
  80. package/dist/core/appInboundDispatch.d.ts.map +1 -1
  81. package/dist/core/appInboundDispatch.js +11 -2
  82. package/dist/core/appInboundDispatch.js.map +1 -1
  83. package/dist/core/appMediaReplyFiles.d.ts +34 -0
  84. package/dist/core/appMediaReplyFiles.d.ts.map +1 -0
  85. package/dist/core/appMediaReplyFiles.js +172 -0
  86. package/dist/core/appMediaReplyFiles.js.map +1 -0
  87. package/dist/core/appMediaTypes.d.ts +64 -0
  88. package/dist/core/appMediaTypes.d.ts.map +1 -0
  89. package/dist/core/appMediaTypes.js +192 -0
  90. package/dist/core/appMediaTypes.js.map +1 -0
  91. package/dist/core/appMediaWorker.d.ts +19 -0
  92. package/dist/core/appMediaWorker.d.ts.map +1 -0
  93. package/dist/core/appMediaWorker.js +162 -0
  94. package/dist/core/appMediaWorker.js.map +1 -0
  95. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  96. package/dist/core/devGatedFeatures.js +6 -0
  97. package/dist/core/devGatedFeatures.js.map +1 -1
  98. package/dist/core/types.d.ts +9 -0
  99. package/dist/core/types.d.ts.map +1 -1
  100. package/dist/core/types.js.map +1 -1
  101. package/dist/data/standards-guard-index.json +1 -1
  102. package/dist/data/standards-guard-index.meta.json +2 -2
  103. package/dist/data/standards-registry.meta.json +1 -1
  104. package/dist/monitoring/templates-drift-verifier.d.ts.map +1 -1
  105. package/dist/monitoring/templates-drift-verifier.js +6 -1
  106. package/dist/monitoring/templates-drift-verifier.js.map +1 -1
  107. package/dist/native/instar-desktop-host +0 -0
  108. package/dist/native/windows/InstarDesktopCompanion.cs +84 -16
  109. package/dist/scaffold/templates.d.ts.map +1 -1
  110. package/dist/scaffold/templates.js +3 -2
  111. package/dist/scaffold/templates.js.map +1 -1
  112. package/dist/server/AgentServer.d.ts.map +1 -1
  113. package/dist/server/AgentServer.js +15 -8
  114. package/dist/server/AgentServer.js.map +1 -1
  115. package/dist/server/CapabilityIndex.js +3 -3
  116. package/dist/server/CapabilityIndex.js.map +1 -1
  117. package/dist/server/DesktopMeshBroker.d.ts +1 -1
  118. package/dist/server/DesktopMeshBroker.d.ts.map +1 -1
  119. package/dist/server/DesktopMeshBroker.js.map +1 -1
  120. package/dist/server/DesktopStreamManager.d.ts +16 -0
  121. package/dist/server/DesktopStreamManager.d.ts.map +1 -1
  122. package/dist/server/DesktopStreamManager.js +39 -1
  123. package/dist/server/DesktopStreamManager.js.map +1 -1
  124. package/dist/server/mobileConversationRoutes.d.ts +5 -2
  125. package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
  126. package/dist/server/mobileConversationRoutes.js +186 -24
  127. package/dist/server/mobileConversationRoutes.js.map +1 -1
  128. package/dist/server/mobileMediaRoutes.d.ts +12 -0
  129. package/dist/server/mobileMediaRoutes.d.ts.map +1 -0
  130. package/dist/server/mobileMediaRoutes.js +337 -0
  131. package/dist/server/mobileMediaRoutes.js.map +1 -0
  132. package/dist/server/mobilePairingRoutes.d.ts +5 -0
  133. package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
  134. package/dist/server/mobilePairingRoutes.js +43 -2
  135. package/dist/server/mobilePairingRoutes.js.map +1 -1
  136. package/dist/server/routes.d.ts.map +1 -1
  137. package/dist/server/routes.js +218 -33
  138. package/dist/server/routes.js.map +1 -1
  139. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  140. package/dist/testing/selfActionRegistry.js +33 -0
  141. package/dist/testing/selfActionRegistry.js.map +1 -1
  142. package/package.json +1 -1
  143. package/scripts/blocking-decision-surfaces-baseline.json +2 -2
  144. package/scripts/windows-desktop-local-transport.ps1 +2 -2
  145. package/src/data/builtin-manifest.json +74 -66
  146. package/src/data/standards-guard-index.json +1 -1
  147. package/src/data/standards-guard-index.meta.json +2 -2
  148. package/src/data/standards-registry.meta.json +1 -1
  149. package/src/data/state-coherence-registry.json +40 -0
  150. package/src/scaffold/templates.ts +3 -2
  151. package/src/templates/scripts/app-media-path +58 -0
  152. package/src/templates/scripts/app-reply.sh +65 -4
  153. package/upgrades/1.3.1593.md +53 -0
  154. package/upgrades/1.3.1594.md +31 -0
  155. package/upgrades/side-effects/console-composer-placeholder.eli16.md +7 -0
  156. package/upgrades/side-effects/console-composer-placeholder.md +31 -0
  157. package/upgrades/side-effects/desktop-multi-display-gestures.md +73 -0
  158. package/upgrades/side-effects/desktop-stream-frame-stats.md +45 -0
  159. package/upgrades/side-effects/mobile-native-chat-media-inc1.md +146 -0
  160. package/upgrades/side-effects/window-run-liveness-per-topic.eli16.md +28 -0
  161. package/upgrades/side-effects/window-run-liveness-per-topic.md +81 -0
  162. package/upgrades/1.3.1592.md +0 -22
@@ -1009,6 +1009,7 @@ button.run-mark:focus-visible { outline: 2px solid var(--accent); outline-offset
1009
1009
  .sess-resume { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); z-index: 2; }
1010
1010
  .sess-composer { display: flex; gap: var(--s-2); align-items: flex-end; position: sticky; bottom: 0; padding: var(--s-2) 0; background: var(--bg); }
1011
1011
  .sess-input { flex: 1; min-height: 44px; font-size: 16px; resize: none; }
1012
+ .sess-input::placeholder { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1012
1013
  .sess-composer-btns { display: flex; gap: var(--s-2); }
1013
1014
  .sess-attach { min-width: 44px; min-height: 44px; padding: 0 10px; font-size: 18px; }
1014
1015
  .sess-attach:disabled { opacity: .45; cursor: not-allowed; }
@@ -239,7 +239,7 @@ function openView(box, row) {
239
239
  const termBox = el('div', { class: 'sess-term', 'aria-label': 'Live terminal' });
240
240
  const resume = el('button', { class: 'btn-primary btn-inline sess-resume', type: 'button', text: '▼ Back to live', hidden: true });
241
241
  const more = el('button', { class: 'btn-quiet sess-more', type: 'button', 'aria-label': 'More', text: '⋯', 'data-more': '1' });
242
- const input = el('textarea', { class: 'reason-input sess-input', rows: '1', placeholder: 'Type a message…', 'aria-label': 'Message to the session' });
242
+ const input = el('textarea', { class: 'reason-input sess-input', rows: '1', placeholder: 'Message…', 'aria-label': 'Message to the session' });
243
243
  const sendBtn = el('button', { class: 'btn-primary btn-inline', type: 'button', text: 'Send', 'data-send': '1' });
244
244
  const keysBtn = el('button', { class: 'btn-quiet', type: 'button', text: 'Keys', 'aria-expanded': 'false', 'data-keys': '1' });
245
245
  // Attach: a screenshot or file, picked, pasted or dropped. Saved on this machine, so a
@@ -20,6 +20,7 @@ body.desktop-live #keyboardBar input{-webkit-touch-callout:default;-webkit-user-
20
20
  .desktop-dock button{min-width:2.9rem;min-height:2.9rem;padding:.35rem;background:#111827e8;color:#f8fafc;border:1px solid #64748b;box-shadow:0 2px 12px #0009}
21
21
  .desktop-tools{position:absolute;z-index:6;right:4rem;bottom:max(.7rem,env(safe-area-inset-bottom));width:min(19rem,calc(100vw - 5rem));max-height:min(60dvh,32rem);overflow:auto;display:flex;flex-wrap:wrap;gap:.4rem;padding:.55rem;background:#111827f2;border:1px solid #64748b;border-radius:10px;box-shadow:0 4px 18px #0009}
22
22
  .desktop-tools[hidden],#keyboardBar[hidden],.desktop-dock button[hidden],.desktop-tools button[hidden]{display:none}
23
+ .display-note{flex:1 0 100%;margin:0;font-size:.8rem;color:#f8fafc}.display-note[hidden]{display:none}#displayMenu button{flex:1 0 100%;text-align:left}
23
24
  .desktop-tools button{flex:1 0 auto;padding:.5rem .65rem}.type-label{display:block;margin:0;font-size:.8rem}
24
25
  .desktop-tools button[aria-pressed="true"]{background:#fbbf24;color:#422006}
25
26
  #keyboardBar{position:absolute;z-index:7;left:max(.5rem,env(safe-area-inset-left));right:max(.5rem,env(safe-area-inset-right));bottom:max(.5rem,env(safe-area-inset-bottom));display:flex;align-items:center;gap:.4rem;padding:.4rem;background:#111827f2;border:1px solid #64748b;border-radius:10px}
@@ -36,11 +36,18 @@
36
36
  </div>
37
37
  <div class="desktop-dock" aria-label="Remote desktop controls">
38
38
  <button id="toolsButton" type="button" aria-label="More controls" aria-expanded="false" aria-controls="desktopTools">☰</button>
39
+ <button id="displaysButton" type="button" aria-label="Screens" aria-expanded="false" aria-controls="displayMenu" hidden>⧉</button>
39
40
  <button id="keyboardButton" type="button" aria-label="Show keyboard" aria-expanded="false" aria-controls="keyboardBar">⌨</button>
40
41
  <button id="zoomResetButton" type="button" aria-label="Fit screen" hidden>Fit</button>
41
42
  <button id="fullscreenButton" type="button" aria-label="Full screen">⛶</button>
42
43
  </div>
44
+ <div class="desktop-tools" id="displayMenu" aria-label="Screens" hidden></div>
43
45
  <div class="desktop-tools" id="desktopTools" hidden>
46
+ <p id="displayNote" class="display-note" hidden></p>
47
+ <button data-shortcut="overview" type="button" hidden></button>
48
+ <button data-shortcut="appWindows" type="button" hidden></button>
49
+ <button data-shortcut="previousSpace" type="button" hidden></button>
50
+ <button data-shortcut="nextSpace" type="button" hidden></button>
44
51
  <button id="modeButton" type="button" aria-pressed="true" hidden>Trackpad · swipe to move</button>
45
52
  <button id="dragButton" type="button" aria-pressed="false">Drag</button>
46
53
  <button id="rightButton" type="button">Right click</button>
@@ -89,8 +89,38 @@ const LONG_PRESS_MS = 550;
89
89
  const DOUBLE_TAP_MS = 300;
90
90
  let mousePress = null;
91
91
  let lastTap = null;
92
+ // A lone tap's click waits this long. A click followed at once by a press reads on
93
+ // the host as a double-click, which maximizes a window being dragged by its title.
94
+ let pendingTapClick = 0;
92
95
  let dragPointerId = null;
93
96
  let multiTap = null;
97
+ // What the connected machine says about its screens. Until it answers
98
+ // supported, selectDisplay is never sent: an older host ends the session on a
99
+ // command it does not know.
100
+ let displayInfo = null;
101
+ let overviewOpen = null;
102
+ let threeFinger = null;
103
+ const THREE_FINGER_TRAVEL = 60;
104
+ // While a switch settles the picture still shows the old screen, and a tap would
105
+ // land on the new one at the wrong place. Hold pointer presses until a new frame.
106
+ let screenSwitch = null;
107
+ const SCREEN_SWITCH_SETTLE_MS = 400;
108
+ const SCREEN_SWITCH_LIMIT_MS = 3_000;
109
+ // Standard shortcuts, sent as ordinary keys. Spaces follow the fingers as on a
110
+ // Mac trackpad: swiping left brings in the Space on the right.
111
+ const DESKTOP_SHORTCUTS = {
112
+ macos: {
113
+ overview: { label: 'Mission Control', hint: 'Control+Up', modifiers: ['Control'], key: 'ArrowUp' },
114
+ appWindows: { label: 'App windows', hint: 'Control+Down', modifiers: ['Control'], key: 'ArrowDown' },
115
+ nextSpace: { label: 'Next Space', hint: 'Control+Right', modifiers: ['Control'], key: 'ArrowRight' },
116
+ previousSpace: { label: 'Previous Space', hint: 'Control+Left', modifiers: ['Control'], key: 'ArrowLeft' },
117
+ },
118
+ windows: {
119
+ overview: { label: 'Task View', hint: 'Win+Tab', modifiers: ['Meta'], key: 'Tab' },
120
+ nextSpace: { label: 'Next desktop', hint: 'Ctrl+Win+Right', modifiers: ['Control', 'Meta'], key: 'ArrowRight' },
121
+ previousSpace: { label: 'Previous desktop', hint: 'Ctrl+Win+Left', modifiers: ['Control', 'Meta'], key: 'ArrowLeft' },
122
+ },
123
+ };
94
124
 
95
125
  // Watching counts as using the session: while the viewer is visible a light
96
126
  // ping keeps the two-minute idle limit from ending it.
@@ -134,6 +164,7 @@ function releaseButton() {
134
164
  }
135
165
  function beginHold(button) {
136
166
  if (heldButton) return;
167
+ overviewOpen = null; // This press may close it.
137
168
  flushPendingMove();
138
169
  buttonReleaseRequested = false;
139
170
  const epoch = streamEpoch;
@@ -393,6 +424,7 @@ async function receiveFrame(recordBuffer, epoch) {
393
424
  additionalData: recordAad('frame') }, aesKey, record.subarray(8));
394
425
  if (!inputEnabled || epoch !== streamEpoch) return;
395
426
  frameSequence = sequence;
427
+ if (screenSwitch && Date.now() - screenSwitch.at >= SCREEN_SWITCH_SETTLE_MS) screenSwitch = null;
396
428
  const bytes = new Uint8Array(frame);
397
429
  if (bytes[0] !== 0xff || bytes[1] !== 0xd8) {
398
430
  try { receiveH264(bytes, epoch); }
@@ -428,6 +460,11 @@ function leave(message) {
428
460
  pendingWheel = null;
429
461
  stopEdgePan();
430
462
  pointers.clear();
463
+ displayInfo = null;
464
+ overviewOpen = null;
465
+ threeFinger = null;
466
+ screenSwitch = null;
467
+ renderDisplays();
431
468
  setHeldButton(null);
432
469
  for (const key of heldKeys.keys()) { heldKeys.delete(key); keyOperationIds.delete(key); updateKeyButton(key); }
433
470
  keyReleaseRequested.clear();
@@ -868,10 +905,12 @@ async function openStream(result, resume = null) {
868
905
  updateCursor();
869
906
  $('machineName').textContent = ticket.label;
870
907
  status('Remote desktop is live.');
908
+ void refreshDisplays(epoch);
871
909
  if (resume !== null) resumeAttempt = null;
872
910
  resumeTransportFailures = 0;
873
911
  heartbeat = setInterval(() => {
874
912
  sendCommand({ type: 'ping' });
913
+ void refreshDisplays(epoch); // Picks up a screen plugged in, unplugged, or a read that failed.
875
914
  }, 30_000);
876
915
  resolve();
877
916
  });
@@ -893,6 +932,8 @@ async function openStream(result, resume = null) {
893
932
 
894
933
  function send(action) {
895
934
  if (!inputEnabled) return Promise.resolve(false);
935
+ if (screenSwitch && Date.now() - screenSwitch.at >= SCREEN_SWITCH_LIMIT_MS) screenSwitch = null;
936
+ if (screenSwitch && ['click', 'buttonDown', 'move'].includes(action.kind)) return Promise.resolve(false);
896
937
  return sendCommand({ type: 'input', action });
897
938
  }
898
939
  function imageArea() {
@@ -981,9 +1022,127 @@ function edgePanTick() {
981
1022
  }
982
1023
  }
983
1024
  function clickCursor(button = 'left', count = 1) {
1025
+ overviewOpen = null; // A click may have closed it.
984
1026
  if (!heldButton) send(count > 1 ? { kind: 'click', ...cursor, button, count } : { kind: 'click', ...cursor, button });
985
1027
  }
986
1028
  function heldModifiers() { return modifierKeys.filter(key => heldKeys.has(key)); }
1029
+ /** Resolves to the new list, or null when this read told us nothing new. */
1030
+ async function refreshDisplays(epoch = streamEpoch) {
1031
+ let info;
1032
+ try { info = await api(`/desktop/requests/${encodeURIComponent(requestId)}/displays`); }
1033
+ catch { info = { supported: false, platform: null, displays: [], currentDisplayId: null, canShowAll: false }; }
1034
+ if (epoch !== streamEpoch) return null;
1035
+ // One failed read must not take working controls away; the next one retries.
1036
+ if (displayInfo?.supported && !info.supported) return null;
1037
+ displayInfo = info;
1038
+ renderDisplays();
1039
+ return info;
1040
+ }
1041
+ function displayChoices() {
1042
+ const info = displayInfo;
1043
+ if (!info?.supported) return [];
1044
+ const rows = info.displays.map(display => ({ id: display.id,
1045
+ label: `${display.name} · ${display.width}×${display.height}${display.main ? ' · Main' : ''}` }));
1046
+ return info.canShowAll ? [{ id: null, label: 'All screens' }, ...rows] : rows;
1047
+ }
1048
+ function renderDisplays() {
1049
+ const menu = $('displayMenu');
1050
+ const button = $('displaysButton');
1051
+ if (!menu || !button) return;
1052
+ const info = displayInfo;
1053
+ const choices = displayChoices();
1054
+ button.hidden = !info?.supported || info.displays.length < 2;
1055
+ if (button.hidden) { menu.hidden = true; button.setAttribute('aria-expanded', 'false'); }
1056
+ menu.replaceChildren(...choices.map(choice => {
1057
+ const item = document.createElement('button');
1058
+ item.type = 'button';
1059
+ item.textContent = choice.label; // Host text: never markup.
1060
+ item.setAttribute('aria-pressed', String(choice.id === info.currentDisplayId));
1061
+ item.addEventListener('click', () => { void chooseDisplay(choice.id); });
1062
+ return item;
1063
+ }));
1064
+ const shortcuts = DESKTOP_SHORTCUTS[info?.platform] ?? null;
1065
+ for (const node of document.querySelectorAll('[data-shortcut]')) {
1066
+ const action = shortcuts?.[node.dataset.shortcut];
1067
+ node.hidden = !action;
1068
+ if (action) node.textContent = action.label;
1069
+ }
1070
+ const note = $('displayNote');
1071
+ if (note) {
1072
+ note.hidden = !info || info.supported;
1073
+ if (!note.hidden) note.textContent = 'This machine needs an update before you can switch screens.';
1074
+ }
1075
+ }
1076
+ async function chooseDisplay(displayId) {
1077
+ if (!displayInfo?.supported || !inputEnabled) return;
1078
+ const epoch = streamEpoch;
1079
+ const chosen = displayChoices().find(choice => choice.id === displayId);
1080
+ $('displayMenu').hidden = true;
1081
+ $('displaysButton').setAttribute('aria-expanded', 'false');
1082
+ overviewOpen = null;
1083
+ flushPendingMove();
1084
+ resetZoom();
1085
+ releaseButton();
1086
+ if (!await send({ kind: 'selectDisplay', displayId })) return;
1087
+ // macOS applies Spaces to the screen under the pointer, so the gestures
1088
+ // follow the picture only if the pointer does.
1089
+ cursor = { x: 0.5, y: 0.5 };
1090
+ void send({ kind: 'move', ...cursor });
1091
+ screenSwitch = { at: Date.now() };
1092
+ updateCursor();
1093
+ status(`Showing ${chosen?.label.split(' · ')[0] ?? 'the chosen screen'}.`);
1094
+ await new Promise(resolve => setTimeout(resolve, 1_200));
1095
+ const info = await refreshDisplays(epoch);
1096
+ if (info?.supported && info.currentDisplayId !== displayId) {
1097
+ status('That screen is not available any more.');
1098
+ }
1099
+ }
1100
+ async function sendChord(modifiers, key) {
1101
+ const epoch = streamEpoch;
1102
+ const pressed = [];
1103
+ let allDown = true;
1104
+ for (const modifier of modifiers) {
1105
+ if (heldKeys.has(modifier)) continue; // A key the operator latched stays latched.
1106
+ if (!await beginKey(modifier)) { allDown = false; break; }
1107
+ pressed.push(modifier);
1108
+ }
1109
+ if (allDown && epoch === streamEpoch) await tapKey(key);
1110
+ for (const modifier of pressed.reverse()) if (epoch === streamEpoch) await releaseKey(modifier);
1111
+ }
1112
+ function runShortcut(name) {
1113
+ const shortcuts = DESKTOP_SHORTCUTS[displayInfo?.platform];
1114
+ const action = shortcuts?.[name];
1115
+ if (!action || !inputEnabled) return;
1116
+ // Named beside the action: this Mac may have the shortcut turned off, and
1117
+ // the viewer cannot see that.
1118
+ status(`${action.label} (${action.hint})`);
1119
+ overviewOpen = name === 'overview' || name === 'appWindows' ? name : null;
1120
+ void sendChord(action.modifiers, action.key);
1121
+ }
1122
+ function closeOverview() {
1123
+ overviewOpen = null;
1124
+ status('Back');
1125
+ void tapKey('Escape');
1126
+ }
1127
+ function swipeThreeFingers(direction) {
1128
+ if (direction === 'left') runShortcut('nextSpace');
1129
+ else if (direction === 'right') runShortcut('previousSpace');
1130
+ else if (direction === 'up') {
1131
+ if (overviewOpen === 'appWindows') closeOverview();
1132
+ else if (!overviewOpen) runShortcut('overview');
1133
+ } else if (overviewOpen) closeOverview();
1134
+ else runShortcut('appWindows');
1135
+ }
1136
+ function moveThreeFingers() {
1137
+ if (!threeFinger || threeFinger.fired || pointers.size < 3 || heldButton) return;
1138
+ const fingers = [...pointers.values()];
1139
+ const dx = fingers.reduce((sum, state) => sum + state.x - state.startX, 0) / fingers.length;
1140
+ const dy = fingers.reduce((sum, state) => sum + state.y - state.startY, 0) / fingers.length;
1141
+ const major = Math.max(Math.abs(dx), Math.abs(dy));
1142
+ if (major < THREE_FINGER_TRAVEL || major < 1.5 * Math.min(Math.abs(dx), Math.abs(dy))) return;
1143
+ threeFinger.fired = true; // One swipe, one action.
1144
+ swipeThreeFingers(Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? 'left' : 'right') : (dy < 0 ? 'up' : 'down'));
1145
+ }
987
1146
  function syncTouchModeButton() {
988
1147
  $('modeButton').hidden = !touchCapable;
989
1148
  $('modeButton').textContent = trackpad ? 'Trackpad · swipe to move' : 'Direct · touch position';
@@ -1076,6 +1235,18 @@ $('keyboardButton').addEventListener('click', () => {
1076
1235
  syncVisualViewport();
1077
1236
  });
1078
1237
  $('hideKeyboardButton').addEventListener('click', closeKeyboard);
1238
+ $('displaysButton')?.addEventListener('click', () => {
1239
+ const open = $('displayMenu').hidden;
1240
+ $('displayMenu').hidden = !open;
1241
+ $('displaysButton').setAttribute('aria-expanded', String(open));
1242
+ if (open) void refreshDisplays(); // A screen may have been plugged in since.
1243
+ });
1244
+ for (const button of document.querySelectorAll('[data-shortcut]')) {
1245
+ button.addEventListener('click', () => {
1246
+ if (overviewOpen && button.dataset.shortcut === overviewOpen) closeOverview();
1247
+ else runShortcut(button.dataset.shortcut);
1248
+ });
1249
+ }
1079
1250
  $('zoomResetButton').addEventListener('click', resetZoom);
1080
1251
  $('fullscreenButton').hidden = !document.fullscreenEnabled || !$('control').requestFullscreen;
1081
1252
  $('fullscreenButton').addEventListener('click', () => {
@@ -1159,8 +1330,10 @@ $('surface').addEventListener('pointerdown', event => {
1159
1330
  multiTap = null;
1160
1331
  const state = pointers.get(event.pointerId);
1161
1332
  const now = Date.now();
1162
- state.secondTap = !!lastTap && now - lastTap.at < DOUBLE_TAP_MS
1333
+ state.secondTap = !!pendingTapClick && !!lastTap && now - lastTap.at < DOUBLE_TAP_MS
1163
1334
  && Math.hypot(event.clientX - lastTap.x, event.clientY - lastTap.y) < 40;
1335
+ if (state.secondTap) { clearTimeout(pendingTapClick); pendingTapClick = 0; }
1336
+ else flushTapClick();
1164
1337
  // Hold still to right-click, as on AnyDesk and most phone remote desktops.
1165
1338
  state.longPress = setTimeout(() => {
1166
1339
  if (pointers.get(event.pointerId) !== state || state.maxDistance >= 10 || state.multi || state.secondTap) return;
@@ -1175,6 +1348,12 @@ $('surface').addEventListener('pointerdown', event => {
1175
1348
  const pair = pairMetrics();
1176
1349
  twoFinger = { ...pair, zoom, panX, panY, lastX: pair.x, lastY: pair.y, mode: 'pending' };
1177
1350
  }
1351
+ if (pointers.size === 3 && event.pointerType !== 'mouse') {
1352
+ stopEdgePan();
1353
+ twoFinger = null;
1354
+ for (const state of pointers.values()) { state.startX = state.x; state.startY = state.y; }
1355
+ threeFinger = { fired: false };
1356
+ }
1178
1357
  if (!trackpad || event.pointerType === 'mouse') {
1179
1358
  const at = position(event);
1180
1359
  if (at) { cursor = at; updateCursor(); queueMove(at); }
@@ -1211,7 +1390,9 @@ $('surface').addEventListener('pointermove', event => {
1211
1390
  state.x = event.clientX; state.y = event.clientY;
1212
1391
  state.maxDistance = Math.max(state.maxDistance,
1213
1392
  Math.hypot(event.clientX - state.startX, event.clientY - state.startY));
1214
- if (pointers.size > 1 && event.pointerType !== 'mouse') {
1393
+ if (threeFinger && event.pointerType !== 'mouse') {
1394
+ moveThreeFingers();
1395
+ } else if (pointers.size > 1 && event.pointerType !== 'mouse') {
1215
1396
  moveTwoFingers();
1216
1397
  } else if (state.multi && event.pointerType !== 'mouse') {
1217
1398
  // Do not turn the trailing finger of a scroll gesture into pointer motion.
@@ -1233,6 +1414,7 @@ $('surface').addEventListener('pointerup', event => {
1233
1414
  clearTimeout(state.longPress);
1234
1415
  pointers.delete(event.pointerId);
1235
1416
  if (pointers.size < 2) twoFinger = null;
1417
+ if (pointers.size === 0) threeFinger = null;
1236
1418
  if (event.pointerType === 'mouse' && mousePress?.pointerId === event.pointerId) {
1237
1419
  const press = mousePress; mousePress = null;
1238
1420
  const at = position(event);
@@ -1268,17 +1450,34 @@ $('surface').addEventListener('pointerup', event => {
1268
1450
  }
1269
1451
  const distance = Math.max(state.maxDistance,
1270
1452
  Math.hypot(event.clientX - state.startX, event.clientY - state.startY));
1271
- if (!state.multi && distance < 10 && Date.now() - state.at < 500) {
1453
+ if (!state.multi && distance < 10 && state.secondTap) {
1454
+ // The first tap's click is still unsent: a real double tap goes out as one
1455
+ // double-click, and a second press held without moving as one click.
1456
+ if (!trackpad) { const at = position(event); if (at) cursor = at; }
1457
+ flushPendingMove();
1458
+ clickCursor('left', Date.now() - state.at < 500 ? 2 : 1);
1459
+ lastTap = null;
1460
+ } else if (!state.multi && distance < 10 && Date.now() - state.at < 500) {
1272
1461
  if (!trackpad || event.pointerType === 'mouse') { const at = position(event); if (at) cursor = at; }
1273
- clickCursor(event.button === 2 ? 'right' : 'left');
1274
- lastTap = event.pointerType === 'mouse' ? null : { at: Date.now(), x: event.clientX, y: event.clientY };
1462
+ if (event.pointerType === 'mouse') { clickCursor(event.button === 2 ? 'right' : 'left'); lastTap = null; }
1463
+ else {
1464
+ lastTap = { at: Date.now(), x: event.clientX, y: event.clientY };
1465
+ pendingTapClick = setTimeout(flushTapClick, DOUBLE_TAP_MS);
1466
+ }
1275
1467
  }
1276
1468
  });
1277
1469
  function twoFingerUsed(tap) { return tap.zoomed === true; }
1470
+ function flushTapClick() {
1471
+ if (!pendingTapClick) return;
1472
+ clearTimeout(pendingTapClick);
1473
+ pendingTapClick = 0;
1474
+ clickCursor('left');
1475
+ }
1278
1476
  function cancelGestures() {
1279
1477
  stopEdgePan();
1478
+ clearTimeout(pendingTapClick); pendingTapClick = 0;
1280
1479
  for (const state of pointers.values()) clearTimeout(state.longPress);
1281
- pointers.clear(); twoFinger = null; multiTap = null; mousePress = null; dragPointerId = null; lastTap = null;
1480
+ pointers.clear(); twoFinger = null; threeFinger = null; multiTap = null; mousePress = null; dragPointerId = null; lastTap = null;
1282
1481
  releaseControls();
1283
1482
  }
1284
1483
  $('surface').addEventListener('pointercancel', cancelGestures);
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
@@ -8658,6 +8658,9 @@ export async function startServer(options) {
8658
8658
  }
8659
8659
  catch { /* @silent-fallback-ok — the stop record is observability */ }
8660
8660
  },
8661
+ mediaMaxStoreBytes: () => config.mobilePairing?.native?.media?.maxStoreBytes,
8662
+ raiseAttention: (item) => { void telegram?.createAttentionItem({ id: item.conditionKey, conditionKey: item.conditionKey, title: item.title, summary: item.body, category: 'general', priority: 'NORMAL', sourceContext: 'app-media' }); },
8663
+ clearAttention: (conditionKey) => { telegram?.clearAttentionCondition({ conditionKey }, 'cleared: the app media store has room again'); },
8661
8664
  reportDegradation: (what, detail) => degradationReporter.report({
8662
8665
  feature: what, primary: 'record and deliver native app conversations', fallback: 'the request was refused or reported outcome-unknown',
8663
8666
  reason: detail, impact: 'An app conversation step needs attention.',