@oar-sh/oar 0.9.6 → 0.9.8
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/CHANGELOG.md +285 -0
- package/README.md +222 -40
- package/bin/oar.js +320 -18
- package/package.json +1 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -13
- package/server/claude-worker/claude-session-process.mjs +225 -24
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
- package/server/db-schema.mjs +17 -0
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/activity-replay-state.mjs +68 -5
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +149 -6
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +468 -88
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +11 -0
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +127 -8
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +145 -11
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +698 -28
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/pwa-install.js +5 -0
- package/server/public/app/relay-error-ctas.mjs +16 -2
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/scrollbar-hover.mjs +78 -0
- package/server/public/app/settings-modal.js +37 -3
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +53 -1
- package/server/public/app/store.js +26 -0
- package/server/public/app/transcript-separators.mjs +48 -11
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon-light-192.png +0 -0
- package/server/public/app-icon-light-512.png +0 -0
- package/server/public/app-icon-light-maskable-512.png +0 -0
- package/server/public/app-icon-light.svg +4 -0
- package/server/public/app-icon-maskable-512.png +0 -0
- package/server/public/app-icon.svg +3 -3
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +564 -20
- package/server/public/manifest.webmanifest +9 -3
- package/server/public/sw.js +6 -1
- package/server/relay-tools.md +13 -1
- package/server/repositories/remote-relay-repository.mjs +42 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +123 -16
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +517 -74
- package/server/server-runtime.mjs +268 -33
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/claude-context-usage.mjs +51 -3
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/oar-cli-helpers.mjs +67 -1
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/pwa-manifest-theme.mjs +30 -0
- package/server/services/remote-relay-caller-context.mjs +49 -10
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +383 -35
- package/server/services/remote-relay-inbound.mjs +49 -2
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +348 -129
- package/server/services/session-worker-registry-service.mjs +18 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +56 -1
- package/server/services/update-install-service.mjs +9 -3
- package/server/services/usage-limit-pause-service.mjs +3 -1
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/remote-relay-contract.mjs +153 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +1 -0
|
@@ -46,7 +46,9 @@ export function areNetworkRequestsEnabled() {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export async function apiFetch(url, opts = {}) {
|
|
49
|
-
|
|
49
|
+
// A keepalive request is a page's last word; it goes out even when the
|
|
50
|
+
// page's regular network work is already suspended.
|
|
51
|
+
if (!networkRequestsEnabled && !opts.keepalive) return null;
|
|
50
52
|
try {
|
|
51
53
|
const response = await fetch(`${BASE}${url}`, {
|
|
52
54
|
signal: requestTimeoutSignal(),
|
|
@@ -273,11 +275,13 @@ export async function updateClaudeSettings({
|
|
|
273
275
|
enabled = undefined,
|
|
274
276
|
model = undefined,
|
|
275
277
|
enabledModels = undefined,
|
|
278
|
+
attributionMode = undefined,
|
|
276
279
|
} = {}) {
|
|
277
280
|
const payload = {};
|
|
278
281
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
279
282
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
280
283
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
284
|
+
if (typeof attributionMode === 'string' && attributionMode.trim()) payload.attributionMode = attributionMode.trim();
|
|
281
285
|
return settingsRequest('/api/settings/claude', payload, 'Failed to update Claude settings');
|
|
282
286
|
}
|
|
283
287
|
|
|
@@ -370,6 +374,65 @@ export async function updateGrokSettings({
|
|
|
370
374
|
return settingsRequest('/api/settings/grok', payload, 'Failed to update Grok settings');
|
|
371
375
|
}
|
|
372
376
|
|
|
377
|
+
// Claude Cloud (provider `claude-cloud`). A relay without these routes answers
|
|
378
|
+
// 404, and a folder outside the workspace roots is refused: both must read as
|
|
379
|
+
// "nothing to show" (the New Chat option stays hidden, the fields stay empty),
|
|
380
|
+
// so these reads skip apiFetch's error logging.
|
|
381
|
+
async function quietGet(path) {
|
|
382
|
+
if (!networkRequestsEnabled) return null;
|
|
383
|
+
try {
|
|
384
|
+
const response = await fetch(`${BASE}${path}`, {
|
|
385
|
+
signal: requestTimeoutSignal(),
|
|
386
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
387
|
+
});
|
|
388
|
+
if (!response.ok) return null;
|
|
389
|
+
noteFetchSuccess();
|
|
390
|
+
return await response.json().catch(() => null);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
noteFetchFailure(path, error);
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function loadClaudeCloudSettings() {
|
|
398
|
+
return quietGet('/api/settings/claude-cloud');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function updateClaudeCloudSettings({
|
|
402
|
+
enabled = undefined,
|
|
403
|
+
defaultModel = undefined,
|
|
404
|
+
environmentId = undefined,
|
|
405
|
+
} = {}) {
|
|
406
|
+
const payload = {};
|
|
407
|
+
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
408
|
+
if (typeof defaultModel === 'string' && defaultModel.trim()) payload.defaultModel = defaultModel.trim();
|
|
409
|
+
if (typeof environmentId === 'string' && environmentId.trim()) payload.environmentId = environmentId.trim();
|
|
410
|
+
return settingsRequest('/api/settings/claude-cloud', payload, 'Failed to update Claude Cloud settings');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// origin + current branch of a folder on the relay host, for the New Chat
|
|
414
|
+
// Repository and Branch fields. null when the relay could not say.
|
|
415
|
+
export async function loadGitRemote(rootPath) {
|
|
416
|
+
const root = String(rootPath || '').trim();
|
|
417
|
+
if (!root) return null;
|
|
418
|
+
return quietGet(`/api/git/remote?root=${encodeURIComponent(root)}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// The repositories the Claude GitHub app can reach, plus the ones used in
|
|
422
|
+
// earlier cloud chats here, for the New Chat Repository suggestions. null
|
|
423
|
+
// when the relay could not say (the field is then typed, as before).
|
|
424
|
+
export async function loadClaudeCloudRepos({ refresh = false } = {}) {
|
|
425
|
+
return quietGet(`/api/claude-cloud/repos${refresh ? '?refresh=1' : ''}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// The branches of a GitHub repository as the relay host's git sees them, for
|
|
429
|
+
// the New Chat Branch suggestions. null when the relay could not say.
|
|
430
|
+
export async function loadClaudeCloudBranches(repo) {
|
|
431
|
+
const text = String(repo || '').trim();
|
|
432
|
+
if (!text) return null;
|
|
433
|
+
return quietGet(`/api/claude-cloud/branches?repo=${encodeURIComponent(text)}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
373
436
|
export async function updateOpenAISettings({
|
|
374
437
|
apiKey = '',
|
|
375
438
|
model = 'gpt-4o',
|
|
@@ -542,6 +605,23 @@ export async function updateRemoteRelaySettings(patch = {}) {
|
|
|
542
605
|
return remoteRelayRequest('/api/settings/remote-relays', { method: 'POST', body });
|
|
543
606
|
}
|
|
544
607
|
|
|
608
|
+
// ─── Agent sessions (Settings → Relays) ──────────────────────────────────────
|
|
609
|
+
// `{ enabled, maxWaitSeconds, limits, maxActiveSessions }`, with `ok` and
|
|
610
|
+
// `status` as above. A relay without the feature answers 404, which the
|
|
611
|
+
// section reads as "do not show me", so the status has to travel with the
|
|
612
|
+
// answer instead of apiFetch's bare null.
|
|
613
|
+
|
|
614
|
+
export async function loadAgentSessionsSettings() {
|
|
615
|
+
return remoteRelayRequest('/api/settings/agent-sessions');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export async function updateAgentSessionsSettings(patch = {}) {
|
|
619
|
+
const body = {};
|
|
620
|
+
if (typeof patch.enabled === 'boolean') body.enabled = patch.enabled;
|
|
621
|
+
if (Number.isFinite(patch.maxWaitSeconds)) body.maxWaitSeconds = patch.maxWaitSeconds;
|
|
622
|
+
return remoteRelayRequest('/api/settings/agent-sessions', { method: 'POST', body });
|
|
623
|
+
}
|
|
624
|
+
|
|
545
625
|
export async function loadUpdateState() {
|
|
546
626
|
return apiFetch('/api/update/state');
|
|
547
627
|
}
|
|
@@ -607,8 +687,18 @@ export async function relaunchSessionWorkerWithWorkspaceRoot(conversationId, roo
|
|
|
607
687
|
});
|
|
608
688
|
}
|
|
609
689
|
|
|
610
|
-
export async function loadUsageSummary() {
|
|
611
|
-
|
|
690
|
+
export async function loadUsageSummary({ providers = null } = {}) {
|
|
691
|
+
// The conversation the modal was opened from: the Claude Cloud card shows
|
|
692
|
+
// that conversation's own cost next to the totals. `providers` names the
|
|
693
|
+
// cards to read live (the rest come from the relay's last live answer);
|
|
694
|
+
// none means everything live.
|
|
695
|
+
const conversationId = String(currentConvId || '').trim();
|
|
696
|
+
const query = new URLSearchParams();
|
|
697
|
+
if (conversationId) query.set('conversationId', conversationId);
|
|
698
|
+
const ids = (Array.isArray(providers) ? providers : []).map((id) => String(id || '').trim()).filter(Boolean);
|
|
699
|
+
if (ids.length) query.set('providers', ids.join(','));
|
|
700
|
+
const text = query.toString();
|
|
701
|
+
return apiFetch(text ? `/api/usage?${text}` : '/api/usage');
|
|
612
702
|
}
|
|
613
703
|
|
|
614
704
|
export async function loadCursorAllowanceSettings() {
|
|
@@ -690,8 +780,9 @@ export async function loadConversations(options = {}) {
|
|
|
690
780
|
if (beforeConversationId) params.set('beforeConversationId', beforeConversationId);
|
|
691
781
|
const beforeUpdatedAt = String(options.beforeUpdatedAt || '').trim();
|
|
692
782
|
if (beforeUpdatedAt) params.set('beforeUpdatedAt', beforeUpdatedAt);
|
|
693
|
-
|
|
694
|
-
|
|
783
|
+
// 'true' lists live and archived chats, 'only' the archived ones.
|
|
784
|
+
const archivedMode = String(options.archived || '').trim().toLowerCase();
|
|
785
|
+
if (archivedMode === 'true' || archivedMode === 'only') params.set('archived', archivedMode);
|
|
695
786
|
const query = params.toString();
|
|
696
787
|
return apiFetch(`/api/conversations${query ? `?${query}` : ''}`);
|
|
697
788
|
}
|
|
@@ -743,6 +834,39 @@ export async function searchMessages(options = {}) {
|
|
|
743
834
|
// Raw fetch instead of apiFetch: a refused delete (the conversation is still
|
|
744
835
|
// working, or its session would not stop) must surface the server's reason.
|
|
745
836
|
// The long timeout covers stopping the session and deleting its CLI session.
|
|
837
|
+
// Archive and unarchive answer `{ ok }`, or `{ ok: false, message }` when the
|
|
838
|
+
// relay refuses (a chat still working on a reply is not archived).
|
|
839
|
+
export async function archiveConversation(id) {
|
|
840
|
+
const convId = String(id || '').trim();
|
|
841
|
+
if (!convId) return null;
|
|
842
|
+
return conversationActionRequest(`/api/conversation/${encodeURIComponent(convId)}/archive`);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export async function unarchiveConversation(id) {
|
|
846
|
+
const convId = String(id || '').trim();
|
|
847
|
+
if (!convId) return null;
|
|
848
|
+
return conversationActionRequest(`/api/conversation/${encodeURIComponent(convId)}/unarchive`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function conversationActionRequest(url) {
|
|
852
|
+
if (!areNetworkRequestsEnabled()) return null;
|
|
853
|
+
try {
|
|
854
|
+
const response = await fetch(`${BASE}${url}`, {
|
|
855
|
+
signal: requestTimeoutSignal(LONG_REQUEST_TIMEOUT_MS),
|
|
856
|
+
method: 'POST',
|
|
857
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
858
|
+
body: '{}',
|
|
859
|
+
});
|
|
860
|
+
const payload = await response.json().catch(() => null);
|
|
861
|
+
if (!response.ok) return { ...(payload || {}), ok: false, status: response.status };
|
|
862
|
+
noteFetchSuccess();
|
|
863
|
+
return payload || { ok: true };
|
|
864
|
+
} catch (error) {
|
|
865
|
+
noteFetchFailure(url, error);
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
746
870
|
export async function deleteConversation(id) {
|
|
747
871
|
const convId = String(id || '').trim();
|
|
748
872
|
if (!convId || !areNetworkRequestsEnabled()) return null;
|
|
@@ -785,12 +909,31 @@ export async function updateConversationTitle(id, title) {
|
|
|
785
909
|
});
|
|
786
910
|
}
|
|
787
911
|
|
|
788
|
-
|
|
912
|
+
// The commit attribution a repo folder's Claude sessions use (null = the
|
|
913
|
+
// provider setting). Keyed by the workspace root path.
|
|
914
|
+
export async function loadWorkspaceRootAttribution(path) {
|
|
915
|
+
const root = String(path || '').trim();
|
|
916
|
+
if (!root) return null;
|
|
917
|
+
return apiFetch(`/api/workspace-root/attribution?path=${encodeURIComponent(root)}`);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
export async function updateWorkspaceRootAttribution(path, attributionMode) {
|
|
921
|
+
const root = String(path || '').trim();
|
|
922
|
+
if (!root) return null;
|
|
923
|
+
return apiFetch('/api/workspace-root/attribution', {
|
|
924
|
+
method: 'POST',
|
|
925
|
+
body: JSON.stringify({ path: root, attributionMode: attributionMode || null }),
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// `keepalive` lets the request outlive the page (a save made while it closes).
|
|
930
|
+
export async function updateConversationPreferences(id, preferences = {}, { keepalive = false } = {}) {
|
|
789
931
|
const convId = String(id || '').trim();
|
|
790
932
|
if (!convId) return null;
|
|
791
933
|
return apiFetch(`/api/conversation/${convId}/preferences`, {
|
|
792
934
|
method: 'PATCH',
|
|
793
935
|
body: JSON.stringify(preferences || {}),
|
|
936
|
+
...(keepalive ? { keepalive: true } : {}),
|
|
794
937
|
});
|
|
795
938
|
}
|
|
796
939
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
showTransientRelayNotice,
|
|
14
14
|
getConversationCurrentWorkspaceRootPath,
|
|
15
15
|
IS_SHARED_VIEW,
|
|
16
|
+
conversations,
|
|
16
17
|
} from './store.js';
|
|
17
18
|
import { openAnnotateEditor } from './annotate-editor.js';
|
|
18
19
|
import {
|
|
@@ -61,6 +62,11 @@ import {
|
|
|
61
62
|
writeRepoBrowserHiddenPreference,
|
|
62
63
|
} from './repo-browser-preferences.mjs';
|
|
63
64
|
import { joinLaunchCwdPath } from './launch-cwd-path.mjs';
|
|
65
|
+
import {
|
|
66
|
+
cloudAttachmentRejectionNotice,
|
|
67
|
+
isClaudeCloudConversation,
|
|
68
|
+
partitionCloudAttachments,
|
|
69
|
+
} from './claude-cloud-ui.mjs';
|
|
64
70
|
|
|
65
71
|
function currentConversationId() {
|
|
66
72
|
return String(currentConvId || '').trim();
|
|
@@ -509,7 +515,7 @@ export async function ingestFiles(files, { source = 'picker' } = {}) {
|
|
|
509
515
|
// conversations, and the result must never land in the wrong composer.
|
|
510
516
|
const ownerConversationId = currentConversationId();
|
|
511
517
|
const now = new Date();
|
|
512
|
-
|
|
518
|
+
let prepared = [];
|
|
513
519
|
for (let index = 0; index < inputFiles.length; index += 1) {
|
|
514
520
|
prepared.push(await prepareIncomingFile(inputFiles[index], { source, now, index }));
|
|
515
521
|
}
|
|
@@ -519,6 +525,19 @@ export async function ingestFiles(files, { source = 'picker' } = {}) {
|
|
|
519
525
|
return [];
|
|
520
526
|
}
|
|
521
527
|
|
|
528
|
+
// A Claude Cloud session takes inline images only; anything else would be
|
|
529
|
+
// refused by its worker with the whole turn. Checked after the re-encode
|
|
530
|
+
// above, which is what brings a large photo under the size limit.
|
|
531
|
+
if (isClaudeCloudConversation(conversations[ownerConversationId])) {
|
|
532
|
+
const { accepted, rejected } = partitionCloudAttachments(prepared);
|
|
533
|
+
if (rejected.length) {
|
|
534
|
+
for (const { attachment } of rejected) releaseAttachmentPreviewUrl(attachment);
|
|
535
|
+
showTransientRelayNotice(cloudAttachmentRejectionNotice(rejected), 7000);
|
|
536
|
+
prepared = accepted;
|
|
537
|
+
if (!prepared.length) return [];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
522
541
|
const plan = planAttachmentMerge(selectedAttachments, prepared, MAX_UPLOAD_ATTACHMENTS);
|
|
523
542
|
const rejected = prepared.filter((item) => !plan.acceptedAdditions.includes(item));
|
|
524
543
|
for (const item of rejected) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The commit-attribution row of the 🧠 context modal (Claude sessions only):
|
|
2
|
+
// what this repo folder's commits say, and a select to override the provider
|
|
3
|
+
// setting for the folder — the payload comes from /api/context (`attribution`).
|
|
4
|
+
|
|
5
|
+
const MODE_LABELS = Object.freeze({
|
|
6
|
+
oar: 'OAR — Open Agent Relay (model)',
|
|
7
|
+
vanilla: 'Vanilla — Claude Code\'s own',
|
|
8
|
+
off: 'Off — none',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function esc(value) {
|
|
12
|
+
return String(value ?? '')
|
|
13
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function attributionEffectText({ effectiveMode, attributionExample } = {}) {
|
|
17
|
+
if (effectiveMode === 'vanilla') return 'Commits keep Claude Code\'s own attribution.';
|
|
18
|
+
if (effectiveMode === 'off') return 'Commits and pull requests carry no attribution line.';
|
|
19
|
+
const trailer = String(attributionExample || '').trim();
|
|
20
|
+
return trailer ? `Commits end with: ${trailer}` : 'Commits end with the Open Agent Relay trailer.';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object|null} attribution the `attribution` field of an /api/context response
|
|
25
|
+
* @returns {string} the row's HTML, or '' when the payload has no folder to speak of
|
|
26
|
+
*/
|
|
27
|
+
export function renderAttributionControlHtml(attribution) {
|
|
28
|
+
if (!attribution || typeof attribution !== 'object') return '';
|
|
29
|
+
const path = String(attribution.path || '').trim();
|
|
30
|
+
if (!path) return '';
|
|
31
|
+
const folderMode = ['oar', 'vanilla', 'off'].includes(attribution.attributionMode) ? attribution.attributionMode : '';
|
|
32
|
+
const providerMode = ['oar', 'vanilla', 'off'].includes(attribution.providerMode) ? attribution.providerMode : 'oar';
|
|
33
|
+
const options = [
|
|
34
|
+
`<option value=""${folderMode === '' ? ' selected' : ''}>Inherit (provider: ${esc(MODE_LABELS[providerMode].split(' — ')[0])})</option>`,
|
|
35
|
+
...['oar', 'vanilla', 'off'].map((mode) => `<option value="${mode}"${folderMode === mode ? ' selected' : ''}>${esc(MODE_LABELS[mode])}</option>`),
|
|
36
|
+
].join('');
|
|
37
|
+
return `
|
|
38
|
+
<div class="ctx-attribution" data-workspace-root="${esc(path)}">
|
|
39
|
+
<div class="ctx-attribution-row">
|
|
40
|
+
<span class="ctx-attribution-label">Commit attribution:</span>
|
|
41
|
+
<select id="ctx-attribution-select" class="ctx-attribution-select" aria-label="Commit attribution for this folder" data-workspace-root="${esc(path)}">${options}</select>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="ctx-attribution-note">For every Claude session in <code>${esc(path)}</code>. ${esc(attributionEffectText(attribution))} A change reaches running sessions with their next message.</div>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
}
|