@oar-sh/oar 0.9.0

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 (293) hide show
  1. package/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
@@ -0,0 +1,101 @@
1
+ import { autoResize, isMobileComposerViewport } from './store.js';
2
+
3
+ const LAST_EMOJI_STORAGE_KEY = 'copilot_last_emoji';
4
+ const DEFAULT_EMOJI = '🙂';
5
+ const EMOJI_CHOICES = ['🙂', '😀', '😁', '😂', '😊', '😍', '😘', '😎', '🤔', '🙌', '👍', '👏', '🎉', '❤️', '🔥', '✅', '👀', '🤝', '🙏', '💡', '🚀', '✨', '🧠', '🛠️'];
6
+
7
+ let pickerOpen = false;
8
+
9
+ function getLastEmoji() {
10
+ const saved = String(localStorage.getItem(LAST_EMOJI_STORAGE_KEY) || '').trim();
11
+ return saved || DEFAULT_EMOJI;
12
+ }
13
+
14
+ function setLastEmoji(emoji) {
15
+ const value = String(emoji || '').trim();
16
+ if (!value) return;
17
+ localStorage.setItem(LAST_EMOJI_STORAGE_KEY, value);
18
+ }
19
+
20
+ function updateEmojiButton() {
21
+ const button = document.getElementById('emoji-btn');
22
+ if (!button) return;
23
+ button.textContent = getLastEmoji();
24
+ }
25
+
26
+ function closeEmojiPicker() {
27
+ const popup = document.getElementById('emoji-popup');
28
+ if (!popup) return;
29
+ popup.classList.remove('visible');
30
+ popup.setAttribute('aria-hidden', 'true');
31
+ pickerOpen = false;
32
+ }
33
+
34
+ function insertEmojiAtCaret(emoji) {
35
+ const input = document.getElementById('msg-input');
36
+ if (!input) return;
37
+ const value = String(input.value || '');
38
+ const start = Number.isFinite(input.selectionStart) ? input.selectionStart : value.length;
39
+ const end = Number.isFinite(input.selectionEnd) ? input.selectionEnd : value.length;
40
+ input.value = `${value.slice(0, start)}${emoji}${value.slice(end)}`;
41
+ const nextCaret = start + emoji.length;
42
+ input.selectionStart = nextCaret;
43
+ input.selectionEnd = nextCaret;
44
+ autoResize(input);
45
+ input.focus();
46
+ }
47
+
48
+ function handleEmojiChoice(emoji) {
49
+ const value = String(emoji || '').trim();
50
+ if (!value) return;
51
+ setLastEmoji(value);
52
+ updateEmojiButton();
53
+ insertEmojiAtCaret(value);
54
+ closeEmojiPicker();
55
+ }
56
+
57
+ function ensureEmojiChoices() {
58
+ const popup = document.getElementById('emoji-popup');
59
+ if (!popup || popup.dataset.bound === '1') return;
60
+ popup.dataset.bound = '1';
61
+ popup.innerHTML = EMOJI_CHOICES
62
+ .map((emoji) => `<button class="emoji-choice" type="button" data-emoji="${emoji}" aria-label="Insert ${emoji}">${emoji}</button>`)
63
+ .join('');
64
+ popup.addEventListener('click', (event) => {
65
+ const target = event.target?.closest?.('.emoji-choice');
66
+ if (!target) return;
67
+ handleEmojiChoice(target.getAttribute('data-emoji'));
68
+ });
69
+ }
70
+
71
+ export function toggleEmojiPicker() {
72
+ if (isMobileComposerViewport()) return;
73
+ const popup = document.getElementById('emoji-popup');
74
+ if (!popup) return;
75
+ if (pickerOpen) {
76
+ closeEmojiPicker();
77
+ return;
78
+ }
79
+ popup.classList.add('visible');
80
+ popup.setAttribute('aria-hidden', 'false');
81
+ pickerOpen = true;
82
+ }
83
+
84
+ export function initEmojiPicker() {
85
+ ensureEmojiChoices();
86
+ updateEmojiButton();
87
+ if (document.body.dataset.emojiPickerBound === '1') return;
88
+ document.body.dataset.emojiPickerBound = '1';
89
+ document.addEventListener('keydown', (event) => {
90
+ if (event.key === 'Escape') closeEmojiPicker();
91
+ });
92
+ document.addEventListener('pointerdown', (event) => {
93
+ if (!pickerOpen) return;
94
+ const popup = document.getElementById('emoji-popup');
95
+ const button = document.getElementById('emoji-btn');
96
+ const target = event.target;
97
+ if (popup?.contains(target) || button?.contains(target)) return;
98
+ closeEmojiPicker();
99
+ });
100
+ }
101
+
@@ -0,0 +1,80 @@
1
+ const EXTERNAL_LINK_PROTOCOLS = new Set(['http:', 'https:']);
2
+
3
+ export function isExternalNavigationHref(rawHref, baseHref) {
4
+ const href = String(rawHref || '').trim();
5
+ if (!href || href.startsWith('#')) return false;
6
+ try {
7
+ const url = new URL(href, baseHref);
8
+ return EXTERNAL_LINK_PROTOCOLS.has(url.protocol);
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ function isInAppLink(anchor) {
15
+ return anchor.matches('a.workspace-file-link[data-workspace-path]')
16
+ || Boolean(anchor.closest('.file-preview-markdown'));
17
+ }
18
+
19
+ export function openExternalNavigation(url, onFallback = null) {
20
+ const opened = window.open(url, '_blank');
21
+ if (opened) opened.opener = null;
22
+ if (!opened && typeof onFallback === 'function') onFallback(url);
23
+ return !!opened;
24
+ }
25
+
26
+ function secureExternalLink(anchor, baseHref, onFallback) {
27
+ if (
28
+ !(anchor instanceof HTMLAnchorElement)
29
+ || anchor.hasAttribute('download')
30
+ || isInAppLink(anchor)
31
+ || !isExternalNavigationHref(anchor.getAttribute('href'), baseHref)
32
+ ) {
33
+ return;
34
+ }
35
+
36
+ if (anchor.target !== '_blank') anchor.target = '_blank';
37
+ const relTokens = new Set(String(anchor.rel || '').split(/\s+/).filter(Boolean));
38
+ relTokens.add('noopener');
39
+ relTokens.add('noreferrer');
40
+ anchor.rel = Array.from(relTokens).join(' ');
41
+ }
42
+
43
+ export function installExternalLinkPolicy({ documentRef = document, onFallback = null } = {}) {
44
+ if (!documentRef || documentRef.documentElement?.dataset.externalLinkPolicy === 'installed') return;
45
+ documentRef.documentElement.dataset.externalLinkPolicy = 'installed';
46
+
47
+ const secureLinksIn = (root) => {
48
+ if (root instanceof HTMLAnchorElement) {
49
+ secureExternalLink(root, documentRef.baseURI, onFallback);
50
+ return;
51
+ }
52
+ if (!(root instanceof Element)) return;
53
+ root.querySelectorAll('a[href]').forEach((anchor) => secureExternalLink(anchor, documentRef.baseURI, onFallback));
54
+ };
55
+
56
+ secureLinksIn(documentRef.documentElement);
57
+ documentRef.addEventListener('click', (event) => {
58
+ const target = event.target instanceof Element ? event.target : event.target?.parentElement;
59
+ const anchor = target?.closest?.('a[href]');
60
+ if (!anchor || isInAppLink(anchor)) return;
61
+ secureExternalLink(anchor, documentRef.baseURI, onFallback);
62
+ if (!isExternalNavigationHref(anchor.getAttribute('href'), documentRef.baseURI)) return;
63
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
64
+ event.preventDefault();
65
+ openExternalNavigation(anchor.href, onFallback);
66
+ }, true);
67
+
68
+ const observer = new MutationObserver((records) => {
69
+ for (const record of records) {
70
+ if (record.type === 'attributes') secureLinksIn(record.target);
71
+ for (const node of record.addedNodes) secureLinksIn(node);
72
+ }
73
+ });
74
+ observer.observe(documentRef.documentElement, {
75
+ subtree: true,
76
+ childList: true,
77
+ attributes: true,
78
+ attributeFilter: ['class', 'download', 'href', 'target'],
79
+ });
80
+ }
@@ -0,0 +1,51 @@
1
+ const EXTERNAL_SCHEME = /^[a-z][a-z\d+.-]*:/i;
2
+
3
+ function decodeFragment(value) {
4
+ try {
5
+ return decodeURIComponent(value);
6
+ } catch {
7
+ return value;
8
+ }
9
+ }
10
+
11
+ function resolveRelativePath(currentPath, hrefPath) {
12
+ const baseParts = String(currentPath || '').replace(/\\/g, '/').split('/').filter(Boolean);
13
+ baseParts.pop();
14
+ const targetParts = String(hrefPath || '').replace(/\\/g, '/').split('/');
15
+ for (const part of targetParts) {
16
+ if (!part || part === '.') continue;
17
+ if (part === '..') {
18
+ if (!baseParts.length) return '';
19
+ baseParts.pop();
20
+ continue;
21
+ }
22
+ baseParts.push(part);
23
+ }
24
+ return baseParts.join('/');
25
+ }
26
+
27
+ export function resolveFilePreviewLink(rawHref, currentPath) {
28
+ const href = String(rawHref || '').trim();
29
+ if (!href) return { kind: 'none' };
30
+ if (href.startsWith('#')) return { kind: 'fragment', fragment: decodeFragment(href.slice(1)) };
31
+ if (href.startsWith('//') || EXTERNAL_SCHEME.test(href)) return { kind: 'external', href };
32
+
33
+ const hashIndex = href.indexOf('#');
34
+ const pathPart = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
35
+ const fragment = hashIndex >= 0 ? decodeFragment(href.slice(hashIndex + 1)) : '';
36
+ if (pathPart.startsWith('/')) return { kind: 'none' };
37
+
38
+ const path = resolveRelativePath(currentPath, pathPart);
39
+ return path ? { kind: 'file', path, fragment } : { kind: 'none' };
40
+ }
41
+
42
+ export function markdownHeadingId(text) {
43
+ return String(text || '')
44
+ .normalize('NFKD')
45
+ .replace(/[\u0300-\u036f]/g, '')
46
+ .toLowerCase()
47
+ .trim()
48
+ .replace(/[^\w\s-]/g, '')
49
+ .replace(/[\s-]+/g, '-')
50
+ .replace(/^-+|-+$/g, '');
51
+ }
@@ -0,0 +1,262 @@
1
+ const FONT_SCALE_STORAGE_KEY = 'copilot_font_scale';
2
+ const FONT_SCALE_MIN = 0.5;
3
+ const FONT_SCALE_MAX = 1.5;
4
+ const FONT_SCALE_DEFAULT = 1;
5
+ const FONT_SCALE_WHEEL_STEP_MIN = 0.01;
6
+ const FONT_SCALE_WHEEL_STEP_MAX = 0.2;
7
+ const FONT_SCALE_PILL_VISIBLE_MS = 900;
8
+
9
+ let fontScaleValue = FONT_SCALE_DEFAULT;
10
+ let fontScaleIndicatorTimer = null;
11
+ let fontScalePinchState = {
12
+ active: false,
13
+ startDistance: 0,
14
+ startScale: FONT_SCALE_DEFAULT,
15
+ };
16
+
17
+ function readLocalStorage(key) {
18
+ try {
19
+ return localStorage.getItem(key);
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function writeLocalStorage(key, value) {
26
+ try {
27
+ localStorage.setItem(key, value);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function clampFontScale(value) {
35
+ if (value == null) return FONT_SCALE_DEFAULT;
36
+ const text = typeof value === 'string' ? value.trim() : value;
37
+ if (text === '') return FONT_SCALE_DEFAULT;
38
+ const numeric = Number(text);
39
+ if (!Number.isFinite(numeric)) return FONT_SCALE_DEFAULT;
40
+ return Math.min(FONT_SCALE_MAX, Math.max(FONT_SCALE_MIN, numeric));
41
+ }
42
+
43
+ function readStoredFontScale() {
44
+ return clampFontScale(readLocalStorage(FONT_SCALE_STORAGE_KEY));
45
+ }
46
+
47
+ function normalizeFontScaleSelectValue(raw = '') {
48
+ const text = String(raw || '').trim();
49
+ if (!text) return null;
50
+ const numeric = Number(text);
51
+ if (!Number.isFinite(numeric)) return null;
52
+ if (numeric > 3) return clampFontScale(numeric / 100);
53
+ return clampFontScale(numeric);
54
+ }
55
+
56
+ function getMessageViewportAnchor() {
57
+ const container = document.getElementById('messages');
58
+ if (!container) return null;
59
+ const containerRect = container.getBoundingClientRect();
60
+ const messages = Array.from(container.querySelectorAll('.msg[data-message-id]'));
61
+ for (const message of messages) {
62
+ const rect = message.getBoundingClientRect();
63
+ if (rect.bottom <= containerRect.top || rect.top >= containerRect.bottom) continue;
64
+ const messageId = String(message.dataset.messageId || '').trim();
65
+ if (!messageId) continue;
66
+ return {
67
+ messageId,
68
+ offsetTop: rect.top - containerRect.top,
69
+ };
70
+ }
71
+ return null;
72
+ }
73
+
74
+ function restoreMessageViewportAnchor(anchor) {
75
+ if (!anchor?.messageId) return;
76
+ const container = document.getElementById('messages');
77
+ if (!container) return;
78
+ const messageId = String(anchor.messageId || '').trim();
79
+ if (!messageId) return;
80
+ const message = Array.from(container.querySelectorAll('.msg[data-message-id]'))
81
+ .find((node) => String(node?.dataset?.messageId || '').trim() === messageId);
82
+ if (!message) return;
83
+ const containerRect = container.getBoundingClientRect();
84
+ const rect = message.getBoundingClientRect();
85
+ const delta = rect.top - containerRect.top - Number(anchor.offsetTop || 0);
86
+ if (Math.abs(delta) > 0.5) {
87
+ container.scrollTop += delta;
88
+ }
89
+ }
90
+
91
+ export function syncFontScaleSelect() {
92
+ const select = document.getElementById('font-scale-select');
93
+ if (!select) return;
94
+ const currentPercent = Math.round(clampFontScale(fontScaleValue) * 100);
95
+ const candidate = String(currentPercent);
96
+ if (Array.from(select.options).some((option) => option.value === candidate)) {
97
+ select.value = candidate;
98
+ return;
99
+ }
100
+ const dynamicValue = String(currentPercent);
101
+ const dynamicLabel = `${currentPercent}%`;
102
+ let dynamicOption = select.querySelector('option[data-dynamic-font-scale="1"]');
103
+ if (!dynamicOption) {
104
+ dynamicOption = document.createElement('option');
105
+ dynamicOption.setAttribute('data-dynamic-font-scale', '1');
106
+ select.appendChild(dynamicOption);
107
+ }
108
+ dynamicOption.value = dynamicValue;
109
+ dynamicOption.textContent = dynamicLabel;
110
+ select.value = dynamicValue;
111
+ }
112
+
113
+ export function setFontScale(nextScale, { persist = true, preserveMessageAnchor = true } = {}) {
114
+ const normalized = clampFontScale(nextScale);
115
+ if (Math.abs(normalized - fontScaleValue) <= 0.0001) {
116
+ if (persist) writeLocalStorage(FONT_SCALE_STORAGE_KEY, String(normalized));
117
+ syncFontScaleSelect();
118
+ return normalized;
119
+ }
120
+ const anchor = preserveMessageAnchor ? getMessageViewportAnchor() : null;
121
+ fontScaleValue = normalized;
122
+ document.documentElement.style.setProperty('--font-scale', normalized.toFixed(4));
123
+ document.documentElement.style.setProperty('--font-scale-percent', `${Math.round(normalized * 100)}%`);
124
+ if (persist) writeLocalStorage(FONT_SCALE_STORAGE_KEY, String(normalized));
125
+ syncFontScaleSelect();
126
+ if (anchor) {
127
+ requestAnimationFrame(() => {
128
+ restoreMessageViewportAnchor(anchor);
129
+ });
130
+ }
131
+ return normalized;
132
+ }
133
+
134
+ function showFontScaleIndicator(scaleValue) {
135
+ const indicator = document.getElementById('font-scale-indicator');
136
+ if (!indicator) return;
137
+ const normalized = clampFontScale(scaleValue);
138
+ indicator.textContent = `${Math.round(normalized * 100)}%`;
139
+ indicator.hidden = false;
140
+ indicator.classList.add('visible');
141
+ if (fontScaleIndicatorTimer) clearTimeout(fontScaleIndicatorTimer);
142
+ fontScaleIndicatorTimer = setTimeout(() => {
143
+ indicator.classList.remove('visible');
144
+ window.setTimeout(() => {
145
+ if (!indicator.classList.contains('visible')) indicator.hidden = true;
146
+ }, 180);
147
+ fontScaleIndicatorTimer = null;
148
+ }, FONT_SCALE_PILL_VISIBLE_MS);
149
+ }
150
+
151
+ function isImageZoomGestureTarget(target) {
152
+ if (!(target instanceof Element)) return false;
153
+ return !!target.closest('#file-preview-body.image-zoom-mode');
154
+ }
155
+
156
+ function pinchDistance(touchA, touchB) {
157
+ return Math.hypot(touchA.clientX - touchB.clientX, touchA.clientY - touchB.clientY);
158
+ }
159
+
160
+ function normalizeWheelDeltaPixels(event) {
161
+ const deltaY = Number(event?.deltaY);
162
+ if (!Number.isFinite(deltaY) || deltaY === 0) return 0;
163
+ const deltaMode = Number(event?.deltaMode || 0);
164
+ if (deltaMode === 1) return deltaY * 16; // lines -> approx pixels
165
+ if (deltaMode === 2) return deltaY * (window.innerHeight || 800); // pages -> pixels
166
+ return deltaY; // already pixels
167
+ }
168
+
169
+ function onGlobalFontScaleWheel(event) {
170
+ if (!(event.ctrlKey || event.metaKey)) return;
171
+ if (isImageZoomGestureTarget(event.target)) return;
172
+ event.preventDefault();
173
+ const deltaPixels = normalizeWheelDeltaPixels(event);
174
+ if (!Number.isFinite(deltaPixels) || deltaPixels === 0) return;
175
+ const direction = deltaPixels < 0 ? 1 : -1;
176
+ // Slow wheel gestures move by 1%, while faster spins accelerate.
177
+ const magnitude = Math.min(
178
+ FONT_SCALE_WHEEL_STEP_MAX,
179
+ Math.max(FONT_SCALE_WHEEL_STEP_MIN, Math.abs(deltaPixels) / 10_000),
180
+ );
181
+ const nextScale = setFontScale(fontScaleValue + (direction * magnitude), { persist: true, preserveMessageAnchor: true });
182
+ showFontScaleIndicator(nextScale);
183
+ }
184
+
185
+ function onGlobalFontScaleTouchStart(event) {
186
+ if (event.touches.length !== 2) return;
187
+ if (isImageZoomGestureTarget(event.target)) {
188
+ fontScalePinchState.active = false;
189
+ return;
190
+ }
191
+ const t0 = event.touches[0];
192
+ const t1 = event.touches[1];
193
+ fontScalePinchState = {
194
+ active: true,
195
+ startDistance: pinchDistance(t0, t1),
196
+ startScale: fontScaleValue,
197
+ };
198
+ event.preventDefault();
199
+ }
200
+
201
+ function onGlobalFontScaleTouchMove(event) {
202
+ if (!fontScalePinchState.active) return;
203
+ if (event.touches.length !== 2) {
204
+ fontScalePinchState.active = false;
205
+ return;
206
+ }
207
+ if (isImageZoomGestureTarget(event.target)) {
208
+ fontScalePinchState.active = false;
209
+ return;
210
+ }
211
+ event.preventDefault();
212
+ const t0 = event.touches[0];
213
+ const t1 = event.touches[1];
214
+ const distance = pinchDistance(t0, t1);
215
+ if (!fontScalePinchState.startDistance || !Number.isFinite(distance) || distance <= 0) return;
216
+ const ratio = distance / fontScalePinchState.startDistance;
217
+ const nextScale = fontScalePinchState.startScale * ratio;
218
+ const appliedScale = setFontScale(nextScale, { persist: true, preserveMessageAnchor: true });
219
+ showFontScaleIndicator(appliedScale);
220
+ }
221
+
222
+ function onGlobalFontScaleTouchEnd(event) {
223
+ if (event.touches.length < 2) {
224
+ fontScalePinchState.active = false;
225
+ }
226
+ }
227
+
228
+ function populateFontScaleSelect() {
229
+ const select = document.getElementById('font-scale-select');
230
+ if (!select || select.dataset.populated === '1') return;
231
+ select.dataset.populated = '1';
232
+ for (let value = 50; value <= 150; value += 10) {
233
+ const option = document.createElement('option');
234
+ option.value = String(value);
235
+ option.textContent = `${value}%`;
236
+ select.appendChild(option);
237
+ }
238
+ }
239
+
240
+ export function initFontScaling() {
241
+ populateFontScaleSelect();
242
+ const inlineScale = Number(document.documentElement.style.getPropertyValue('--font-scale'));
243
+ const initialScale = Number.isFinite(inlineScale) ? clampFontScale(inlineScale) : readStoredFontScale();
244
+ setFontScale(initialScale, { persist: false, preserveMessageAnchor: false });
245
+ if (!window.__fontScaleGestureHandlersBound) {
246
+ window.__fontScaleGestureHandlersBound = true;
247
+ window.addEventListener('wheel', onGlobalFontScaleWheel, { passive: false, capture: true });
248
+ window.addEventListener('touchstart', onGlobalFontScaleTouchStart, { passive: false, capture: true });
249
+ window.addEventListener('touchmove', onGlobalFontScaleTouchMove, { passive: false, capture: true });
250
+ window.addEventListener('touchend', onGlobalFontScaleTouchEnd, { passive: true, capture: true });
251
+ window.addEventListener('touchcancel', onGlobalFontScaleTouchEnd, { passive: true, capture: true });
252
+ }
253
+ }
254
+
255
+ export function updateFontScaleFromSelect(rawValue) {
256
+ const next = normalizeFontScaleSelectValue(rawValue);
257
+ if (next == null) {
258
+ syncFontScaleSelect();
259
+ return;
260
+ }
261
+ setFontScale(next, { persist: true, preserveMessageAnchor: true });
262
+ }