@oar-sh/oar 0.9.6 → 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/CHANGELOG.md +172 -0
- package/README.md +176 -22
- 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 -2
- package/server/claude-worker/claude-session-process.mjs +33 -0
- package/server/claude-worker/claude-session-worker.mjs +6 -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/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +143 -4
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +344 -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 +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/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +83 -4
- 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/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/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +51 -0
- package/server/public/index.html +381 -7
- 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 +120 -15
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +502 -72
- package/server/server-runtime.mjs +258 -25
- 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/provider-reasoning-effort.mjs +3 -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-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +56 -1
- 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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// A context menu for the conversation list: right-click on a desktop, a long
|
|
2
|
+
// press on a phone. One menu at a time, closed by a pointer-down outside it,
|
|
3
|
+
// Escape, a scroll or a resize, like a native menu; flipped at the screen
|
|
4
|
+
// edge rather than clipped. The entries are buttons, so keyboard users get
|
|
5
|
+
// focus on the first one.
|
|
6
|
+
|
|
7
|
+
const LONG_PRESS_MS = 500;
|
|
8
|
+
const LONG_PRESS_SLOP_PX = 10;
|
|
9
|
+
|
|
10
|
+
/** A horizontal rule between groups. */
|
|
11
|
+
export const MENU_SEPARATOR = null;
|
|
12
|
+
|
|
13
|
+
let closeOpenMenu = null;
|
|
14
|
+
|
|
15
|
+
function esc(value) {
|
|
16
|
+
return String(value ?? '')
|
|
17
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Close the open menu, if any. */
|
|
21
|
+
export function closeContextMenu() {
|
|
22
|
+
closeOpenMenu?.();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Open a menu at viewport coordinates. `items`: `{ label, onSelect, danger?,
|
|
27
|
+
* disabled?, id? }`, or MENU_SEPARATOR between groups. Returns the menu node.
|
|
28
|
+
*/
|
|
29
|
+
export function openContextMenu(x, y, items, { documentRef = document, windowRef = window } = {}) {
|
|
30
|
+
closeOpenMenu?.();
|
|
31
|
+
const menu = documentRef.createElement('div');
|
|
32
|
+
menu.className = 'context-menu';
|
|
33
|
+
menu.setAttribute('role', 'menu');
|
|
34
|
+
|
|
35
|
+
const close = () => {
|
|
36
|
+
if (closeOpenMenu !== close) return;
|
|
37
|
+
closeOpenMenu = null;
|
|
38
|
+
documentRef.removeEventListener('pointerdown', onOutside, true);
|
|
39
|
+
documentRef.removeEventListener('keydown', onKey, true);
|
|
40
|
+
windowRef.removeEventListener('resize', close);
|
|
41
|
+
windowRef.removeEventListener('scroll', close, true);
|
|
42
|
+
menu.remove();
|
|
43
|
+
};
|
|
44
|
+
const onOutside = (event) => {
|
|
45
|
+
if (!menu.contains(event.target)) close();
|
|
46
|
+
};
|
|
47
|
+
const onKey = (event) => {
|
|
48
|
+
if (event.key === 'Escape') {
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
close();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
|
54
|
+
const buttons = [...menu.querySelectorAll('button:not([disabled])')];
|
|
55
|
+
if (!buttons.length) return;
|
|
56
|
+
const index = buttons.indexOf(documentRef.activeElement);
|
|
57
|
+
const next = event.key === 'ArrowDown'
|
|
58
|
+
? buttons[(index + 1) % buttons.length]
|
|
59
|
+
: buttons[(index <= 0 ? buttons.length : index) - 1];
|
|
60
|
+
next.focus();
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
if (item === MENU_SEPARATOR) {
|
|
66
|
+
const rule = documentRef.createElement('div');
|
|
67
|
+
rule.className = 'context-menu-sep';
|
|
68
|
+
rule.setAttribute('role', 'separator');
|
|
69
|
+
menu.append(rule);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!item) continue;
|
|
73
|
+
const entry = documentRef.createElement('button');
|
|
74
|
+
entry.type = 'button';
|
|
75
|
+
entry.className = `context-menu-item${item.danger ? ' context-menu-item-danger' : ''}`;
|
|
76
|
+
entry.setAttribute('role', 'menuitem');
|
|
77
|
+
if (item.id) entry.dataset.menuItem = String(item.id);
|
|
78
|
+
entry.innerHTML = esc(item.label);
|
|
79
|
+
entry.disabled = item.disabled === true;
|
|
80
|
+
entry.addEventListener('click', (event) => {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
event.stopPropagation();
|
|
83
|
+
close();
|
|
84
|
+
item.onSelect?.();
|
|
85
|
+
});
|
|
86
|
+
menu.append(entry);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
documentRef.body.append(menu);
|
|
90
|
+
// Positioned after insertion so the measured size is the real one.
|
|
91
|
+
const rect = menu.getBoundingClientRect();
|
|
92
|
+
const left = Math.max(4, Math.min(x, windowRef.innerWidth - rect.width - 4));
|
|
93
|
+
const top = Math.max(4, Math.min(y, windowRef.innerHeight - rect.height - 4));
|
|
94
|
+
menu.style.left = `${left}px`;
|
|
95
|
+
menu.style.top = `${top}px`;
|
|
96
|
+
|
|
97
|
+
closeOpenMenu = close;
|
|
98
|
+
documentRef.addEventListener('pointerdown', onOutside, true);
|
|
99
|
+
documentRef.addEventListener('keydown', onKey, true);
|
|
100
|
+
windowRef.addEventListener('resize', close);
|
|
101
|
+
windowRef.addEventListener('scroll', close, true);
|
|
102
|
+
queueMicrotask(() => menu.querySelector('button:not([disabled])')?.focus());
|
|
103
|
+
return menu;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Long-press as the touch equivalent of right-click, on every descendant of
|
|
108
|
+
* `root` that matches `selector`. Android fires no `contextmenu` for a long
|
|
109
|
+
* press on a non-selectable element. Cancelled by movement, so it never fires
|
|
110
|
+
* mid-scroll; mouse pointers are left to the real `contextmenu` event.
|
|
111
|
+
* `handler(target, x, y)`.
|
|
112
|
+
*/
|
|
113
|
+
export function bindLongPress(root, selector, handler, { setTimeoutImpl = setTimeout, clearTimeoutImpl = clearTimeout } = {}) {
|
|
114
|
+
let timer = null;
|
|
115
|
+
let startX = 0;
|
|
116
|
+
let startY = 0;
|
|
117
|
+
const cancel = () => {
|
|
118
|
+
if (timer !== null) clearTimeoutImpl(timer);
|
|
119
|
+
timer = null;
|
|
120
|
+
};
|
|
121
|
+
root.addEventListener('pointerdown', (event) => {
|
|
122
|
+
if (event.pointerType === 'mouse') return;
|
|
123
|
+
const target = event.target?.closest?.(selector);
|
|
124
|
+
if (!target || !root.contains(target)) return;
|
|
125
|
+
startX = event.clientX;
|
|
126
|
+
startY = event.clientY;
|
|
127
|
+
cancel();
|
|
128
|
+
timer = setTimeoutImpl(() => {
|
|
129
|
+
timer = null;
|
|
130
|
+
handler(target, startX, startY);
|
|
131
|
+
}, LONG_PRESS_MS);
|
|
132
|
+
}, { passive: true });
|
|
133
|
+
root.addEventListener('pointermove', (event) => {
|
|
134
|
+
if (Math.abs(event.clientX - startX) > LONG_PRESS_SLOP_PX || Math.abs(event.clientY - startY) > LONG_PRESS_SLOP_PX) cancel();
|
|
135
|
+
}, { passive: true });
|
|
136
|
+
for (const name of ['pointerup', 'pointercancel', 'pointerleave']) {
|
|
137
|
+
root.addEventListener(name, cancel, { passive: true });
|
|
138
|
+
}
|
|
139
|
+
return cancel;
|
|
140
|
+
}
|
|
@@ -55,6 +55,10 @@ export function buildContextTierOptions({
|
|
|
55
55
|
const meta = metadata && typeof metadata === 'object' ? metadata : {};
|
|
56
56
|
const defaultLabel = tokenLabel(defaultWindowTokens(meta));
|
|
57
57
|
const provider = String(providerType || '').trim().toLowerCase();
|
|
58
|
+
// A cloud session has no tier to choose and its window is not in the merged
|
|
59
|
+
// metadata either (that number is Copilot's): one blank default, as for a
|
|
60
|
+
// Claude model whose window is unknown.
|
|
61
|
+
if (provider === 'claude-cloud') return [{ value: 'default', label: UNKNOWN_WINDOW_LABEL }];
|
|
58
62
|
const claudeTierList = provider === 'claude' ? claudeTiersForModel(modelId, claudeTiers) : null;
|
|
59
63
|
if (claudeTierList) {
|
|
60
64
|
return claudeTierList
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The header's "via agent · “title”" line: the conversation on screen was
|
|
2
|
+
// started by an agent in another conversation on this relay, and the line
|
|
3
|
+
// opens that conversation. Derived from the conversation record alone, so the
|
|
4
|
+
// one sync function is idempotent and runs with every header sync. A
|
|
5
|
+
// conversation another relay's agent created has no header line (its sidebar
|
|
6
|
+
// marker is all there is), and neither has one a person started.
|
|
7
|
+
|
|
8
|
+
import { conversations, currentConvId, IS_SHARED_VIEW } from './store.js';
|
|
9
|
+
import { renderConversationHeaderOriginHtml } from './remote-relay-origin-view.mjs';
|
|
10
|
+
|
|
11
|
+
let renderedHtml = '';
|
|
12
|
+
|
|
13
|
+
export function syncConversationOriginHeader() {
|
|
14
|
+
const line = document.getElementById('chat-title-origin');
|
|
15
|
+
if (!line) return;
|
|
16
|
+
const id = String(currentConvId || '').trim();
|
|
17
|
+
const conversation = id ? (conversations[id] || null) : null;
|
|
18
|
+
const html = conversation
|
|
19
|
+
? renderConversationHeaderOriginHtml(conversation, { conversations, linkable: !IS_SHARED_VIEW })
|
|
20
|
+
: '';
|
|
21
|
+
// This runs on every list render; the button must not be rebuilt under a tap.
|
|
22
|
+
if (html !== renderedHtml) {
|
|
23
|
+
line.innerHTML = html;
|
|
24
|
+
renderedHtml = html;
|
|
25
|
+
}
|
|
26
|
+
line.hidden = !html;
|
|
27
|
+
}
|
|
@@ -23,23 +23,39 @@ export function isConversationUsingGrokProvider(conversation = null) {
|
|
|
23
23
|
return resolveConversationProviderType(conversation) === 'grok';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
26
|
+
// Its own provider, never a flavour of `claude`: the session runs in a sandbox
|
|
27
|
+
// at Anthropic, so nothing that is true of a local Claude worker (modes,
|
|
28
|
+
// effort, steering, a working directory) may be assumed for it.
|
|
29
|
+
export function isConversationUsingClaudeCloudProvider(conversation = null) {
|
|
30
|
+
return resolveConversationProviderType(conversation) === 'claude-cloud';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The marker that tells a cloud chat from a local Claude one at a glance.
|
|
34
|
+
const CLAUDE_CLOUD_MARKER = '☁';
|
|
35
|
+
|
|
36
|
+
function conversationProviderName(conversation = null) {
|
|
29
37
|
const providerType = resolveConversationProviderType(conversation);
|
|
30
38
|
if (providerType === 'openai') return 'OpenAI';
|
|
31
39
|
if (providerType === 'claude') return 'Claude';
|
|
40
|
+
if (providerType === 'claude-cloud') return 'Claude Cloud';
|
|
32
41
|
if (providerType === 'cursor') return 'Cursor';
|
|
33
42
|
if (providerType === 'grok') return 'Grok';
|
|
34
43
|
if (providerType === 'github' || providerType === 'github-copilot') return 'Copilot';
|
|
35
44
|
return '';
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
// An unresolved provider stays unlabelled rather than defaulting to Copilot, so
|
|
48
|
+
// a conversation the client has not loaded yet cannot claim the wrong provider.
|
|
49
|
+
export function conversationProviderIndicatorLabel(conversation = null) {
|
|
50
|
+
const name = conversationProviderName(conversation);
|
|
51
|
+
return isConversationUsingClaudeCloudProvider(conversation) ? `${CLAUDE_CLOUD_MARKER} ${name}` : name;
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
// A question card names the runtime that asks, the way the list pill names it.
|
|
39
55
|
// Unresolved reads plain "Question": every card used to say "Copilot question",
|
|
40
56
|
// Claude's and Cursor's included.
|
|
41
57
|
export function questionCardHeadLabel(conversation = null) {
|
|
42
|
-
const provider =
|
|
58
|
+
const provider = conversationProviderName(conversation);
|
|
43
59
|
return provider ? `${provider} question` : 'Question';
|
|
44
60
|
}
|
|
45
61
|
|
|
@@ -50,6 +66,7 @@ const SESSION_LOCK_LABELS = {
|
|
|
50
66
|
claude: 'Claude SDK',
|
|
51
67
|
cursor: 'Cursor SDK',
|
|
52
68
|
grok: 'Grok',
|
|
69
|
+
'claude-cloud': `${CLAUDE_CLOUD_MARKER} Claude Cloud`,
|
|
53
70
|
};
|
|
54
71
|
|
|
55
72
|
export function isOpenAIImageModelId(modelId = '') {
|
|
@@ -66,6 +83,7 @@ export function sessionLockProviderKey({ providerType = '', model = '' } = {}) {
|
|
|
66
83
|
}
|
|
67
84
|
if (normalized === 'openai-image' || normalized === 'openai-image-byok') return 'openai-image';
|
|
68
85
|
if (normalized === 'claude' || normalized === 'claude-agent-sdk' || normalized === 'anthropic') return 'claude';
|
|
86
|
+
if (normalized === 'claude-cloud') return 'claude-cloud';
|
|
69
87
|
if (normalized === 'cursor') return 'cursor';
|
|
70
88
|
if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
|
|
71
89
|
if (normalized === 'github' || normalized === 'github-copilot') return 'github';
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
closeSummaryModal,
|
|
50
50
|
} from './store.js';
|
|
51
51
|
import { getPendingQuestionCountsByConversation } from './ask-user-view.js';
|
|
52
|
-
import { sendMessage as sendMessageApi, cancelConversationTurn, cancelQueuedConversationTurn, cancelSubagentRun, compactConversation as compactConversationApi, scheduleContextUsageRefresh, loadConversation as loadConversationApi, loadSharedConversation, updateConversationDraft as updateConversationDraftApi, updateMessageShareVisibility } from './api-client.js';
|
|
52
|
+
import { sendMessage as sendMessageApi, cancelConversationTurn, cancelQueuedConversationTurn, cancelSubagentRun, compactConversation as compactConversationApi, scheduleContextUsageRefresh, loadConversation as loadConversationApi, loadSharedConversation, updateConversationDraft as updateConversationDraftApi, updateMessageShareVisibility, launchSessionWorker } from './api-client.js';
|
|
53
53
|
import { enqueueOutboxRequest, registerOutboxSync } from './sync-outbox.mjs';
|
|
54
54
|
import { linkifyWorkspaceMentionsInNode, parseAppFileHref, renderMarkdownPreview, rewriteLocalAssetUrlsInNode } from './router.js';
|
|
55
55
|
import { renderAttachmentMarkup, removeComposerAttachments, uploadAttachments, setComposerAttachments, setRepoBrowserSessionInfo, openDriveFilePreview, openWorkspaceFilePreview, openUploadedAttachmentViewer } from './attachments-view.js';
|
|
@@ -546,6 +546,7 @@ function syncSendButtonState() {
|
|
|
546
546
|
steeringSupported: conversationSupportsSteering(currentConvId),
|
|
547
547
|
steeringHeld: steeringHold.held,
|
|
548
548
|
steeringHoldReason: steeringHold.reason,
|
|
549
|
+
archived: conversations[String(currentConvId || '').trim()]?.archived === true,
|
|
549
550
|
});
|
|
550
551
|
btn.disabled = state.disabled;
|
|
551
552
|
btn.dataset.action = state.action;
|
|
@@ -1283,6 +1284,15 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1283
1284
|
const steerMarkerClass = msg.role === 'assistant' ? STEER_MARKER_CLASS_BY_KIND[messageKind] : null;
|
|
1284
1285
|
if (steerMarkerClass) div.classList.add(steerMarkerClass);
|
|
1285
1286
|
const isStoppedSteer = msg.role === 'assistant' && messageKind === 'stopped';
|
|
1287
|
+
// The relay's note while this session's worker cannot be started
|
|
1288
|
+
// (worker-launch-note-service.mjs), rewritten in place as the episode goes
|
|
1289
|
+
// on. Once the relay has stopped trying, the note carries Retry: a launch
|
|
1290
|
+
// for once the cause is fixed.
|
|
1291
|
+
const isWorkerLaunchNote = msg.role === 'assistant' && messageKind.startsWith('worker-launch-');
|
|
1292
|
+
if (isWorkerLaunchNote) div.classList.add('msg-worker-launch', `msg-${messageKind}`);
|
|
1293
|
+
const workerLaunchActionsHtml = (!IS_SHARED_VIEW && msgId && messageKind === 'worker-launch-stopped')
|
|
1294
|
+
? `<div class="msg-bubble-actions"><button type="button" class="bubble-action-btn" data-action="retry-worker-launch" data-message-id="${escHtml(msgId)}" title="Start the worker for this conversation again now">Retry</button></div>`
|
|
1295
|
+
: '';
|
|
1286
1296
|
// A turn answered by a different provider than the conversation is bound to
|
|
1287
1297
|
// (e.g. the Copilot relay answering a Cursor conversation) must be visible,
|
|
1288
1298
|
// not silent: it ran on another plan than the header indicates.
|
|
@@ -1404,13 +1414,14 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1404
1414
|
? `<div class="msg-bubble-actions"><button type="button" class="bubble-action-btn" data-action="resend-stopped-steer" data-message-id="${escHtml(msgId)}" title="Send this message again as a new turn"${resendState ? ' disabled' : ''}>${resendLabel}</button></div>`
|
|
1405
1415
|
: '';
|
|
1406
1416
|
|
|
1407
|
-
// Shared viewers get the badge without the link to the other relay
|
|
1417
|
+
// Shared viewers get the badge without the link to the other relay (or, for
|
|
1418
|
+
// an agent on this relay, to the conversation it runs in).
|
|
1408
1419
|
const originBadgeHtml = remoteOrigin
|
|
1409
|
-
? renderRemoteOriginBadgeHtml(remoteOrigin, { linkable: !IS_SHARED_VIEW })
|
|
1420
|
+
? renderRemoteOriginBadgeHtml(remoteOrigin, { linkable: !IS_SHARED_VIEW, conversations })
|
|
1410
1421
|
: '';
|
|
1411
1422
|
|
|
1412
1423
|
div.innerHTML = `
|
|
1413
|
-
<div class="${bubbleClass}">${shareVisibilityActionHtml}${thoughtsHtml}${originBadgeHtml}${content}${relayErrorCtaHtml}${attachmentHtml}${activityHtml}${subagentHtml}${workflowRunsHtml}${previewCardsHtml}${userBubbleActionsHtml}${stoppedSteerActionsHtml}</div>
|
|
1424
|
+
<div class="${bubbleClass}">${shareVisibilityActionHtml}${thoughtsHtml}${originBadgeHtml}${content}${relayErrorCtaHtml}${attachmentHtml}${activityHtml}${subagentHtml}${workflowRunsHtml}${previewCardsHtml}${userBubbleActionsHtml}${stoppedSteerActionsHtml}${workerLaunchActionsHtml}</div>
|
|
1414
1425
|
<div class="msg-label">${label}${modelTag}${reasoningTag}${modeTag}${autoTag}${continuationTag}${crossProviderTag}${usageTurnTag}${usageRemainingTag}${usageStaleTag} · ${fmtDate(msg.timestamp)}</div>`;
|
|
1415
1426
|
|
|
1416
1427
|
const bubble = div.querySelector('.msg-bubble');
|
|
@@ -1812,6 +1823,19 @@ function openStopTurnConfirmation(conversationId, messageId) {
|
|
|
1812
1823
|
}, { once: true });
|
|
1813
1824
|
}
|
|
1814
1825
|
|
|
1826
|
+
/** The Stop confirmation for a conversation's running turn, from outside the bubble (the sidebar menu). */
|
|
1827
|
+
export function openStopTurnConfirmationForConversation(conversationId) {
|
|
1828
|
+
const turn = getActiveTurnForConversation(conversationId);
|
|
1829
|
+
if (!turn?.messageId) return false;
|
|
1830
|
+
openStopTurnConfirmation(conversationId, turn.messageId);
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/** Whether a turn of this conversation is running as far as this page knows. */
|
|
1835
|
+
export function conversationHasActiveTurn(conversationId) {
|
|
1836
|
+
return !!getActiveTurnForConversation(conversationId);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1815
1839
|
export function collapseThinkingThoughts() {
|
|
1816
1840
|
const panel = document.getElementById('thinking-thoughts');
|
|
1817
1841
|
if (!(panel instanceof HTMLDetailsElement)) return;
|
|
@@ -2773,6 +2797,55 @@ function releaseStoppedSteerResendsOf(messageId) {
|
|
|
2773
2797
|
}
|
|
2774
2798
|
}
|
|
2775
2799
|
|
|
2800
|
+
// Retry on the worker-launch note: ask the relay for a launch now. The relay
|
|
2801
|
+
// rewrites the note itself — the worker started, or the launches are
|
|
2802
|
+
// exhausted again — so the button only has to report a refused request.
|
|
2803
|
+
async function retryWorkerLaunch(conversationId, noteId) {
|
|
2804
|
+
const conversationKey = String(conversationId || '').trim();
|
|
2805
|
+
const id = String(noteId || '').trim();
|
|
2806
|
+
if (!conversationKey || !id) return;
|
|
2807
|
+
const btn = document.querySelector(`.bubble-action-btn[data-message-id="${CSS.escape(id)}"][data-action="retry-worker-launch"]`);
|
|
2808
|
+
if (btn?.disabled) return;
|
|
2809
|
+
const sdkSessionId = String(conversations[conversationKey]?.sdkSessionId || '').trim();
|
|
2810
|
+
if (!sdkSessionId) {
|
|
2811
|
+
showTransientRelayNotice('This conversation has no session to start a worker for.');
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
if (btn) {
|
|
2815
|
+
btn.disabled = true;
|
|
2816
|
+
btn.textContent = 'Retrying…';
|
|
2817
|
+
}
|
|
2818
|
+
try {
|
|
2819
|
+
// apiFetch answers null for a refused request (the route says 409 with
|
|
2820
|
+
// the reason) as for a network failure: both re-arm the button.
|
|
2821
|
+
const result = await launchSessionWorker(sdkSessionId);
|
|
2822
|
+
if (!result || result.ok === false) throw new Error(String(result?.error || 'the relay refused the launch'));
|
|
2823
|
+
} catch (error) {
|
|
2824
|
+
showTransientRelayNotice(`The worker could not be started: ${error?.message || error}`);
|
|
2825
|
+
if (btn) {
|
|
2826
|
+
btn.disabled = false;
|
|
2827
|
+
btn.textContent = 'Retry';
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// A message the relay rewrote in place (the worker-launch note): the bubble is
|
|
2833
|
+
// rebuilt where it stands, so the transcript keeps its order. False when the
|
|
2834
|
+
// message is not on the page.
|
|
2835
|
+
export function replaceRenderedMessage(msg, msgId) {
|
|
2836
|
+
const el = getMessagesElement();
|
|
2837
|
+
const id = String(msgId || '').trim();
|
|
2838
|
+
if (!el || !id) return false;
|
|
2839
|
+
const existing = el.querySelector(`[data-message-id="${CSS.escape(id)}"]`);
|
|
2840
|
+
if (!existing) return false;
|
|
2841
|
+
const placeholder = document.createComment('replacing');
|
|
2842
|
+
existing.replaceWith(placeholder);
|
|
2843
|
+
seenMessageIds.delete(id);
|
|
2844
|
+
const fresh = createMessageNode(msg, id, true);
|
|
2845
|
+
placeholder.replaceWith(fresh || existing);
|
|
2846
|
+
return Boolean(fresh);
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2776
2849
|
async function resendStoppedSteer(conversationId, markerId) {
|
|
2777
2850
|
const conversationKey = String(conversationId || '').trim();
|
|
2778
2851
|
const id = String(markerId || '').trim();
|
|
@@ -3112,6 +3185,12 @@ function handleBubbleActionClick(event) {
|
|
|
3112
3185
|
void resendStoppedSteer(currentConvId, messageId);
|
|
3113
3186
|
}
|
|
3114
3187
|
|
|
3188
|
+
if (action === 'retry-worker-launch' && messageId) {
|
|
3189
|
+
event.preventDefault();
|
|
3190
|
+
event.stopPropagation();
|
|
3191
|
+
void retryWorkerLaunch(currentConvId, messageId);
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3115
3194
|
if (action === 'stop-subagent' && subagentRunId) {
|
|
3116
3195
|
event.preventDefault();
|
|
3117
3196
|
event.stopPropagation();
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
buildKnownCwdOptions as buildKnownCwdOptionsFromInputs,
|
|
24
24
|
normalizeKnownCwdPath,
|
|
25
25
|
} from './known-cwd-options.mjs';
|
|
26
|
+
import { buildCloudLineModel, cloudHeaderLabel, isClaudeCloudConversation } from './claude-cloud-ui.mjs';
|
|
26
27
|
|
|
27
28
|
export { normalizeKnownCwdPath };
|
|
28
29
|
|
|
@@ -462,6 +463,17 @@ export function syncChatHeaderWorkspaceLabel() {
|
|
|
462
463
|
const labelEl = document.getElementById('chat-title-cwd');
|
|
463
464
|
if (!labelEl) return;
|
|
464
465
|
const convId = String(currentConvId || '').trim();
|
|
466
|
+
const conversation = convId ? (conversations?.[convId] || null) : null;
|
|
467
|
+
if (isClaudeCloudConversation(conversation)) {
|
|
468
|
+
// A cloud chat runs on a clone at Anthropic, so the line names the
|
|
469
|
+
// repository instead of a folder on the relay host.
|
|
470
|
+
const label = cloudHeaderLabel(conversation.cloud);
|
|
471
|
+
labelEl.hidden = !label;
|
|
472
|
+
labelEl.textContent = label;
|
|
473
|
+
if (label) labelEl.title = buildCloudLineModel(conversation.cloud)?.repoUrl || label;
|
|
474
|
+
else labelEl.removeAttribute('title');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
465
477
|
const cwd = getSelectedConversationCurrentCwd();
|
|
466
478
|
if (!convId || !cwd) {
|
|
467
479
|
labelEl.hidden = true;
|