@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.
- package/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- 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
|
+
}
|