@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,110 @@
|
|
|
1
|
+
// What a Claude Cloud conversation looks like once it is open: the cloud line
|
|
2
|
+
// above the composer (repository, branch, the session on claude.ai, one link
|
|
3
|
+
// per pushed branch, cost), the controls that do not apply to a cloud session
|
|
4
|
+
// taken away, and an attach button that only offers images.
|
|
5
|
+
//
|
|
6
|
+
// Everything is derived from the conversation record (`runtimeProviderType`
|
|
7
|
+
// and the server's `cloud` payload field), so the one sync function below is
|
|
8
|
+
// idempotent and is simply called whenever the header is synced. A
|
|
9
|
+
// conversation of any other provider gets every element back exactly as it was.
|
|
10
|
+
|
|
11
|
+
import { conversations, currentConvId, IS_SHARED_VIEW } from './store.js';
|
|
12
|
+
import { openExternalNavigation } from './external-link-policy.mjs';
|
|
13
|
+
import {
|
|
14
|
+
CLAUDE_CLOUD_ATTACH_ACCEPT,
|
|
15
|
+
CLAUDE_CLOUD_ATTACH_HINT,
|
|
16
|
+
buildCloudLineModel,
|
|
17
|
+
isClaudeCloudConversation,
|
|
18
|
+
mergeCloudSessionUpdate,
|
|
19
|
+
renderCloudLineHtml,
|
|
20
|
+
} from './claude-cloud-ui.mjs';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_ATTACH_TITLE = 'Attach file';
|
|
23
|
+
|
|
24
|
+
let renderedLineHtml = '';
|
|
25
|
+
|
|
26
|
+
function currentConversation() {
|
|
27
|
+
const id = String(currentConvId || '').trim();
|
|
28
|
+
return id ? (conversations[id] || null) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isCurrentConversationClaudeCloud() {
|
|
32
|
+
return !IS_SHARED_VIEW && isClaudeCloudConversation(currentConversation());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Hides an element for cloud chats and gives it back afterwards, without ever
|
|
36
|
+
// un-hiding something another owner (shared view, feature gates) had hidden.
|
|
37
|
+
function setHiddenForCloud(element, hide) {
|
|
38
|
+
if (!element) return;
|
|
39
|
+
if (hide) {
|
|
40
|
+
if (!element.hidden) {
|
|
41
|
+
element.hidden = true;
|
|
42
|
+
element.dataset.cloudHidden = '1';
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (element.dataset.cloudHidden === '1') {
|
|
47
|
+
element.hidden = false;
|
|
48
|
+
delete element.dataset.cloudHidden;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function syncClaudeCloudConversationUi() {
|
|
53
|
+
const conversation = currentConversation();
|
|
54
|
+
const isCloud = isCurrentConversationClaudeCloud();
|
|
55
|
+
document.body.classList.toggle('claude-cloud-conversation', isCloud);
|
|
56
|
+
|
|
57
|
+
const line = document.getElementById('cloud-session-line');
|
|
58
|
+
const lineHtml = isCloud ? renderCloudLineHtml(conversation?.cloud) : '';
|
|
59
|
+
if (line) {
|
|
60
|
+
// This runs on every list render; the links must not be rebuilt under a tap.
|
|
61
|
+
if (lineHtml !== renderedLineHtml) {
|
|
62
|
+
line.innerHTML = lineHtml;
|
|
63
|
+
renderedLineHtml = lineHtml;
|
|
64
|
+
}
|
|
65
|
+
line.hidden = !lineHtml;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The session has no working directory on the relay host to change.
|
|
69
|
+
setHiddenForCloud(document.getElementById('chat-menu-change-cwd'), isCloud);
|
|
70
|
+
|
|
71
|
+
const openSessionButton = document.getElementById('chat-menu-open-cloud-session');
|
|
72
|
+
if (openSessionButton) {
|
|
73
|
+
openSessionButton.hidden = !(isCloud && buildCloudLineModel(conversation?.cloud)?.sessionUrl);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fileInput = document.getElementById('image-input');
|
|
77
|
+
if (fileInput) {
|
|
78
|
+
if (isCloud) fileInput.setAttribute('accept', CLAUDE_CLOUD_ATTACH_ACCEPT);
|
|
79
|
+
else fileInput.removeAttribute('accept');
|
|
80
|
+
}
|
|
81
|
+
const attachButton = document.getElementById('attach-btn');
|
|
82
|
+
if (attachButton) {
|
|
83
|
+
const title = isCloud ? CLAUDE_CLOUD_ATTACH_HINT : DEFAULT_ATTACH_TITLE;
|
|
84
|
+
if (attachButton.title !== title) attachButton.title = title;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The `claude_cloud_session` socket event: the worker reported a session id, a
|
|
90
|
+
* push or a new cost. Returns true when the record of a known conversation
|
|
91
|
+
* changed; redrawing is left to the caller's usual header sync.
|
|
92
|
+
*/
|
|
93
|
+
export function applyClaudeCloudSessionEvent(payload = null) {
|
|
94
|
+
const id = String(payload?.conversationId || '').trim();
|
|
95
|
+
if (!id || !conversations[id]) return false;
|
|
96
|
+
conversations[id] = {
|
|
97
|
+
...conversations[id],
|
|
98
|
+
cloud: mergeCloudSessionUpdate(conversations[id].cloud, payload?.cloud),
|
|
99
|
+
};
|
|
100
|
+
if (id === String(currentConvId || '').trim()) syncClaudeCloudConversationUi();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function openCurrentCloudSession(onFallback = null) {
|
|
105
|
+
if (!isCurrentConversationClaudeCloud()) return false;
|
|
106
|
+
const sessionUrl = buildCloudLineModel(currentConversation()?.cloud)?.sessionUrl;
|
|
107
|
+
if (!sessionUrl) return false;
|
|
108
|
+
openExternalNavigation(sessionUrl, onFallback);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// Settings → Providers → Claude Cloud, and the client's copy of that tab's
|
|
2
|
+
// settings (the New Chat modal and the composer read their model list from it).
|
|
3
|
+
//
|
|
4
|
+
// One payload shape everywhere: GET and POST /api/settings/claude-cloud and the
|
|
5
|
+
// `claude_cloud_settings_updated` socket event all carry
|
|
6
|
+
// { enabled, defaultModel, environmentId, environments, environmentsError,
|
|
7
|
+
// account, token, models }, so every source goes through
|
|
8
|
+
// applyClaudeCloudSettingsState and rendering is idempotent.
|
|
9
|
+
//
|
|
10
|
+
// The tab is always there so the provider can be switched on from it. A relay
|
|
11
|
+
// that does not know the routes yet answers nothing: the tab then says so in
|
|
12
|
+
// plain muted text and the controls stay off — not an error, just an older relay.
|
|
13
|
+
//
|
|
14
|
+
// Secret hygiene: the payload never carries the login token, only where it was
|
|
15
|
+
// read from and when it expires.
|
|
16
|
+
|
|
17
|
+
import { showTransientRelayNotice } from './store.js';
|
|
18
|
+
import { loadClaudeCloudSettings, updateClaudeCloudSettings } from './api-client.js';
|
|
19
|
+
import { selectSettingsTab } from './settings-tabs.js';
|
|
20
|
+
import { humanizeModelLabel } from './model-selector-options.mjs';
|
|
21
|
+
import { claudeCloudDefaultModel, claudeCloudModelIds } from './claude-cloud-ui.mjs';
|
|
22
|
+
|
|
23
|
+
// null until the first successful read: "never answered" is not "disabled".
|
|
24
|
+
let settingsState = null;
|
|
25
|
+
let settingsLoadFailed = false;
|
|
26
|
+
let updateInFlight = false;
|
|
27
|
+
let inputsDirty = false;
|
|
28
|
+
// A refused save or toggle, shown in the status line until the next state.
|
|
29
|
+
let localError = '';
|
|
30
|
+
const listeners = new Set();
|
|
31
|
+
|
|
32
|
+
export function getClaudeCloudSettings() {
|
|
33
|
+
return settingsState;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isClaudeCloudEnabled() {
|
|
37
|
+
return settingsState?.enabled === true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Called after every state change; returns an unsubscribe function. */
|
|
41
|
+
export function subscribeClaudeCloudSettings(listener) {
|
|
42
|
+
if (typeof listener !== 'function') return () => {};
|
|
43
|
+
listeners.add(listener);
|
|
44
|
+
return () => listeners.delete(listener);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function notifyListeners() {
|
|
48
|
+
for (const listener of listeners) {
|
|
49
|
+
try {
|
|
50
|
+
listener(settingsState);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[claude-cloud] settings listener failed', error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function oneLine(value) {
|
|
58
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeSettingsPayload(payload = {}, previous = null) {
|
|
62
|
+
const has = (key) => Object.prototype.hasOwnProperty.call(payload, key);
|
|
63
|
+
const keep = (key, fallback) => (previous && Object.prototype.hasOwnProperty.call(previous, key)
|
|
64
|
+
? previous[key]
|
|
65
|
+
: fallback);
|
|
66
|
+
return {
|
|
67
|
+
enabled: has('enabled') ? payload.enabled === true : keep('enabled', false),
|
|
68
|
+
defaultModel: has('defaultModel') ? oneLine(payload.defaultModel) : keep('defaultModel', ''),
|
|
69
|
+
environmentId: has('environmentId') ? oneLine(payload.environmentId) : keep('environmentId', ''),
|
|
70
|
+
environments: has('environments')
|
|
71
|
+
? (Array.isArray(payload.environments) ? payload.environments : null)
|
|
72
|
+
: keep('environments', null),
|
|
73
|
+
environmentsError: has('environmentsError') ? oneLine(payload.environmentsError) : keep('environmentsError', ''),
|
|
74
|
+
account: has('account')
|
|
75
|
+
? (payload.account && typeof payload.account === 'object' ? payload.account : null)
|
|
76
|
+
: keep('account', null),
|
|
77
|
+
token: has('token')
|
|
78
|
+
? (payload.token && typeof payload.token === 'object' ? payload.token : null)
|
|
79
|
+
: keep('token', null),
|
|
80
|
+
models: has('models')
|
|
81
|
+
? (Array.isArray(payload.models) ? payload.models.map((model) => oneLine(model)).filter(Boolean) : [])
|
|
82
|
+
: keep('models', []),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function subscriptionLabel(value) {
|
|
87
|
+
const text = oneLine(value);
|
|
88
|
+
return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** "dev@example.com · Example Org · Max": the account cloud chats are billed to. */
|
|
92
|
+
export function claudeCloudAccountLineText(account) {
|
|
93
|
+
if (!account) return 'Claude account status unavailable.';
|
|
94
|
+
if (account.loggedIn !== true) return 'Not signed in to Claude on the relay host.';
|
|
95
|
+
const parts = [oneLine(account.email), oneLine(account.orgName), subscriptionLabel(account.subscriptionType)]
|
|
96
|
+
.filter((part, index, all) => part && all.indexOf(part) === index);
|
|
97
|
+
return parts.length ? `Billed to ${parts.join(' · ')}` : 'Signed in to Claude.';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function claudeCloudAccountLineState(account) {
|
|
101
|
+
if (!account) return 'pending';
|
|
102
|
+
return account.loggedIn === true ? 'active' : 'unconfigured';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function defaultFormatTime(date) {
|
|
106
|
+
return date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Where the relay reads the login from and how long it lasts. An expired file
|
|
111
|
+
* token is not a logged-out verdict: the Claude CLI rewrites the file whenever
|
|
112
|
+
* it refreshes, and the relay re-reads it on the next cloud call.
|
|
113
|
+
*/
|
|
114
|
+
export function claudeCloudTokenLineText(token, { now = Date.now(), formatTime = defaultFormatTime } = {}) {
|
|
115
|
+
const source = oneLine(token?.source).toLowerCase();
|
|
116
|
+
if (!token || token.hasToken !== true || source === 'none') {
|
|
117
|
+
return 'No Claude login found on the relay host. Sign in on the Claude tab first.';
|
|
118
|
+
}
|
|
119
|
+
if (source === 'env') return 'Login: CLAUDE_CODE_OAUTH_TOKEN from the relay environment.';
|
|
120
|
+
const expiresAtMs = token.expiresAt ? Date.parse(token.expiresAt) : NaN;
|
|
121
|
+
if (!Number.isFinite(expiresAtMs)) return 'Login: read from the Claude CLI on the relay host.';
|
|
122
|
+
if (expiresAtMs <= Number(now)) {
|
|
123
|
+
return 'Login: read from the Claude CLI on the relay host. It has expired; the relay picks up the new one as soon as the CLI refreshes it.';
|
|
124
|
+
}
|
|
125
|
+
return `Login: read from the Claude CLI on the relay host, valid until ${formatTime(new Date(expiresAtMs))}.`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function claudeCloudTokenLineState(token, { now = Date.now() } = {}) {
|
|
129
|
+
if (!token || token.hasToken !== true || oneLine(token.source).toLowerCase() === 'none') return 'error';
|
|
130
|
+
const expiresAtMs = token.expiresAt ? Date.parse(token.expiresAt) : NaN;
|
|
131
|
+
return Number.isFinite(expiresAtMs) && expiresAtMs <= Number(now) ? 'pending' : 'active';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The environment select's options: what the account lists, plus the stored id
|
|
136
|
+
* when the list does not carry it (the list call failed, or the environment
|
|
137
|
+
* was removed) so saving something else never silently drops it.
|
|
138
|
+
*/
|
|
139
|
+
export function claudeCloudEnvironmentOptions(settings) {
|
|
140
|
+
const options = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const environment of Array.isArray(settings?.environments) ? settings.environments : []) {
|
|
143
|
+
const id = oneLine(environment?.id);
|
|
144
|
+
if (!id || seen.has(id)) continue;
|
|
145
|
+
seen.add(id);
|
|
146
|
+
const name = oneLine(environment?.name);
|
|
147
|
+
options.push({ value: id, label: name ? `${name} (${id})` : id });
|
|
148
|
+
}
|
|
149
|
+
const stored = oneLine(settings?.environmentId);
|
|
150
|
+
if (stored && !seen.has(stored)) options.push({ value: stored, label: `${stored} (saved)` });
|
|
151
|
+
return options;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Why the environment select has nothing (or not everything) to offer. The
|
|
156
|
+
* relay asks Anthropic for the list only while the provider is on and a login
|
|
157
|
+
* exists, so "not listed" and "the account has none" are different answers.
|
|
158
|
+
*/
|
|
159
|
+
export function claudeCloudEnvironmentNoteText(settings) {
|
|
160
|
+
const error = oneLine(settings?.environmentsError);
|
|
161
|
+
if (error) return `Could not list the cloud environments: ${error}`;
|
|
162
|
+
if (claudeCloudEnvironmentOptions(settings).length) return '';
|
|
163
|
+
if (Array.isArray(settings?.environments)) {
|
|
164
|
+
return 'No cloud environment found. Open claude.ai/code once with this account to create the default one.';
|
|
165
|
+
}
|
|
166
|
+
if (settings?.token && settings.token.hasToken !== true) {
|
|
167
|
+
return 'The environments are listed once the relay host is signed in to Claude.';
|
|
168
|
+
}
|
|
169
|
+
return settings?.enabled
|
|
170
|
+
? 'The cloud environments have not been listed yet.'
|
|
171
|
+
: 'The environments of the account are listed once Claude Cloud is enabled.';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function claudeCloudModelOptions(settings) {
|
|
175
|
+
return claudeCloudModelIds(settings).map((value) => ({ value, label: humanizeModelLabel(value) || value }));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function claudeCloudStatusLine(settings, { loadFailed = false, error = '' } = {}) {
|
|
179
|
+
if (error) return { text: error, state: 'error' };
|
|
180
|
+
if (!settings) {
|
|
181
|
+
return loadFailed
|
|
182
|
+
? { text: 'Claude Cloud is not available on this relay.', state: 'unconfigured' }
|
|
183
|
+
: { text: 'Loading Claude Cloud settings…', state: 'pending' };
|
|
184
|
+
}
|
|
185
|
+
if (settings.enabled) {
|
|
186
|
+
const model = oneLine(settings.defaultModel);
|
|
187
|
+
return {
|
|
188
|
+
text: `Claude Cloud is enabled. Select Claude Cloud in New Chat${model ? ` to start with ${humanizeModelLabel(model) || model}` : ''}.`,
|
|
189
|
+
state: 'active',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
text: 'Not enabled. Enable to offer Claude Cloud in New Chat (needs a Claude login on the relay host).',
|
|
194
|
+
state: 'unconfigured',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function fillSelect(select, options, selectedValue, emptyLabel) {
|
|
199
|
+
if (!select) return;
|
|
200
|
+
// The user's unsaved pick survives a re-render (a socket update, a refresh).
|
|
201
|
+
const wanted = inputsDirty ? String(select.value || '') : String(selectedValue || '');
|
|
202
|
+
select.innerHTML = '';
|
|
203
|
+
if (!options.length) {
|
|
204
|
+
const option = document.createElement('option');
|
|
205
|
+
option.value = '';
|
|
206
|
+
option.textContent = emptyLabel;
|
|
207
|
+
select.appendChild(option);
|
|
208
|
+
select.value = '';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const entry of options) {
|
|
212
|
+
const option = document.createElement('option');
|
|
213
|
+
option.value = entry.value;
|
|
214
|
+
option.textContent = entry.label;
|
|
215
|
+
select.appendChild(option);
|
|
216
|
+
}
|
|
217
|
+
const values = options.map((entry) => entry.value);
|
|
218
|
+
select.value = values.includes(wanted)
|
|
219
|
+
? wanted
|
|
220
|
+
: (values.includes(String(selectedValue || '')) ? String(selectedValue) : values[0]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function ensureInputTracking() {
|
|
224
|
+
for (const id of ['claude-cloud-environment-select', 'claude-cloud-model-select']) {
|
|
225
|
+
const select = document.getElementById(id);
|
|
226
|
+
if (!select || select.dataset.claudeCloudDirtyTracking === '1') continue;
|
|
227
|
+
select.dataset.claudeCloudDirtyTracking = '1';
|
|
228
|
+
select.addEventListener('change', () => {
|
|
229
|
+
inputsDirty = true;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function renderClaudeCloudSettingsSection() {
|
|
235
|
+
const panel = document.getElementById('settings-provider-panel-claude-cloud');
|
|
236
|
+
if (!panel) return;
|
|
237
|
+
ensureInputTracking();
|
|
238
|
+
const settings = settingsState;
|
|
239
|
+
const available = !!settings;
|
|
240
|
+
const environmentOptions = claudeCloudEnvironmentOptions(settings);
|
|
241
|
+
const modelOptions = claudeCloudModelOptions(settings);
|
|
242
|
+
|
|
243
|
+
const toggle = document.getElementById('claude-cloud-enabled-toggle');
|
|
244
|
+
if (toggle) {
|
|
245
|
+
toggle.checked = settings?.enabled === true;
|
|
246
|
+
toggle.disabled = !available || updateInFlight;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const account = document.getElementById('claude-cloud-account');
|
|
250
|
+
if (account) {
|
|
251
|
+
account.textContent = available ? claudeCloudAccountLineText(settings.account) : '';
|
|
252
|
+
account.dataset.state = available ? claudeCloudAccountLineState(settings.account) : 'pending';
|
|
253
|
+
}
|
|
254
|
+
const accountRow = document.getElementById('claude-cloud-account-row');
|
|
255
|
+
if (accountRow) accountRow.hidden = !available;
|
|
256
|
+
|
|
257
|
+
const token = document.getElementById('claude-cloud-token');
|
|
258
|
+
if (token) {
|
|
259
|
+
token.hidden = !available;
|
|
260
|
+
token.textContent = available ? claudeCloudTokenLineText(settings.token) : '';
|
|
261
|
+
token.dataset.state = available ? claudeCloudTokenLineState(settings.token) : 'pending';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const environmentSelect = document.getElementById('claude-cloud-environment-select');
|
|
265
|
+
fillSelect(
|
|
266
|
+
environmentSelect,
|
|
267
|
+
environmentOptions,
|
|
268
|
+
settings?.environmentId,
|
|
269
|
+
Array.isArray(settings?.environments) ? 'No environment found' : 'Not listed yet',
|
|
270
|
+
);
|
|
271
|
+
if (environmentSelect) environmentSelect.disabled = !available || updateInFlight || !environmentOptions.length;
|
|
272
|
+
|
|
273
|
+
const environmentNote = document.getElementById('claude-cloud-environment-note');
|
|
274
|
+
if (environmentNote) {
|
|
275
|
+
const noteText = available ? claudeCloudEnvironmentNoteText(settings) : '';
|
|
276
|
+
environmentNote.textContent = noteText;
|
|
277
|
+
environmentNote.hidden = !noteText;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const modelSelect = document.getElementById('claude-cloud-model-select');
|
|
281
|
+
fillSelect(modelSelect, modelOptions, claudeCloudDefaultModel(settings), 'No model available');
|
|
282
|
+
if (modelSelect) modelSelect.disabled = !available || updateInFlight || !modelOptions.length;
|
|
283
|
+
|
|
284
|
+
const saveButton = document.getElementById('claude-cloud-save-btn');
|
|
285
|
+
if (saveButton) saveButton.disabled = !available || updateInFlight;
|
|
286
|
+
|
|
287
|
+
const status = document.getElementById('claude-cloud-settings-status');
|
|
288
|
+
if (status) {
|
|
289
|
+
const line = claudeCloudStatusLine(settings, { loadFailed: settingsLoadFailed, error: localError });
|
|
290
|
+
status.textContent = line.text;
|
|
291
|
+
status.dataset.state = line.state;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Single entry point for every payload (GET, POST response, socket event).
|
|
296
|
+
export function applyClaudeCloudSettingsState(payload, { resetInputs = false } = {}) {
|
|
297
|
+
if (!payload || typeof payload !== 'object') return settingsState;
|
|
298
|
+
settingsState = normalizeSettingsPayload(payload, settingsState);
|
|
299
|
+
settingsLoadFailed = false;
|
|
300
|
+
localError = '';
|
|
301
|
+
if (resetInputs) inputsDirty = false;
|
|
302
|
+
renderClaudeCloudSettingsSection();
|
|
303
|
+
notifyListeners();
|
|
304
|
+
return settingsState;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function refreshClaudeCloudSettingsState() {
|
|
308
|
+
const payload = await loadClaudeCloudSettings();
|
|
309
|
+
if (!payload) {
|
|
310
|
+
// Keep what an earlier read said: one failed refresh is not "gone".
|
|
311
|
+
settingsLoadFailed = !settingsState;
|
|
312
|
+
renderClaudeCloudSettingsSection();
|
|
313
|
+
return settingsState;
|
|
314
|
+
}
|
|
315
|
+
return applyClaudeCloudSettingsState(payload);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Opening the modal starts from what is saved, as the other provider tabs do.
|
|
319
|
+
export function openClaudeCloudSettingsSection() {
|
|
320
|
+
inputsDirty = false;
|
|
321
|
+
localError = '';
|
|
322
|
+
renderClaudeCloudSettingsSection();
|
|
323
|
+
return refreshClaudeCloudSettingsState();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function submitClaudeCloudSettings(change, { successNotice, failureText, resetInputs = false }) {
|
|
327
|
+
if (updateInFlight || !settingsState) return;
|
|
328
|
+
updateInFlight = true;
|
|
329
|
+
localError = '';
|
|
330
|
+
renderClaudeCloudSettingsSection();
|
|
331
|
+
try {
|
|
332
|
+
const result = await updateClaudeCloudSettings(change);
|
|
333
|
+
if (!result) throw new Error(failureText);
|
|
334
|
+
updateInFlight = false;
|
|
335
|
+
applyClaudeCloudSettingsState(result, { resetInputs });
|
|
336
|
+
showTransientRelayNotice(successNotice(settingsState));
|
|
337
|
+
} catch (error) {
|
|
338
|
+
// Inline rather than an alert: "sign in first" is an instruction to act on
|
|
339
|
+
// in the tab next door, and it should still be there after switching back.
|
|
340
|
+
localError = String(error?.message || failureText);
|
|
341
|
+
} finally {
|
|
342
|
+
updateInFlight = false;
|
|
343
|
+
renderClaudeCloudSettingsSection();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function toggleClaudeCloudProvider(enabled) {
|
|
348
|
+
await submitClaudeCloudSettings(
|
|
349
|
+
{ enabled: enabled === true },
|
|
350
|
+
{
|
|
351
|
+
successNotice: (state) => (state?.enabled ? 'Claude Cloud enabled.' : 'Claude Cloud disabled.'),
|
|
352
|
+
failureText: 'Failed to update Claude Cloud.',
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function saveClaudeCloudSettings() {
|
|
358
|
+
const defaultModel = String(document.getElementById('claude-cloud-model-select')?.value || '').trim();
|
|
359
|
+
const environmentId = String(document.getElementById('claude-cloud-environment-select')?.value || '').trim();
|
|
360
|
+
await submitClaudeCloudSettings(
|
|
361
|
+
{ defaultModel: defaultModel || undefined, environmentId: environmentId || undefined },
|
|
362
|
+
{
|
|
363
|
+
successNotice: () => 'Claude Cloud settings saved.',
|
|
364
|
+
failureText: 'Failed to save Claude Cloud settings.',
|
|
365
|
+
resetInputs: true,
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// The login itself lives on the Claude tab (Relogin / Logout): one account,
|
|
371
|
+
// one place to change it.
|
|
372
|
+
export function openClaudeCloudLoginTab() {
|
|
373
|
+
selectSettingsTab('providers', 'claude');
|
|
374
|
+
}
|