@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,339 @@
1
+ import { escHtml, currentConvId } from './store.js';
2
+ import { loadGitStatus, loadGitDiff, requestGitPull } from './api-client.js';
3
+ import {
4
+ parseUnifiedDiff,
5
+ collapseContextLines,
6
+ describeGitFileStatus,
7
+ summarizeGitStatusHeader,
8
+ } from './git-diff-model.mjs';
9
+
10
+ const CHANGES_MODE_CONTEXT_LINES = 3;
11
+
12
+ const gitState = {
13
+ loading: false,
14
+ error: '',
15
+ status: null,
16
+ notice: '',
17
+ pulling: false,
18
+ requestSeq: 0,
19
+ diff: {
20
+ path: '',
21
+ status: '',
22
+ untracked: false,
23
+ loading: false,
24
+ error: '',
25
+ parsed: null,
26
+ mode: 'changes',
27
+ requestSeq: 0,
28
+ },
29
+ };
30
+
31
+ function getGitElements() {
32
+ return {
33
+ modal: document.getElementById('git-changes-modal'),
34
+ branch: document.getElementById('git-changes-branch'),
35
+ meta: document.getElementById('git-changes-meta'),
36
+ notice: document.getElementById('git-changes-notice'),
37
+ list: document.getElementById('git-changes-list'),
38
+ pullBtn: document.getElementById('git-changes-pull'),
39
+ refreshBtn: document.getElementById('git-changes-refresh'),
40
+ closeBtn: document.getElementById('git-changes-close'),
41
+ diffModal: document.getElementById('git-diff-modal'),
42
+ diffTitle: document.getElementById('git-diff-title'),
43
+ diffMeta: document.getElementById('git-diff-meta'),
44
+ diffBody: document.getElementById('git-diff-body'),
45
+ diffModeFull: document.getElementById('git-diff-mode-full'),
46
+ diffModeChanges: document.getElementById('git-diff-mode-changes'),
47
+ diffCloseBtn: document.getElementById('git-diff-close'),
48
+ };
49
+ }
50
+
51
+ function renderGitChanges() {
52
+ const { branch, meta, notice, list, pullBtn } = getGitElements();
53
+ if (!list) return;
54
+
55
+ const status = gitState.status;
56
+ if (branch) {
57
+ if (gitState.loading && !status) {
58
+ branch.textContent = 'Loading…';
59
+ } else if (status && status.isRepo === false) {
60
+ branch.textContent = 'Not a git repository';
61
+ } else {
62
+ branch.textContent = summarizeGitStatusHeader(status) || '(no branch)';
63
+ }
64
+ }
65
+ if (meta) {
66
+ const bits = [];
67
+ const upstream = String(status?.upstream || '').trim();
68
+ if (upstream) bits.push(upstream);
69
+ if (status?.isRepo !== false) {
70
+ const count = Array.isArray(status?.files) ? status.files.length : 0;
71
+ bits.push(`${count} changed file${count === 1 ? '' : 's'}`);
72
+ }
73
+ meta.textContent = bits.join(' · ');
74
+ }
75
+ if (notice) {
76
+ notice.textContent = gitState.error || gitState.notice || '';
77
+ notice.classList.toggle('error', !!gitState.error);
78
+ notice.hidden = !gitState.error && !gitState.notice;
79
+ }
80
+ if (pullBtn) {
81
+ pullBtn.disabled = gitState.pulling || gitState.loading || !status || status.isRepo === false;
82
+ pullBtn.textContent = gitState.pulling ? 'Pulling…' : '⬇ Pull';
83
+ }
84
+
85
+ if (gitState.loading && !status) {
86
+ list.innerHTML = '<div class="git-changes-empty">Loading changes…</div>';
87
+ return;
88
+ }
89
+ if (status && status.isRepo === false) {
90
+ list.innerHTML = '<div class="git-changes-empty">The current workspace root is not a git repository.</div>';
91
+ return;
92
+ }
93
+ const files = Array.isArray(status?.files) ? status.files : [];
94
+ if (!files.length) {
95
+ list.innerHTML = `<div class="git-changes-empty">${escHtml(gitState.error || 'Working tree clean — no changes.')}</div>`;
96
+ return;
97
+ }
98
+ list.innerHTML = files.map((file) => {
99
+ const statusCode = String(file.status || '').toUpperCase();
100
+ const label = describeGitFileStatus(statusCode);
101
+ const fullPath = String(file.path || '');
102
+ const slashIndex = fullPath.lastIndexOf('/');
103
+ const fileName = slashIndex === -1 ? fullPath : fullPath.slice(slashIndex + 1);
104
+ const dirName = slashIndex === -1 ? '' : fullPath.slice(0, slashIndex);
105
+ const nameClass = file.deleted ? 'git-changes-file-name deleted' : 'git-changes-file-name';
106
+ const dirRow = dirName ? `<span class="git-changes-file-dir">${escHtml(dirName)}</span>` : '';
107
+ const renameNote = file.renamed && file.origPath
108
+ ? `<span class="git-changes-file-orig">← ${escHtml(file.origPath)}</span>`
109
+ : '';
110
+ return `
111
+ <button type="button" class="git-changes-file" data-git-diff-path="${escHtml(fullPath)}" data-git-status="${escHtml(statusCode)}" data-git-untracked="${file.untracked ? '1' : '0'}" title="${escHtml(`${label} · ${fullPath}`)}">
112
+ <span class="git-changes-file-status status-${escHtml(statusCode)}">${escHtml(statusCode)}</span>
113
+ <span class="git-changes-file-text">
114
+ <span class="${nameClass}">${escHtml(fileName)}</span>
115
+ ${dirRow}
116
+ ${renameNote}
117
+ </span>
118
+ </button>
119
+ `;
120
+ }).join('');
121
+ }
122
+
123
+ async function refreshGitChanges() {
124
+ gitState.loading = true;
125
+ gitState.error = '';
126
+ renderGitChanges();
127
+ const reqId = ++gitState.requestSeq;
128
+ const payload = await loadGitStatus(currentConvId);
129
+ if (reqId !== gitState.requestSeq) return;
130
+ gitState.loading = false;
131
+ if (!payload || payload.ok === false) {
132
+ gitState.error = payload?.error || 'Failed to load git status.';
133
+ } else {
134
+ gitState.status = payload;
135
+ gitState.error = '';
136
+ }
137
+ renderGitChanges();
138
+ }
139
+
140
+ async function runGitPull() {
141
+ if (gitState.pulling) return;
142
+ gitState.pulling = true;
143
+ gitState.notice = '';
144
+ gitState.error = '';
145
+ renderGitChanges();
146
+ const result = await requestGitPull(currentConvId);
147
+ gitState.pulling = false;
148
+ if (result?.ok) {
149
+ const output = String(result.output || '').trim();
150
+ const lastLine = output.split('\n').filter(Boolean).pop() || 'Pull complete.';
151
+ gitState.notice = `Pull: ${lastLine}`;
152
+ } else {
153
+ gitState.error = `Pull failed: ${result?.error || 'unknown error'}`;
154
+ }
155
+ renderGitChanges();
156
+ await refreshGitChanges();
157
+ }
158
+
159
+ export function openGitChangesModal() {
160
+ const { modal } = getGitElements();
161
+ if (!modal) return;
162
+ modal.classList.add('visible');
163
+ modal.setAttribute('aria-hidden', 'false');
164
+ gitState.notice = '';
165
+ renderGitChanges();
166
+ void refreshGitChanges();
167
+ }
168
+
169
+ export function closeGitChangesModal() {
170
+ const { modal } = getGitElements();
171
+ if (!modal) return;
172
+ modal.classList.remove('visible');
173
+ modal.setAttribute('aria-hidden', 'true');
174
+ }
175
+
176
+ function diffLineRowHtml(line) {
177
+ if (line.type === 'gap') {
178
+ return `<div class="git-diff-line gap"><span class="git-diff-gutter"></span><span class="git-diff-gutter"></span><span class="git-diff-text">⋯ ${line.count} unchanged line${line.count === 1 ? '' : 's'}</span></div>`;
179
+ }
180
+ const oldNo = line.oldLine == null ? '' : String(line.oldLine);
181
+ const newNo = line.newLine == null ? '' : String(line.newLine);
182
+ const marker = line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ');
183
+ return `<div class="git-diff-line ${line.type}"><span class="git-diff-gutter">${oldNo}</span><span class="git-diff-gutter">${newNo}</span><span class="git-diff-text">${escHtml(`${marker} ${line.text}`)}</span></div>`;
184
+ }
185
+
186
+ function renderGitDiff() {
187
+ const { diffTitle, diffMeta, diffBody, diffModeFull, diffModeChanges } = getGitElements();
188
+ if (!diffBody) return;
189
+ const diff = gitState.diff;
190
+ if (diffTitle) diffTitle.textContent = diff.path || 'Diff';
191
+ if (diffModeFull) diffModeFull.classList.toggle('active', diff.mode === 'full');
192
+ if (diffModeChanges) diffModeChanges.classList.toggle('active', diff.mode === 'changes');
193
+
194
+ if (diffMeta) {
195
+ const bits = [describeGitFileStatus(diff.status)];
196
+ if (diff.parsed && !diff.parsed.isBinary) {
197
+ bits.push(`+${diff.parsed.additions}`, `−${diff.parsed.deletions}`);
198
+ }
199
+ diffMeta.textContent = bits.join(' · ');
200
+ }
201
+
202
+ if (diff.loading) {
203
+ diffBody.innerHTML = '<div class="git-changes-empty">Loading diff…</div>';
204
+ return;
205
+ }
206
+ if (diff.error) {
207
+ diffBody.innerHTML = `<div class="git-changes-empty">${escHtml(diff.error)}</div>`;
208
+ return;
209
+ }
210
+ const parsed = diff.parsed;
211
+ if (!parsed) {
212
+ diffBody.innerHTML = '';
213
+ return;
214
+ }
215
+ if (parsed.isBinary) {
216
+ diffBody.innerHTML = '<div class="git-changes-empty">Binary file — no text diff available.</div>';
217
+ return;
218
+ }
219
+ if (parsed.isEmpty) {
220
+ diffBody.innerHTML = '<div class="git-changes-empty">No textual changes detected for this file.</div>';
221
+ return;
222
+ }
223
+ const lines = diff.mode === 'changes'
224
+ ? collapseContextLines(parsed.lines, CHANGES_MODE_CONTEXT_LINES)
225
+ : parsed.lines;
226
+ diffBody.innerHTML = `<div class="git-diff-lines">${lines.map(diffLineRowHtml).join('')}</div>`;
227
+ }
228
+
229
+ export async function openGitDiffViewer(path, { status = '', untracked = false } = {}) {
230
+ const { diffModal } = getGitElements();
231
+ if (!diffModal) return;
232
+ const diff = gitState.diff;
233
+ diff.path = String(path || '');
234
+ diff.status = String(status || '');
235
+ diff.untracked = !!untracked;
236
+ diff.loading = true;
237
+ diff.error = '';
238
+ diff.parsed = null;
239
+ diffModal.classList.add('visible');
240
+ diffModal.setAttribute('aria-hidden', 'false');
241
+ renderGitDiff();
242
+
243
+ const reqId = ++diff.requestSeq;
244
+ const payload = await loadGitDiff(diff.path, { conversationId: currentConvId, untracked: diff.untracked });
245
+ if (reqId !== diff.requestSeq) return;
246
+ diff.loading = false;
247
+ if (!payload || payload.ok === false) {
248
+ diff.error = payload?.error || 'Failed to load diff.';
249
+ } else {
250
+ diff.parsed = parseUnifiedDiff(payload.patch);
251
+ }
252
+ renderGitDiff();
253
+ }
254
+
255
+ export function closeGitDiffViewer() {
256
+ const { diffModal } = getGitElements();
257
+ if (!diffModal) return;
258
+ diffModal.classList.remove('visible');
259
+ diffModal.setAttribute('aria-hidden', 'true');
260
+ gitState.diff.requestSeq += 1;
261
+ }
262
+
263
+ export function setGitDiffMode(mode) {
264
+ gitState.diff.mode = mode === 'full' ? 'full' : 'changes';
265
+ renderGitDiff();
266
+ }
267
+
268
+ function bindGitChangesEvents() {
269
+ const {
270
+ modal, list, pullBtn, refreshBtn, closeBtn,
271
+ diffModal, diffModeFull, diffModeChanges, diffCloseBtn,
272
+ } = getGitElements();
273
+ if (!modal || !diffModal) return;
274
+ if (modal.dataset.bound === '1') return;
275
+ modal.dataset.bound = '1';
276
+
277
+ closeBtn?.addEventListener('click', (event) => {
278
+ event.preventDefault();
279
+ closeGitChangesModal();
280
+ });
281
+ refreshBtn?.addEventListener('click', (event) => {
282
+ event.preventDefault();
283
+ void refreshGitChanges();
284
+ });
285
+ pullBtn?.addEventListener('click', (event) => {
286
+ event.preventDefault();
287
+ void runGitPull();
288
+ });
289
+ modal.addEventListener('click', (event) => {
290
+ if (event.target === modal) closeGitChangesModal();
291
+ });
292
+ list?.addEventListener('click', (event) => {
293
+ const target = event.target instanceof Element ? event.target.closest('[data-git-diff-path]') : null;
294
+ if (!target) return;
295
+ const path = String(target.getAttribute('data-git-diff-path') || '').trim();
296
+ if (!path) return;
297
+ void openGitDiffViewer(path, {
298
+ status: String(target.getAttribute('data-git-status') || ''),
299
+ untracked: target.getAttribute('data-git-untracked') === '1',
300
+ });
301
+ });
302
+
303
+ diffCloseBtn?.addEventListener('click', (event) => {
304
+ event.preventDefault();
305
+ closeGitDiffViewer();
306
+ });
307
+ diffModal.addEventListener('click', (event) => {
308
+ if (event.target === diffModal) closeGitDiffViewer();
309
+ });
310
+ diffModeFull?.addEventListener('click', (event) => {
311
+ event.preventDefault();
312
+ setGitDiffMode('full');
313
+ });
314
+ diffModeChanges?.addEventListener('click', (event) => {
315
+ event.preventDefault();
316
+ setGitDiffMode('changes');
317
+ });
318
+
319
+ // Capture-phase so the diff viewer wins over the shared Escape chain in
320
+ // attachments-view.js; closing the diff keeps the changes modal open.
321
+ document.addEventListener('keydown', (event) => {
322
+ if (event.key !== 'Escape') return;
323
+ if (diffModal.classList.contains('visible')) {
324
+ event.preventDefault();
325
+ event.stopImmediatePropagation();
326
+ closeGitDiffViewer();
327
+ return;
328
+ }
329
+ if (modal.classList.contains('visible')) {
330
+ event.preventDefault();
331
+ event.stopImmediatePropagation();
332
+ closeGitChangesModal();
333
+ }
334
+ }, true);
335
+ }
336
+
337
+ export function initGitChangesView() {
338
+ bindGitChangesEvents();
339
+ }
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ // Pure parsing/shaping helpers for the git diff viewer. The server returns a
4
+ // unified diff generated with a huge context window (-U999999), so a single
5
+ // patch carries the whole file: "full" mode renders every line, "changes"
6
+ // mode collapses long context runs into gap markers.
7
+
8
+ const HUNK_HEADER_PATTERN = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
9
+
10
+ export function parseUnifiedDiff(patchText) {
11
+ const text = String(patchText || '');
12
+ const result = {
13
+ lines: [],
14
+ isBinary: /^Binary files .* differ$/m.test(text),
15
+ additions: 0,
16
+ deletions: 0,
17
+ isEmpty: false,
18
+ };
19
+ if (result.isBinary) return result;
20
+
21
+ let oldLine = 0;
22
+ let newLine = 0;
23
+ let inHunk = false;
24
+ for (const raw of text.split('\n')) {
25
+ const header = raw.match(HUNK_HEADER_PATTERN);
26
+ if (header) {
27
+ oldLine = Number(header[1]);
28
+ newLine = Number(header[3]);
29
+ inHunk = true;
30
+ continue;
31
+ }
32
+ if (!inHunk) continue;
33
+ if (raw.startsWith('\\')) continue; // ""
34
+ if (raw.startsWith('+')) {
35
+ result.lines.push({ type: 'add', text: raw.slice(1), oldLine: null, newLine });
36
+ newLine += 1;
37
+ result.additions += 1;
38
+ } else if (raw.startsWith('-')) {
39
+ result.lines.push({ type: 'del', text: raw.slice(1), oldLine, newLine: null });
40
+ oldLine += 1;
41
+ result.deletions += 1;
42
+ } else if (raw.startsWith(' ')) {
43
+ result.lines.push({ type: 'context', text: raw.slice(1), oldLine, newLine });
44
+ oldLine += 1;
45
+ newLine += 1;
46
+ } else if (raw.startsWith('diff --git')) {
47
+ inHunk = false;
48
+ }
49
+ // Anything else inside a hunk (empty trailing split artifact) is skipped.
50
+ }
51
+ result.isEmpty = result.lines.length === 0;
52
+ return result;
53
+ }
54
+
55
+ // Collapse context runs for "changes only" mode: keep `contextLines` context
56
+ // around every add/del line, replace longer runs with {type:'gap', count}.
57
+ export function collapseContextLines(lines, contextLines = 3) {
58
+ const source = Array.isArray(lines) ? lines : [];
59
+ const keep = new Array(source.length).fill(false);
60
+ source.forEach((line, index) => {
61
+ if (line.type !== 'add' && line.type !== 'del') return;
62
+ const from = Math.max(0, index - contextLines);
63
+ const to = Math.min(source.length - 1, index + contextLines);
64
+ for (let i = from; i <= to; i += 1) keep[i] = true;
65
+ });
66
+ const collapsed = [];
67
+ let gap = 0;
68
+ const flushGap = () => {
69
+ if (gap > 0) collapsed.push({ type: 'gap', count: gap });
70
+ gap = 0;
71
+ };
72
+ source.forEach((line, index) => {
73
+ if (keep[index]) {
74
+ flushGap();
75
+ collapsed.push(line);
76
+ } else {
77
+ gap += 1;
78
+ }
79
+ });
80
+ flushGap();
81
+ return collapsed;
82
+ }
83
+
84
+ const STATUS_LABELS = {
85
+ M: 'Modified',
86
+ A: 'Added',
87
+ D: 'Deleted',
88
+ R: 'Renamed',
89
+ C: 'Copied',
90
+ U: 'Untracked',
91
+ T: 'Type changed',
92
+ };
93
+
94
+ export function describeGitFileStatus(statusCode) {
95
+ const code = String(statusCode || '').trim().toUpperCase();
96
+ return STATUS_LABELS[code] || 'Changed';
97
+ }
98
+
99
+ export function summarizeGitStatusHeader(status) {
100
+ const parts = [];
101
+ const branch = String(status?.branch || '').trim();
102
+ if (status?.detached) parts.push('detached HEAD');
103
+ else if (branch) parts.push(branch);
104
+ const ahead = Number(status?.ahead || 0);
105
+ const behind = Number(status?.behind || 0);
106
+ if (ahead > 0) parts.push(`↑${ahead}`);
107
+ if (behind > 0) parts.push(`↓${behind}`);
108
+ return parts.join(' ');
109
+ }