@oar-sh/oar 0.9.5 → 0.9.7
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/polling/polling-loop.mjs +12 -32
- package/.github/extensions/web-relay/skills/question-bridge.mjs +14 -39
- package/CHANGELOG.md +340 -0
- package/README.md +196 -26
- package/package.json +2 -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 -2
- package/server/claude-worker/claude-session-process.mjs +151 -1
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/claude-turn-failure.mjs +92 -0
- package/server/claude-worker/claude-turn-publisher.mjs +52 -3
- package/server/claude-worker/sdk-message-normalizer.mjs +13 -1
- package/server/copilot-worker/copilot-model-switch.mjs +29 -3
- package/server/copilot-worker/copilot-question-bridge.mjs +2 -4
- package/server/copilot-worker/copilot-runtime-tree.mjs +91 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +48 -8
- package/server/copilot-worker/copilot-sdk-session-process.mjs +897 -166
- package/server/cursor-worker/cursor-sdk-adapter.mjs +80 -13
- package/server/cursor-worker/cursor-turn-runner.mjs +5 -2
- package/server/db-schema.mjs +24 -0
- package/server/grok-worker/acp-client.mjs +68 -12
- package/server/grok-worker/grok-sdk-adapter.mjs +4 -0
- package/server/grok-worker/grok-turn-runner.mjs +6 -2
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- 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 +160 -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 +347 -31
- 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 +15 -3
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- 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 +178 -8
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +704 -28
- package/server/public/app/markdown-strikethrough.mjs +31 -0
- 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/relay-board-inline.mjs +70 -0
- package/server/public/app/relay-board-view.js +157 -61
- package/server/public/app/relay-error-ctas.mjs +28 -5
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +60 -0
- package/server/public/app/store.js +4 -1
- package/server/public/app/subagent-fold.mjs +61 -0
- package/server/public/app/usage-limit-ui.js +225 -0
- package/server/public/index.html +471 -8
- package/server/relay-tools.md +13 -1
- package/server/repositories/message-repository.mjs +10 -1
- package/server/repositories/question-repository.mjs +1 -0
- package/server/repositories/remote-relay-repository.mjs +72 -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 +202 -26
- package/server/routes/relay-board-routes.mjs +56 -2
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +538 -72
- package/server/server-runtime.mjs +291 -27
- 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/git-remote-service.mjs +115 -0
- 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/preview-proxy-server.mjs +64 -36
- package/server/services/process-list-service.mjs +122 -0
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/relay-console-log.mjs +199 -0
- package/server/services/remote-relay-caller-context.mjs +78 -19
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +451 -39
- package/server/services/remote-relay-inbound.mjs +83 -1
- 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 +410 -138
- package/server/services/session-worker-registry-service.mjs +1 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +89 -2
- package/server/services/usage-limit-pause-service.mjs +443 -0
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/ask-user-bridge.mjs +14 -39
- 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/claude-usage-limit.mjs +257 -0
- package/shared/control-poller.mjs +69 -20
- package/shared/failure-note-text.mjs +38 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/question-wait.mjs +108 -0
- package/shared/remote-relay-contract.mjs +167 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +17 -0
- package/shared/steer-settle-markers.mjs +8 -0
- package/shared/worker-runtime/process-tree.mjs +483 -0
- package/shared/worker-runtime/relay-errors.mjs +31 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +4 -4
- package/shared/worker-runtime/turn-liveness.mjs +249 -0
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
// to the source session) and the "↗ win-test" marker on the sidebar row of a
|
|
4
4
|
// conversation such an agent created. Both read the `origin` the relay stores
|
|
5
5
|
// with the message or conversation (docs/plans/2026-09-27-remote-relays.md §4).
|
|
6
|
+
//
|
|
7
|
+
// An agent on THIS relay can start sessions too. Such an origin carries
|
|
8
|
+
// `local: true` and names the conversation the agent runs in: the marker then
|
|
9
|
+
// reads "via agent · “title”" and opens that conversation inside the app
|
|
10
|
+
// (window.openOriginConversation, journal-view.js) instead of linking to
|
|
11
|
+
// another relay. An origin without `local` renders exactly as before.
|
|
6
12
|
|
|
7
13
|
import { remoteConversationUrl } from './remote-relay-shared.mjs';
|
|
8
14
|
|
|
@@ -19,6 +25,76 @@ function oneLine(value) {
|
|
|
19
25
|
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
// How much of the other conversation's title a marker carries; CSS ellipsis
|
|
29
|
+
// takes over where the row is narrower than that.
|
|
30
|
+
export const ORIGIN_TITLE_MAX_SIDEBAR = 28;
|
|
31
|
+
export const ORIGIN_TITLE_MAX = 60;
|
|
32
|
+
|
|
33
|
+
/** "a long conversation tit…": at most `max` characters, one line. */
|
|
34
|
+
export function truncateOriginTitle(value, max = ORIGIN_TITLE_MAX) {
|
|
35
|
+
const text = oneLine(value);
|
|
36
|
+
const limit = Math.max(2, Math.floor(Number(max)) || ORIGIN_TITLE_MAX);
|
|
37
|
+
// By code point, so an emoji is never cut in half.
|
|
38
|
+
const chars = Array.from(text);
|
|
39
|
+
if (chars.length <= limit) return text;
|
|
40
|
+
return `${chars.slice(0, limit - 1).join('').trimEnd()}…`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True for the origin of work an agent on this relay started. */
|
|
44
|
+
export function isLocalAgentOrigin(raw) {
|
|
45
|
+
return !!raw && typeof raw === 'object' && !Array.isArray(raw) && raw.local === true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function knownConversation(conversations, id) {
|
|
49
|
+
if (!id || !conversations || typeof conversations !== 'object') return null;
|
|
50
|
+
const found = Object.prototype.hasOwnProperty.call(conversations, id) ? conversations[id] : null;
|
|
51
|
+
return found && typeof found === 'object' ? found : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What a local origin shows, or null when the origin is not a local one.
|
|
56
|
+
* `conversations` is the browser's list by id: the marker is a link only while
|
|
57
|
+
* the orchestrating conversation is in it, and shows that conversation's
|
|
58
|
+
* current title (the origin stores the one it had at creation). `ownId` is the
|
|
59
|
+
* conversation the marker sits on, which never links to itself.
|
|
60
|
+
*/
|
|
61
|
+
export function localAgentOriginModel(origin, {
|
|
62
|
+
conversations = null,
|
|
63
|
+
ownId = '',
|
|
64
|
+
linkable = true,
|
|
65
|
+
maxTitleLength = ORIGIN_TITLE_MAX,
|
|
66
|
+
} = {}) {
|
|
67
|
+
if (!isLocalAgentOrigin(origin)) return null;
|
|
68
|
+
const conversationId = oneLine(origin.conversationId);
|
|
69
|
+
const known = conversationId !== oneLine(ownId) ? knownConversation(conversations, conversationId) : null;
|
|
70
|
+
const fullTitle = oneLine(known?.title) || oneLine(origin.conversationTitle);
|
|
71
|
+
const title = truncateOriginTitle(fullTitle, maxTitleLength);
|
|
72
|
+
return {
|
|
73
|
+
conversationId,
|
|
74
|
+
fullTitle,
|
|
75
|
+
title,
|
|
76
|
+
model: oneLine(origin.model),
|
|
77
|
+
text: title ? `via agent · “${title}”` : 'via agent',
|
|
78
|
+
linkable: linkable !== false && !!known,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A button while the orchestrating conversation can be opened, plain text
|
|
83
|
+
// otherwise. `tooltip` is a full sentence.
|
|
84
|
+
function localAgentElementHtml(model, { className, text = model.text, tooltip }) {
|
|
85
|
+
const label = escapeHtml(text);
|
|
86
|
+
if (model.linkable) {
|
|
87
|
+
return `<button type="button" class="${className}" data-origin-conversation-id="${escapeHtml(model.conversationId)}" title="${escapeHtml(`${tooltip} Opens that conversation.`)}" onclick="openOriginConversation(event, this)">${label}</button>`;
|
|
88
|
+
}
|
|
89
|
+
return `<span class="${className}" title="${escapeHtml(tooltip)}">${label}</span>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function startedByTooltip(model) {
|
|
93
|
+
return model.fullTitle
|
|
94
|
+
? `Started by an agent in “${model.fullTitle}”.`
|
|
95
|
+
: 'Started by an agent in another conversation on this relay.';
|
|
96
|
+
}
|
|
97
|
+
|
|
22
98
|
/** The display fields of an `origin`, or null when it names no relay. */
|
|
23
99
|
export function normalizeRemoteOrigin(raw) {
|
|
24
100
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
@@ -35,7 +111,9 @@ export function normalizeRemoteOrigin(raw) {
|
|
|
35
111
|
};
|
|
36
112
|
}
|
|
37
113
|
|
|
38
|
-
export function remoteOriginBadgeText(origin) {
|
|
114
|
+
export function remoteOriginBadgeText(origin, options = {}) {
|
|
115
|
+
const local = localAgentOriginModel(origin, options);
|
|
116
|
+
if (local) return local.model ? `${local.text} · ${local.model}` : local.text;
|
|
39
117
|
const normalized = normalizeRemoteOrigin(origin);
|
|
40
118
|
if (!normalized) return '';
|
|
41
119
|
const parts = [`↗ from ${normalized.relayName}`];
|
|
@@ -54,8 +132,21 @@ export function remoteOriginLink(origin) {
|
|
|
54
132
|
/**
|
|
55
133
|
* The badge row that sits above a user bubble's text. `linkable: false` (the
|
|
56
134
|
* shared read-only view) renders it without the link to the other relay.
|
|
135
|
+
* `conversations` (the browser's list by id) lets a local agent's badge open
|
|
136
|
+
* the conversation that agent runs in.
|
|
57
137
|
*/
|
|
58
|
-
export function renderRemoteOriginBadgeHtml(origin, { linkable = true } = {}) {
|
|
138
|
+
export function renderRemoteOriginBadgeHtml(origin, { linkable = true, conversations = null } = {}) {
|
|
139
|
+
const local = localAgentOriginModel(origin, { conversations, linkable });
|
|
140
|
+
if (local) {
|
|
141
|
+
const badge = localAgentElementHtml(local, {
|
|
142
|
+
className: 'msg-origin-badge msg-origin-agent',
|
|
143
|
+
text: remoteOriginBadgeText(origin, { conversations }),
|
|
144
|
+
tooltip: local.fullTitle
|
|
145
|
+
? `Sent by an agent in “${local.fullTitle}”.`
|
|
146
|
+
: 'Sent by an agent in another conversation on this relay.',
|
|
147
|
+
});
|
|
148
|
+
return `<div class="msg-origin">${badge}</div>`;
|
|
149
|
+
}
|
|
59
150
|
const normalized = normalizeRemoteOrigin(origin);
|
|
60
151
|
if (!normalized) return '';
|
|
61
152
|
const label = escapeHtml(remoteOriginBadgeText(normalized));
|
|
@@ -67,10 +158,35 @@ export function renderRemoteOriginBadgeHtml(origin, { linkable = true } = {}) {
|
|
|
67
158
|
return `<div class="msg-origin"><span class="msg-origin-badge" title="Sent by an agent on relay ${name}.">${label}</span></div>`;
|
|
68
159
|
}
|
|
69
160
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The sidebar marker for a conversation an agent created: "↗ win-test" for
|
|
163
|
+
* another relay's agent, "via agent · “title”" for one on this relay.
|
|
164
|
+
*/
|
|
165
|
+
export function renderConversationOriginMarkerHtml(conversation, { conversations = null, linkable = true } = {}) {
|
|
166
|
+
const local = localAgentOriginModel(conversation?.origin, {
|
|
167
|
+
conversations,
|
|
168
|
+
linkable,
|
|
169
|
+
ownId: conversation?.id,
|
|
170
|
+
maxTitleLength: ORIGIN_TITLE_MAX_SIDEBAR,
|
|
171
|
+
});
|
|
172
|
+
if (local) {
|
|
173
|
+
return localAgentElementHtml(local, {
|
|
174
|
+
className: 'conv-origin-marker conv-origin-agent',
|
|
175
|
+
tooltip: startedByTooltip(local),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
72
178
|
const normalized = normalizeRemoteOrigin(conversation?.origin);
|
|
73
179
|
if (!normalized) return '';
|
|
74
180
|
const name = escapeHtml(normalized.relayName);
|
|
75
181
|
return `<span class="conv-origin-marker" title="Started by an agent on relay ${name}">↗ ${name}</span>`;
|
|
76
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The same marker for the conversation header, with room for more of the
|
|
186
|
+
* title. Only a local origin has one; '' for everything else.
|
|
187
|
+
*/
|
|
188
|
+
export function renderConversationHeaderOriginHtml(conversation, { conversations = null, linkable = true } = {}) {
|
|
189
|
+
const local = localAgentOriginModel(conversation?.origin, { conversations, linkable, ownId: conversation?.id });
|
|
190
|
+
if (!local) return '';
|
|
191
|
+
return localAgentElementHtml(local, { className: 'conv-origin-agent', tooltip: startedByTooltip(local) });
|
|
192
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
WORKSPACE_FILE_EXTENSIONS,
|
|
7
7
|
serverPlatform,
|
|
8
8
|
} from './store.js';
|
|
9
|
+
import { scanInlineDollars, splitAtLiteralDollars } from './math-dollars.mjs';
|
|
9
10
|
|
|
10
11
|
export function workspaceMentionRegex() {
|
|
11
12
|
return /(?:[A-Za-z]:[\\/])?(?:\.{1,2}[\\/])?(?:[A-Za-z0-9._-]+[\\/])*[A-Za-z0-9._-]+\.[A-Za-z0-9]{1,16}/g;
|
|
@@ -397,7 +398,8 @@ function normalizeInlineMathDelimiters(line) {
|
|
|
397
398
|
let output = '';
|
|
398
399
|
let index = 0;
|
|
399
400
|
let inCode = false;
|
|
400
|
-
|
|
401
|
+
// Only real dollar maths is skipped over; a price's `$` opens nothing.
|
|
402
|
+
const dollarMathEnds = new Map(scanInlineDollars(line).spans);
|
|
401
403
|
|
|
402
404
|
while (index < line.length) {
|
|
403
405
|
const char = line[index];
|
|
@@ -412,15 +414,10 @@ function normalizeInlineMathDelimiters(line) {
|
|
|
412
414
|
index += 1;
|
|
413
415
|
continue;
|
|
414
416
|
}
|
|
415
|
-
if (char === '$' &&
|
|
416
|
-
|
|
417
|
-
output +=
|
|
418
|
-
index
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (inDollarMath) {
|
|
422
|
-
output += char;
|
|
423
|
-
index += 1;
|
|
417
|
+
if (char === '$' && dollarMathEnds.has(index)) {
|
|
418
|
+
const closeIndex = dollarMathEnds.get(index);
|
|
419
|
+
output += line.slice(index, closeIndex + 1);
|
|
420
|
+
index = closeIndex + 1;
|
|
424
421
|
continue;
|
|
425
422
|
}
|
|
426
423
|
if (char === '\\' && line[index + 1] === '(') {
|
|
@@ -469,6 +466,7 @@ function protectMarkdownEscapesInMath(source) {
|
|
|
469
466
|
let output = '';
|
|
470
467
|
let index = 0;
|
|
471
468
|
let inCode = false;
|
|
469
|
+
const dollarMathEnds = new Map(scanInlineDollars(line).spans);
|
|
472
470
|
while (index < line.length) {
|
|
473
471
|
if (line[index] === '`') {
|
|
474
472
|
inCode = !inCode;
|
|
@@ -482,7 +480,10 @@ function protectMarkdownEscapesInMath(source) {
|
|
|
482
480
|
continue;
|
|
483
481
|
}
|
|
484
482
|
const delimiter = line[index + 1] === '$' ? '$$' : '$';
|
|
485
|
-
|
|
483
|
+
// A single `$` that is a plain dollar sign (a price) opens no formula.
|
|
484
|
+
const closeIndex = delimiter === '$'
|
|
485
|
+
? (dollarMathEnds.has(index) ? dollarMathEnds.get(index) : -1)
|
|
486
|
+
: line.indexOf(delimiter, index + delimiter.length);
|
|
486
487
|
if (closeIndex < 0) {
|
|
487
488
|
output += line[index];
|
|
488
489
|
index += 1;
|
|
@@ -550,6 +551,40 @@ export function normalizeMathDelimiters(source) {
|
|
|
550
551
|
return protectMarkdownEscapesInMath(normalized.join('\n'));
|
|
551
552
|
}
|
|
552
553
|
|
|
554
|
+
const MATH_IGNORED_TAGS = ['pre', 'code', 'script', 'style', 'textarea', 'option', 'a'];
|
|
555
|
+
const LITERAL_DOLLAR_CLASS = 'literal-dollar';
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* KaTeX's auto-render pairs any two `$` in a block, so "$5 … $9" would become
|
|
559
|
+
* one formula. Each plain dollar sign (see math-dollars.mjs) is moved into a
|
|
560
|
+
* span of its own that auto-render skips; with no partner in its text node,
|
|
561
|
+
* it stays a dollar sign.
|
|
562
|
+
*/
|
|
563
|
+
function isolateLiteralDollars(container) {
|
|
564
|
+
const ignored = MATH_IGNORED_TAGS.join(',');
|
|
565
|
+
const walker = document.createTreeWalker(container, 4 /* NodeFilter.SHOW_TEXT */);
|
|
566
|
+
const textNodes = [];
|
|
567
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
568
|
+
if (node.nodeValue.includes('$') && !node.parentElement?.closest(ignored)) textNodes.push(node);
|
|
569
|
+
}
|
|
570
|
+
for (const node of textNodes) {
|
|
571
|
+
const parts = splitAtLiteralDollars(node.nodeValue);
|
|
572
|
+
if (!parts.some((part) => part.literal)) continue;
|
|
573
|
+
const fragment = document.createDocumentFragment();
|
|
574
|
+
for (const part of parts) {
|
|
575
|
+
if (!part.literal) {
|
|
576
|
+
fragment.appendChild(document.createTextNode(part.text));
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const span = document.createElement('span');
|
|
580
|
+
span.className = LITERAL_DOLLAR_CLASS;
|
|
581
|
+
span.textContent = part.text;
|
|
582
|
+
fragment.appendChild(span);
|
|
583
|
+
}
|
|
584
|
+
node.parentNode.replaceChild(fragment, node);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
553
588
|
function renderMathInHtml(html) {
|
|
554
589
|
const renderMathInElement = globalThis.renderMathInElement;
|
|
555
590
|
if (typeof renderMathInElement !== 'function' || !globalThis.document?.createElement) return html;
|
|
@@ -557,6 +592,7 @@ function renderMathInHtml(html) {
|
|
|
557
592
|
const container = document.createElement('div');
|
|
558
593
|
container.innerHTML = html;
|
|
559
594
|
try {
|
|
595
|
+
isolateLiteralDollars(container);
|
|
560
596
|
renderMathInElement(container, {
|
|
561
597
|
delimiters: [
|
|
562
598
|
{ left: '$$', right: '$$', display: true },
|
|
@@ -564,7 +600,8 @@ function renderMathInHtml(html) {
|
|
|
564
600
|
{ left: '\\(', right: '\\)', display: false },
|
|
565
601
|
{ left: '$', right: '$', display: false },
|
|
566
602
|
],
|
|
567
|
-
ignoredTags:
|
|
603
|
+
ignoredTags: MATH_IGNORED_TAGS,
|
|
604
|
+
ignoredClasses: [LITERAL_DOLLAR_CLASS],
|
|
568
605
|
throwOnError: false,
|
|
569
606
|
strict: 'ignore',
|
|
570
607
|
trust: false,
|
|
@@ -41,9 +41,11 @@ import { refreshPushSettingsSection } from './push-settings.js';
|
|
|
41
41
|
import { selectSettingsTab } from './settings-tabs.js';
|
|
42
42
|
import { refreshClaudeAuthSection } from './claude-auth-ui.js';
|
|
43
43
|
import { refreshGrokAuthSection } from './grok-auth-ui.js';
|
|
44
|
+
import { openClaudeCloudSettingsSection } from './claude-cloud-settings-ui.js';
|
|
44
45
|
import { refreshCliInstallSections } from './cli-install-ui.js';
|
|
45
46
|
import { refreshUpdateSection } from './update-ui.js';
|
|
46
47
|
import { refreshRemoteRelaysSection } from './remote-relays-settings.js';
|
|
48
|
+
import { refreshAgentSessionsSection } from './agent-sessions-settings-ui.js';
|
|
47
49
|
import {
|
|
48
50
|
getPreviews,
|
|
49
51
|
renderPreviewRowsInto,
|
|
@@ -437,7 +439,7 @@ function ensureClaudeSettingsInputTracking() {
|
|
|
437
439
|
}
|
|
438
440
|
|
|
439
441
|
function setClaudeSettingsControlsDisabled(disabled) {
|
|
440
|
-
for (const id of ['claude-model-input', 'claude-enabled-toggle', 'claude-save-btn']) {
|
|
442
|
+
for (const id of ['claude-model-input', 'claude-enabled-toggle', 'claude-attribution-select', 'claude-save-btn']) {
|
|
441
443
|
const element = document.getElementById(id);
|
|
442
444
|
if (element) element.disabled = disabled;
|
|
443
445
|
}
|
|
@@ -450,10 +452,15 @@ export function applyClaudeSettingsState(settings = {}, { resetInputs = false }
|
|
|
450
452
|
enabled: settings?.enabled === true,
|
|
451
453
|
model: String(settings?.model || claudeSettingsState.model || 'claude-sonnet-5').trim() || 'claude-sonnet-5',
|
|
452
454
|
models: Array.isArray(settings?.models) ? settings.models : claudeSettingsState.models,
|
|
455
|
+
attributionMode: ['oar', 'vanilla', 'off'].includes(settings?.attributionMode) ? settings.attributionMode : (claudeSettingsState.attributionMode || 'oar'),
|
|
456
|
+
attributionExample: String(settings?.attributionExample || claudeSettingsState.attributionExample || '').trim(),
|
|
453
457
|
};
|
|
454
458
|
const modelInput = document.getElementById('claude-model-input');
|
|
455
459
|
const toggle = document.getElementById('claude-enabled-toggle');
|
|
456
460
|
const status = document.getElementById('claude-settings-status');
|
|
461
|
+
const attributionSelect = document.getElementById('claude-attribution-select');
|
|
462
|
+
if (attributionSelect && (!claudeSettingsInputsDirty || resetInputs)) attributionSelect.value = claudeSettingsState.attributionMode;
|
|
463
|
+
renderClaudeAttributionExample(attributionSelect?.value || claudeSettingsState.attributionMode);
|
|
457
464
|
if (modelInput && (!claudeSettingsInputsDirty || resetInputs)) modelInput.value = claudeSettingsState.model;
|
|
458
465
|
if (resetInputs) claudeSettingsInputsDirty = false;
|
|
459
466
|
if (toggle) {
|
|
@@ -486,14 +493,36 @@ async function syncClaudeSettingsInputs() {
|
|
|
486
493
|
}
|
|
487
494
|
}
|
|
488
495
|
|
|
496
|
+
// What commits will carry for the chosen mode: the example trailer the relay
|
|
497
|
+
// rendered for the default model, Claude Code's own line, or nothing.
|
|
498
|
+
export function claudeAttributionExampleText(mode, example = '') {
|
|
499
|
+
if (mode === 'vanilla') return 'Commits keep Claude Code\'s own attribution (Co-Authored-By: Claude … <noreply@anthropic.com>).';
|
|
500
|
+
if (mode === 'off') return 'Commits and pull requests carry no attribution line.';
|
|
501
|
+
const trailer = String(example || '').trim();
|
|
502
|
+
return trailer ? `Commits end with: ${trailer}` : 'Commits end with: Co-authored-by: Open Agent Relay (<model>) <no-reply@oar.sh>';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function renderClaudeAttributionExample(mode) {
|
|
506
|
+
const example = document.getElementById('claude-attribution-example');
|
|
507
|
+
if (!example) return;
|
|
508
|
+
example.textContent = claudeAttributionExampleText(mode, claudeSettingsState.attributionExample);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
window.previewClaudeAttribution = (mode) => {
|
|
512
|
+
claudeSettingsInputsDirty = true;
|
|
513
|
+
renderClaudeAttributionExample(mode);
|
|
514
|
+
};
|
|
515
|
+
|
|
489
516
|
export async function saveClaudeSettings() {
|
|
490
517
|
if (claudeSettingsUpdateInFlight) return;
|
|
491
518
|
const modelInput = document.getElementById('claude-model-input');
|
|
492
519
|
const model = String(modelInput?.value || '').trim() || 'claude-sonnet-5';
|
|
520
|
+
const attributionSelect = document.getElementById('claude-attribution-select');
|
|
521
|
+
const attributionMode = ['oar', 'vanilla', 'off'].includes(attributionSelect?.value) ? attributionSelect.value : undefined;
|
|
493
522
|
claudeSettingsUpdateInFlight = true;
|
|
494
523
|
setClaudeSettingsControlsDisabled(true);
|
|
495
524
|
try {
|
|
496
|
-
const result = await updateClaudeSettings({ model });
|
|
525
|
+
const result = await updateClaudeSettings({ model, attributionMode });
|
|
497
526
|
if (!result) throw new Error('Failed to save Claude settings.');
|
|
498
527
|
applyClaudeSettingsState(result, { resetInputs: true });
|
|
499
528
|
showTransientRelayNotice(
|
|
@@ -1655,6 +1684,7 @@ export function openSettingsModal(tab, providerTab) {
|
|
|
1655
1684
|
ensureClaudeSettingsInputTracking();
|
|
1656
1685
|
void syncClaudeSettingsInputs();
|
|
1657
1686
|
void refreshClaudeAuthSection();
|
|
1687
|
+
void openClaudeCloudSettingsSection();
|
|
1658
1688
|
void refreshGrokAuthSection();
|
|
1659
1689
|
// Forced: the cached rows can be 30s stale, and a CLI installed from a shell
|
|
1660
1690
|
// in the meantime should show up the moment the panel opens. Sockets keep it
|
|
@@ -1678,6 +1708,7 @@ export function openSettingsModal(tab, providerTab) {
|
|
|
1678
1708
|
void refreshPushSettingsSection();
|
|
1679
1709
|
void refreshPreviewsSection();
|
|
1680
1710
|
void refreshRemoteRelaysSection();
|
|
1711
|
+
void refreshAgentSessionsSection();
|
|
1681
1712
|
void refreshFeatureFlagsSection();
|
|
1682
1713
|
void refreshUpdateSection();
|
|
1683
1714
|
modal?.classList.add('visible');
|
|
@@ -4,7 +4,7 @@ const SETTINGS_PROVIDER_TAB_STORAGE_KEY = 'copilot_settings_provider_tab';
|
|
|
4
4
|
const SETTINGS_TABS = ['general', 'providers', 'relays', 'previews', 'notifications', 'features'];
|
|
5
5
|
// Copilot leads: it is the default provider, and `SETTINGS_PROVIDER_TABS[0]`
|
|
6
6
|
// is also the sub-tab a first-time visitor lands on.
|
|
7
|
-
const SETTINGS_PROVIDER_TABS = ['copilot', 'openai', 'claude', 'grok', 'cursor'];
|
|
7
|
+
const SETTINGS_PROVIDER_TABS = ['copilot', 'openai', 'claude', 'claude-cloud', 'grok', 'cursor'];
|
|
8
8
|
|
|
9
9
|
let settingsTabsBound = false;
|
|
10
10
|
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
setConversationWatcherCount,
|
|
31
31
|
setUpdateState,
|
|
32
32
|
applySessionWorkerSteering,
|
|
33
|
+
refreshSummaryModal,
|
|
34
|
+
summaryModalState,
|
|
33
35
|
} from './store.js';
|
|
34
36
|
import { scheduleContextUsageRefresh } from './api-client.js';
|
|
35
37
|
import { publishStatusEvent, recordStatusEvent } from './status-store.mjs';
|
|
@@ -44,11 +46,16 @@ import { setConversationBackgroundTasks } from './background-tasks-view.mjs';
|
|
|
44
46
|
import { setPreviews } from './preview-cards.mjs';
|
|
45
47
|
import { setRemoteRelaysSnapshot } from './remote-relays-store.mjs';
|
|
46
48
|
import { REMOTE_RELAY_SOCKET_EVENT } from './remote-relay-shared.mjs';
|
|
49
|
+
import { AGENT_SESSIONS_SOCKET_EVENT } from './agent-sessions-settings.mjs';
|
|
50
|
+
import { applyAgentSessionsSettingsState, refreshAgentSessionsSection } from './agent-sessions-settings-ui.js';
|
|
47
51
|
import { applyClaudeAuthState } from './claude-auth-ui.js';
|
|
48
52
|
import { applyGrokAuthState } from './grok-auth-ui.js';
|
|
53
|
+
import { applyClaudeCloudSettingsState, refreshClaudeCloudSettingsState } from './claude-cloud-settings-ui.js';
|
|
54
|
+
import { applyClaudeCloudSessionEvent } from './claude-cloud-conversation-ui.js';
|
|
49
55
|
import { applyCliInstallState } from './cli-install-ui.js';
|
|
50
56
|
import { renderUpdateSection } from './update-ui.js';
|
|
51
57
|
import { applyHostSuspendState, applyRelayShutdownState } from './host-suspend-ui.js';
|
|
58
|
+
import { applyClaudeUsageLimit, applyUsageLimitPause } from './usage-limit-ui.js';
|
|
52
59
|
import {
|
|
53
60
|
showThinking,
|
|
54
61
|
removeThinking,
|
|
@@ -64,6 +71,7 @@ import {
|
|
|
64
71
|
applyConversationTurnStatus,
|
|
65
72
|
renderMessages,
|
|
66
73
|
appendMessage,
|
|
74
|
+
replaceRenderedMessage,
|
|
67
75
|
applyIncomingConversationDraftUpdate,
|
|
68
76
|
getRenderedConversationMessageFingerprints,
|
|
69
77
|
clearBubbleCancelState,
|
|
@@ -399,6 +407,26 @@ export async function connectSocket(overrideDeps) {
|
|
|
399
407
|
socket.on('relay_shutdown_state', (payload) => {
|
|
400
408
|
applyRelayShutdownState(payload || null);
|
|
401
409
|
});
|
|
410
|
+
// The Claude usage limit: a conversation's paused turn, and the account's
|
|
411
|
+
// latest report (also sent on connect).
|
|
412
|
+
socket.on('usage_limit_pause', (payload) => {
|
|
413
|
+
applyUsageLimitPause(payload || null);
|
|
414
|
+
});
|
|
415
|
+
socket.on('claude_usage_limit', (payload) => {
|
|
416
|
+
applyClaudeUsageLimit(payload || null);
|
|
417
|
+
});
|
|
418
|
+
// Claude Cloud settings (enable, default model, environment). The event
|
|
419
|
+
// carries the same shape as GET /api/settings/claude-cloud; one without a
|
|
420
|
+
// body is read as "something changed" and answered with a fresh read.
|
|
421
|
+
socket.on('claude_cloud_settings_updated', (payload) => {
|
|
422
|
+
if (payload && typeof payload === 'object') applyClaudeCloudSettingsState(payload);
|
|
423
|
+
else void refreshClaudeCloudSettingsState();
|
|
424
|
+
});
|
|
425
|
+
// A cloud session got its id, pushed a branch or reported a new cost: the
|
|
426
|
+
// conversation's `cloud` field changed, and with it the cloud line.
|
|
427
|
+
socket.on('claude_cloud_session', (payload) => {
|
|
428
|
+
if (applyClaudeCloudSessionEvent(payload || null)) deps?.syncChatTitleControls?.();
|
|
429
|
+
});
|
|
402
430
|
socket.on('grok_settings_updated', (payload) => {
|
|
403
431
|
deps?.applyGrokSettingsState?.(payload || {});
|
|
404
432
|
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
@@ -489,6 +517,20 @@ export async function connectSocket(overrideDeps) {
|
|
|
489
517
|
if (sourceMessageId) clearRelayStreamStateForMessage(sourceMessageId);
|
|
490
518
|
refreshSessionWorkerStatus().catch(() => {});
|
|
491
519
|
});
|
|
520
|
+
// A folder's commit attribution changed (any client): an open context modal
|
|
521
|
+
// re-reads it so its row and effect text match.
|
|
522
|
+
socket.on('workspace_root_attribution_updated', () => {
|
|
523
|
+
if (summaryModalState.kind === 'context') refreshSummaryModal().catch(() => {});
|
|
524
|
+
});
|
|
525
|
+
// A message the relay rewrote in place (the note while a session's worker
|
|
526
|
+
// cannot be started): same id, new text or kind. Not on the page yet (the
|
|
527
|
+
// conversation was opened after it was written): appended like any other.
|
|
528
|
+
socket.on('message_updated', ({ conversationId, messageId, message }) => {
|
|
529
|
+
if (!messageId || conversationId !== currentConvId || !message) return;
|
|
530
|
+
if (!replaceRenderedMessage(message, messageId)) {
|
|
531
|
+
appendMessage(message, isMessagesAtBottom(), messageId, false);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
492
534
|
socket.on('relay_question', ({ question }) => upsertRelayQuestion(question));
|
|
493
535
|
socket.on('relay_question_updated', ({ question }) => upsertRelayQuestion(question));
|
|
494
536
|
socket.on('relay_question_changed', () => {
|
|
@@ -512,6 +554,13 @@ export async function connectSocket(overrideDeps) {
|
|
|
512
554
|
socket.on(REMOTE_RELAY_SOCKET_EVENT, (payload) => {
|
|
513
555
|
setRemoteRelaysSnapshot(payload || null);
|
|
514
556
|
});
|
|
557
|
+
// Agent sessions (Settings → Relays): the event carries the same shape as
|
|
558
|
+
// GET /api/settings/agent-sessions; one without a body is answered with a
|
|
559
|
+
// fresh read.
|
|
560
|
+
socket.on(AGENT_SESSIONS_SOCKET_EVENT, (payload) => {
|
|
561
|
+
if (payload && typeof payload === 'object') applyAgentSessionsSettingsState(payload);
|
|
562
|
+
else void refreshAgentSessionsSection();
|
|
563
|
+
});
|
|
515
564
|
socket.on('relay_activity', ({ conversationId, messageId, text, subagentRunId, metadata }) => {
|
|
516
565
|
if (!messageId || !text) return;
|
|
517
566
|
const entry = {
|
|
@@ -728,6 +777,17 @@ export async function connectSocket(overrideDeps) {
|
|
|
728
777
|
}
|
|
729
778
|
renderConvList();
|
|
730
779
|
});
|
|
780
|
+
socket.on('conversation_archived', ({ conversationId, archived }) => {
|
|
781
|
+
const id = String(conversationId || '').trim();
|
|
782
|
+
if (!id) return;
|
|
783
|
+
const conversation = conversations[id];
|
|
784
|
+
if (conversation) conversation.archived = archived !== false;
|
|
785
|
+
renderConvList();
|
|
786
|
+
if (currentConvId === id) {
|
|
787
|
+
syncChatTitleControls();
|
|
788
|
+
syncComposerButtonState();
|
|
789
|
+
}
|
|
790
|
+
});
|
|
731
791
|
socket.on('conversation_deleted', ({ conversationId }) => {
|
|
732
792
|
delete conversations[conversationId];
|
|
733
793
|
for (const [id, question] of relayQuestions.entries()) {
|
|
@@ -3,6 +3,8 @@ import { recordCliLifecycleEvent, recordRelayLifecycleEvent } from './status-sto
|
|
|
3
3
|
import { resolveRelayDotState } from './relay-dot-state.mjs';
|
|
4
4
|
import { readRepoBrowserPreferences } from './repo-browser-preferences.mjs';
|
|
5
5
|
import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
6
|
+
import { nextSubagentStatus } from './subagent-fold.mjs';
|
|
7
|
+
import { useStrictStrikethrough } from './markdown-strikethrough.mjs';
|
|
6
8
|
|
|
7
9
|
function resolveAppBase() {
|
|
8
10
|
const configuredBase = typeof window.__COPILOT_APP_CONFIG?.basePath === 'string'
|
|
@@ -173,6 +175,7 @@ const CONVERSATION_SCROLL_STORAGE_PREFIX = 'copilot_message_scroll_';
|
|
|
173
175
|
|
|
174
176
|
if (globalThis.marked && typeof globalThis.marked.setOptions === 'function') {
|
|
175
177
|
globalThis.marked.setOptions({ breaks: true });
|
|
178
|
+
useStrictStrikethrough(globalThis.marked);
|
|
176
179
|
} else {
|
|
177
180
|
console.warn('[store] marked unavailable; markdown rendering will use plain-text fallback.');
|
|
178
181
|
}
|
|
@@ -1475,7 +1478,7 @@ export function upsertSubagentRun(payload) {
|
|
|
1475
1478
|
conversationId: String(payload?.conversationId || existing?.conversationId || '').trim() || null,
|
|
1476
1479
|
parentSubagentId: normalizeSubagentRunId(payload?.parentSubagentId) || existing?.parentSubagentId || null,
|
|
1477
1480
|
displayName: String(payload?.displayName || existing?.displayName || '').trim() || null,
|
|
1478
|
-
status:
|
|
1481
|
+
status: nextSubagentStatus({ reported: payload?.status, known: existing?.status, normalize: normalizeSubagentStatus }),
|
|
1479
1482
|
startedAt: existing?.startedAt || payload?.timestamp || now,
|
|
1480
1483
|
updatedAt: payload?.timestamp || now,
|
|
1481
1484
|
activities: existing?.activities || [],
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Whether a live subagent bubble is shown folded (header only), and what the
|
|
2
|
+
// header says while it is.
|
|
3
|
+
//
|
|
4
|
+
// A running turn can hold many subagents, and each keeps its whole transcript
|
|
5
|
+
// in its bubble: on a phone that is screens of text to scroll past to reach
|
|
6
|
+
// the turn's own output. So a bubble is folded from the start and stays folded
|
|
7
|
+
// until the user opens it; what they chose by hand holds, in either direction,
|
|
8
|
+
// for as long as the bubble lives. The folded header makes up for it with one
|
|
9
|
+
// line: how many steps the run has taken and the latest one.
|
|
10
|
+
|
|
11
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'dropped', 'done']);
|
|
12
|
+
|
|
13
|
+
export function isTerminalSubagentStatus(status) {
|
|
14
|
+
return TERMINAL_STATUSES.has(String(status || '').trim().toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `choice` is the user's own: 'open', 'folded', or nothing.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveSubagentFold({ choice = null } = {}) {
|
|
21
|
+
return choice !== 'open';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The choice a tap on the header records, given what the bubble shows now. */
|
|
25
|
+
export function toggledSubagentFoldChoice(folded) {
|
|
26
|
+
return folded ? 'open' : 'folded';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SUMMARY_MAX_CHARS = 160;
|
|
30
|
+
|
|
31
|
+
function oneLine(text) {
|
|
32
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The line a folded header shows: the number of steps (activity lines) and
|
|
37
|
+
* the latest one, or the end of what the run has written when it has taken
|
|
38
|
+
* no step yet. Empty when there is nothing to say.
|
|
39
|
+
*/
|
|
40
|
+
export function subagentFoldSummary({ activities = [], streamText = '' } = {}) {
|
|
41
|
+
const steps = (Array.isArray(activities) ? activities : []).map(oneLine).filter(Boolean);
|
|
42
|
+
if (steps.length) {
|
|
43
|
+
const latest = steps[steps.length - 1];
|
|
44
|
+
const count = steps.length === 1 ? '1 step' : `${steps.length} steps`;
|
|
45
|
+
return `${count} · ${latest}`.slice(0, SUMMARY_MAX_CHARS);
|
|
46
|
+
}
|
|
47
|
+
const lines = String(streamText || '').split(/\r?\n/).map(oneLine).filter(Boolean);
|
|
48
|
+
return lines.length ? lines[lines.length - 1].slice(0, SUMMARY_MAX_CHARS) : '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The status a run has after an update. Most updates report none (an
|
|
53
|
+
* activity line, a stream frame, a restored snapshot's activity list), and
|
|
54
|
+
* those must leave the status alone: reading "no status" as "running" turned
|
|
55
|
+
* every finished run back into a running one on the next line of any kind.
|
|
56
|
+
*/
|
|
57
|
+
export function nextSubagentStatus({ reported, known, normalize = (value) => value } = {}) {
|
|
58
|
+
const text = String(reported || '').trim();
|
|
59
|
+
if (text) return normalize(text);
|
|
60
|
+
return String(known || '').trim() || 'running';
|
|
61
|
+
}
|