@oar-sh/oar 0.9.20 → 0.9.22
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 +325 -0
- package/README.md +13 -13
- package/bin/oar.js +2 -2
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +231 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +31 -2
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +16 -4
- package/server/claude-worker/claude-session-worker-main.mjs +18 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +75 -1
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +18 -2
- package/server/cursor-worker/cursor-session-worker.mjs +21 -2
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +10 -0
- package/server/grok-worker/grok-session-worker.mjs +22 -2
- package/server/grok-worker/grok-turn-runner.mjs +4 -0
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +19 -0
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +11 -2
- package/server/public/app/ask-user-view.js +24 -9
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +618 -72
- package/server/public/app/chat-actions-menu-state.mjs +141 -4
- package/server/public/app/chat-header-meta.mjs +27 -0
- package/server/public/app/claude-billing-toggle-ui.js +2 -1
- package/server/public/app/claude-cloud-conversation-ui.js +2 -2
- package/server/public/app/composer-settings-label.mjs +90 -0
- package/server/public/app/composer-settings-ui.js +442 -0
- package/server/public/app/context-menu.mjs +36 -5
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-deep-link.mjs +15 -11
- package/server/public/app/conversation-group-drag.mjs +12 -2
- package/server/public/app/conversation-group-order.mjs +9 -2
- package/server/public/app/conversation-group-state.mjs +47 -10
- package/server/public/app/conversation-origin-ui.js +17 -1
- package/server/public/app/conversation-view.js +309 -49
- package/server/public/app/cwd-menu-placement.mjs +18 -6
- package/server/public/app/cwd-picker.js +36 -6
- package/server/public/app/emoji-view.js +1 -1
- package/server/public/app/font-scaling.js +26 -0
- package/server/public/app/header-priority-actions.mjs +238 -0
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +88 -81
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +328 -58
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/phone-landscape-ui.js +225 -0
- package/server/public/app/phone-landscape.mjs +99 -0
- package/server/public/app/phone-typing-state.mjs +104 -0
- package/server/public/app/pinned-messages-view.mjs +5 -0
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +38 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/relay-schedule-view.mjs +32 -6
- package/server/public/app/relay-schedules-ui.js +69 -20
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +251 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +67 -6
- package/server/public/app/settings-tabs.js +12 -0
- package/server/public/app/share-state.js +1 -1
- package/server/public/app/shared-conversations-settings.js +2 -2
- package/server/public/app/socket-handlers.js +47 -13
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +35 -8
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/app/usage-limit-ui.js +63 -2
- package/server/public/app/viewer-history.mjs +13 -2
- package/server/public/index.html +2603 -1767
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +33 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/repositories/session-repository.mjs +19 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +92 -15
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +117 -12
- package/server/server-runtime.mjs +367 -170
- package/server/services/about-service.mjs +22 -53
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +84 -18
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +183 -31
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/build-id.mjs +128 -0
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +31 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +44 -10
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The header's
|
|
1
|
+
// The header's ︙ menu: one decision for its trigger and for every entry, with
|
|
2
2
|
// or without a conversation open. No DOM here; bootstrap.js applies the result.
|
|
3
3
|
//
|
|
4
4
|
// The menu holds two kinds of entries. Relay-wide ones (Settings, About OAR,
|
|
@@ -12,10 +12,27 @@
|
|
|
12
12
|
//
|
|
13
13
|
// The shared read-only view is its own branch: Day/Night only, whatever the
|
|
14
14
|
// conversation state, and the trigger stays usable even when the shared
|
|
15
|
-
// conversation failed to load.
|
|
15
|
+
// conversation failed to load. Once Day/Night fits in the header the shared
|
|
16
|
+
// menu is empty and the trigger hides.
|
|
17
|
+
//
|
|
18
|
+
// The twins of the header's priority+ actions (pins, hidden, search, context,
|
|
19
|
+
// Day/Night, Check Usage, fullscreen) show only while their header button
|
|
20
|
+
// does not fit; the header's fit pass hands in which ones that is.
|
|
21
|
+
//
|
|
22
|
+
// A phone on its side hides the header's meta line, and with it the chips
|
|
23
|
+
// that open something (the watching chip's list, the schedule chip's list,
|
|
24
|
+
// the origin's conversation). There, and only for a chip the conversation
|
|
25
|
+
// has, the menu carries an entry that does the same.
|
|
16
26
|
|
|
17
27
|
export const CHAT_ACTIONS_MENU_TRIGGER_ID = 'chat-actions-menu-btn';
|
|
18
28
|
|
|
29
|
+
/** The stand-ins for the meta line's chips in phone landscape, by chip. */
|
|
30
|
+
export const CHAT_ACTIONS_MENU_META_CHIP_IDS = Object.freeze({
|
|
31
|
+
watching: 'chat-menu-watching',
|
|
32
|
+
schedules: 'chat-menu-schedules',
|
|
33
|
+
origin: 'chat-menu-origin',
|
|
34
|
+
});
|
|
35
|
+
|
|
19
36
|
/** Entries that are text, not actions: `hidden` is applied, nothing else. */
|
|
20
37
|
export const CHAT_ACTIONS_MENU_META_IDS = Object.freeze([
|
|
21
38
|
'chat-menu-pwa-version',
|
|
@@ -26,9 +43,18 @@ export const CHAT_ACTIONS_MENU_META_IDS = Object.freeze([
|
|
|
26
43
|
export const CHAT_ACTIONS_MENU_ENTRY_IDS = Object.freeze([
|
|
27
44
|
'chat-menu-pwa-version',
|
|
28
45
|
'chat-menu-queue-status',
|
|
46
|
+
'chat-menu-watching',
|
|
47
|
+
'chat-menu-schedules',
|
|
48
|
+
'chat-menu-origin',
|
|
29
49
|
'chat-menu-edit-title',
|
|
50
|
+
'chat-menu-questions',
|
|
51
|
+
'chat-menu-pins',
|
|
52
|
+
'chat-menu-hidden',
|
|
53
|
+
'chat-menu-search',
|
|
54
|
+
'chat-menu-context',
|
|
30
55
|
'chat-menu-theme-toggle',
|
|
31
56
|
'chat-menu-usage',
|
|
57
|
+
'chat-menu-fullscreen',
|
|
32
58
|
'chat-menu-compact',
|
|
33
59
|
'chat-menu-share-conversation',
|
|
34
60
|
'chat-menu-stop-sharing',
|
|
@@ -51,6 +77,36 @@ export const CHAT_ACTIONS_MENU_ENTRY_IDS = Object.freeze([
|
|
|
51
77
|
/** Entries that are never shown: retired actions kept in the markup. */
|
|
52
78
|
const ALWAYS_HIDDEN_IDS = new Set(['chat-menu-compact', 'chat-menu-refresh-history']);
|
|
53
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The twins of the header's priority+ actions (header-priority-actions.mjs):
|
|
82
|
+
* each shows here only while its header button does not fit. Kept in sync
|
|
83
|
+
* with HEADER_PRIORITY_MENU_IDS by the tests.
|
|
84
|
+
*/
|
|
85
|
+
export const CHAT_ACTIONS_MENU_TWIN_IDS = Object.freeze([
|
|
86
|
+
'chat-menu-questions',
|
|
87
|
+
'chat-menu-pins',
|
|
88
|
+
'chat-menu-hidden',
|
|
89
|
+
'chat-menu-search',
|
|
90
|
+
'chat-menu-context',
|
|
91
|
+
'chat-menu-usage',
|
|
92
|
+
'chat-menu-theme-toggle',
|
|
93
|
+
'chat-menu-fullscreen',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// Before the header was ever measured, the menu keeps the reach it always
|
|
97
|
+
// had: Day/Night and Check Usage. The other twins stay hidden, their header
|
|
98
|
+
// buttons show as they always did.
|
|
99
|
+
const UNMEASURED_TWIN_IDS = new Set(['chat-menu-theme-toggle', 'chat-menu-usage']);
|
|
100
|
+
|
|
101
|
+
// The shared read-only view offers Day/Night and nothing else.
|
|
102
|
+
const SHARED_TWIN_IDS = new Set(['chat-menu-theme-toggle']);
|
|
103
|
+
|
|
104
|
+
function twinShown(id, { sharedMode, headerMenuIds }) {
|
|
105
|
+
if (sharedMode && !SHARED_TWIN_IDS.has(id)) return false;
|
|
106
|
+
if (!Array.isArray(headerMenuIds)) return UNMEASURED_TWIN_IDS.has(id);
|
|
107
|
+
return headerMenuIds.includes(id);
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
const shown = (disabled = false) => ({ hidden: false, disabled: !!disabled });
|
|
55
111
|
const hidden = () => ({ hidden: true, disabled: true });
|
|
56
112
|
|
|
@@ -65,6 +121,17 @@ const hidden = () => ({ hidden: true, disabled: true });
|
|
|
65
121
|
* @param {boolean} [input.editing] its title is being edited
|
|
66
122
|
* @param {boolean} [input.suspendVisible] the "show Suspend host" setting
|
|
67
123
|
* @param {boolean} [input.hasActiveShare] it has a public share link
|
|
124
|
+
* @param {string[]|null} [input.headerMenuIds] the twin entries the header's
|
|
125
|
+
* fit pass put into the menu (available and not fitting); null before the
|
|
126
|
+
* header was measured
|
|
127
|
+
* @param {boolean} [input.fullscreenSupported] the browser can go fullscreen
|
|
128
|
+
* (requestFullscreen exists); the twin is disabled otherwise, as the header
|
|
129
|
+
* button is
|
|
130
|
+
* @param {boolean} [input.phoneLandscape] :root[data-phone-landscape], where
|
|
131
|
+
* the header's meta line is hidden
|
|
132
|
+
* @param {{ watching?: boolean, schedules?: boolean, origin?: boolean }} [input.metaChips]
|
|
133
|
+
* the meta line's chips the conversation has (a watching chip, a schedule
|
|
134
|
+
* chip, an origin that opens its conversation)
|
|
68
135
|
* @returns {{ trigger: { hidden: boolean, disabled: boolean }, entries: Record<string, { hidden: boolean, disabled: boolean }> }}
|
|
69
136
|
*/
|
|
70
137
|
export function resolveChatActionsMenuState({
|
|
@@ -77,12 +144,19 @@ export function resolveChatActionsMenuState({
|
|
|
77
144
|
editing = false,
|
|
78
145
|
suspendVisible = false,
|
|
79
146
|
hasActiveShare = false,
|
|
147
|
+
headerMenuIds = null,
|
|
148
|
+
fullscreenSupported = true,
|
|
149
|
+
phoneLandscape = false,
|
|
150
|
+
metaChips = null,
|
|
80
151
|
} = {}) {
|
|
81
152
|
const entries = {};
|
|
153
|
+
const twins = { sharedMode: !!sharedMode, headerMenuIds };
|
|
82
154
|
if (sharedMode) {
|
|
83
155
|
for (const id of CHAT_ACTIONS_MENU_ENTRY_IDS) entries[id] = hidden();
|
|
84
|
-
entries[
|
|
85
|
-
|
|
156
|
+
for (const id of CHAT_ACTIONS_MENU_TWIN_IDS) entries[id] = twinShown(id, twins) ? shown() : hidden();
|
|
157
|
+
// With Day/Night in the header there is nothing left to open.
|
|
158
|
+
const empty = !CHAT_ACTIONS_MENU_ENTRY_IDS.some((id) => !entries[id].hidden);
|
|
159
|
+
return { trigger: { hidden: empty, disabled: false }, entries };
|
|
86
160
|
}
|
|
87
161
|
|
|
88
162
|
const convId = String(conversationId || '').trim();
|
|
@@ -94,6 +168,14 @@ export function resolveChatActionsMenuState({
|
|
|
94
168
|
|
|
95
169
|
for (const id of CHAT_ACTIONS_MENU_ENTRY_IDS) entries[id] = shown();
|
|
96
170
|
for (const id of ALWAYS_HIDDEN_IDS) entries[id] = hidden();
|
|
171
|
+
for (const id of CHAT_ACTIONS_MENU_TWIN_IDS) entries[id] = twinShown(id, twins) ? shown() : hidden();
|
|
172
|
+
// Offered where the browser has no fullscreen (iPhone): disabled, as its
|
|
173
|
+
// header button is, rather than a live entry that does nothing.
|
|
174
|
+
if (!entries['chat-menu-fullscreen'].hidden && !fullscreenSupported) entries['chat-menu-fullscreen'] = shown(true);
|
|
175
|
+
// The meta line's chips, standing in where that line is hidden.
|
|
176
|
+
for (const [chip, id] of Object.entries(CHAT_ACTIONS_MENU_META_CHIP_IDS)) {
|
|
177
|
+
entries[id] = hasConversation && phoneLandscape && !!metaChips?.[chip] ? shown() : hidden();
|
|
178
|
+
}
|
|
97
179
|
entries['chat-menu-edit-title'] = perConversation();
|
|
98
180
|
entries['chat-menu-share-conversation'] = perConversation();
|
|
99
181
|
// Only a conversation with a live link has something to stop.
|
|
@@ -119,6 +201,61 @@ export function resolveChatActionsMenuState({
|
|
|
119
201
|
};
|
|
120
202
|
}
|
|
121
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The labels of the meta line's stand-ins: "👁 Watching 2 sessions…" (from the
|
|
206
|
+
* watching chip's text), "⏰ Schedules…", and the origin's line ("Started by
|
|
207
|
+
* an agent in “title”"). An entry with no text gets ''.
|
|
208
|
+
*
|
|
209
|
+
* @param {{ watching?: string, origin?: string }} input
|
|
210
|
+
* @returns {{ watching: string, schedules: string, origin: string }}
|
|
211
|
+
*/
|
|
212
|
+
export function chatActionsMenuMetaChipLabels({ watching = '', origin = '' } = {}) {
|
|
213
|
+
const watchingText = String(watching || '').trim();
|
|
214
|
+
const originText = String(origin || '').trim();
|
|
215
|
+
return {
|
|
216
|
+
watching: watchingText ? `👁 ${watchingText.charAt(0).toUpperCase()}${watchingText.slice(1)}…` : '',
|
|
217
|
+
schedules: '⏰ Schedules…',
|
|
218
|
+
origin: originText ? `↗ ${originText}` : '',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Where the open menu goes. It hangs from ︙ with its right edge on the
|
|
224
|
+
* button's; this keeps it inside `bounds` (the viewport and #main, so never
|
|
225
|
+
* off-screen and never under the sidebar) with `margin` to spare, narrows it
|
|
226
|
+
* when even that is too wide, and caps its height at what is left below its
|
|
227
|
+
* top (the menu then scrolls).
|
|
228
|
+
*
|
|
229
|
+
* @param {object} input
|
|
230
|
+
* @param {{ left: number, right: number, top: number }} input.rect the menu's box as CSS placed it
|
|
231
|
+
* @param {{ left: number, right: number, bottom: number }} input.bounds the area it must stay in
|
|
232
|
+
* @param {number} [input.margin] the gap it keeps from the bounds
|
|
233
|
+
* @param {number} [input.minHeight] the height it keeps at least
|
|
234
|
+
* @returns {{ right: number, maxWidth: number|null, maxHeight: number }}
|
|
235
|
+
* `right`: how far to move its right edge to the left of where CSS put it
|
|
236
|
+
* (negative: to the right); `maxWidth`: null when it fits as it is
|
|
237
|
+
*/
|
|
238
|
+
export function resolveChatActionsMenuPlacement({ rect, bounds, margin = 8, minHeight = 120 } = {}) {
|
|
239
|
+
const left = Number(rect?.left) || 0;
|
|
240
|
+
const right = Number(rect?.right) || 0;
|
|
241
|
+
const top = Number(rect?.top) || 0;
|
|
242
|
+
const width = Math.max(0, right - left);
|
|
243
|
+
const minLeft = (Number(bounds?.left) || 0) + margin;
|
|
244
|
+
const maxRight = (Number(bounds?.right) || 0) - margin;
|
|
245
|
+
const room = Math.max(0, maxRight - minLeft);
|
|
246
|
+
const fitWidth = Math.min(width, room);
|
|
247
|
+
let nextLeft = right - fitWidth;
|
|
248
|
+
if (nextLeft + fitWidth > maxRight) nextLeft = maxRight - fitWidth;
|
|
249
|
+
if (nextLeft < minLeft) nextLeft = minLeft;
|
|
250
|
+
const nextRight = nextLeft + fitWidth;
|
|
251
|
+
const bottom = Number(bounds?.bottom) || 0;
|
|
252
|
+
return {
|
|
253
|
+
right: Math.round((right - nextRight) * 100) / 100,
|
|
254
|
+
maxWidth: width > room ? Math.floor(room) : null,
|
|
255
|
+
maxHeight: Math.floor(Math.max(minHeight, bottom - margin - top)),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
122
259
|
/** The ids of the entries a state shows, in menu order. */
|
|
123
260
|
export function visibleChatActionsMenuEntryIds(state) {
|
|
124
261
|
const entries = state?.entries || {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The lines under the chat title (origin, relay chips, folder) come with a
|
|
2
|
+
// long and a short label. On a desktop the long one shows; on a phone the
|
|
3
|
+
// meta lines share one row that scrolls sideways and show the short one. CSS
|
|
4
|
+
// picks (index.html, .chat-meta-long / .chat-meta-short). DOM-free.
|
|
5
|
+
|
|
6
|
+
export const CHAT_META_LONG_CLASS = 'chat-meta-long';
|
|
7
|
+
export const CHAT_META_SHORT_CLASS = 'chat-meta-short';
|
|
8
|
+
|
|
9
|
+
/** Both labels; the arguments are HTML the caller already escaped. */
|
|
10
|
+
export function chatMetaLabelHtml(longHtml, shortHtml) {
|
|
11
|
+
return `<span class="${CHAT_META_LONG_CLASS}">${longHtml}</span><span class="${CHAT_META_SHORT_CLASS}">${shortHtml}</span>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The last folder name of a path, for the phone's folder chip:
|
|
16
|
+
* "/home/dev/sample-repo/" → "sample-repo", "C:\\work\\api" → "api". A root
|
|
17
|
+
* ("/", "C:\\") is kept whole; '' for nothing.
|
|
18
|
+
*/
|
|
19
|
+
export function cwdFolderName(path) {
|
|
20
|
+
const raw = String(path ?? '').trim();
|
|
21
|
+
if (!raw) return '';
|
|
22
|
+
const trimmed = raw.replace(/[\\/]+$/, '');
|
|
23
|
+
if (!trimmed) return raw.slice(0, 1);
|
|
24
|
+
if (/^[A-Za-z]:$/.test(trimmed)) return raw;
|
|
25
|
+
const parts = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
26
|
+
return parts.length ? parts[parts.length - 1] : raw;
|
|
27
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The composer's billing toggle (claude-billing-toggle.mjs) on the page: a
|
|
2
|
-
//
|
|
2
|
+
// field of the ⚙ panel, under Model, in Claude SDK chats; the label row's
|
|
3
|
+
// billing pill (composer-settings-ui.js) shows its selected side. A press
|
|
3
4
|
// moves the open chat between the subscription and the saved API key through
|
|
4
5
|
// the relay's billing route; the relay's answer and its `conversation_billing`
|
|
5
6
|
// events (usage-limit-ui.js keeps them per chat) redraw it.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// and the server's `cloud` payload field), so the one sync function below is
|
|
8
8
|
// idempotent and is simply called whenever the header is synced. A
|
|
9
9
|
// conversation of any other provider gets every element back exactly as it was.
|
|
10
|
-
// The
|
|
10
|
+
// The ︙ menu is not touched here: its entries are decided in one place
|
|
11
11
|
// (chat-actions-menu-state.mjs), which reads the cloud state through
|
|
12
12
|
// isCurrentConversationClaudeCloud() and currentCloudSessionUrl().
|
|
13
13
|
|
|
@@ -57,7 +57,7 @@ export function syncClaudeCloudConversationUi() {
|
|
|
57
57
|
line.hidden = !lineHtml;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// The
|
|
60
|
+
// The ︙ menu entries (no Change CWD for a cloud chat, Open on claude.ai once
|
|
61
61
|
// there is a link) are decided with the rest of the menu in
|
|
62
62
|
// chat-actions-menu-state.mjs, from `isCloud` and currentCloudSessionUrl().
|
|
63
63
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The composer's settings label ("🔒 Opus 5.5 · high · Auto · 1M") and
|
|
2
|
+
// its billing pill. Pure and DOM-free: composer-settings-ui.js reads the
|
|
3
|
+
// selects' shown option texts and hands them in.
|
|
4
|
+
|
|
5
|
+
export const COMPOSER_LABEL_SEPARATOR = ' · ';
|
|
6
|
+
export const COMPOSER_LOCK_GLYPH = '🔒';
|
|
7
|
+
|
|
8
|
+
// The effort select's placeholder when a model has no effort choice.
|
|
9
|
+
const UNAVAILABLE_EFFORT = 'unavailable';
|
|
10
|
+
|
|
11
|
+
// Values of the context-window select that are its default: the model's base
|
|
12
|
+
// window, or "auto" when the select is the image-size picker.
|
|
13
|
+
const DEFAULT_TIER_VALUES = new Set(['', 'default', 'auto']);
|
|
14
|
+
|
|
15
|
+
// Mode names the label shortens; the panel's select keeps the full name.
|
|
16
|
+
const SHORT_MODE_NAMES = new Map([['autopilot', 'Auto']]);
|
|
17
|
+
|
|
18
|
+
function clean(value) {
|
|
19
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The label's parts, in order: model, effort, mode, context window.
|
|
24
|
+
* Each is `{ field, text }`; `field` names the panel field a tap on it opens
|
|
25
|
+
* ('model' | 'effort' | 'mode' | 'tier'). The model part carries `locked`.
|
|
26
|
+
*
|
|
27
|
+
* - `cloud`: a cloud chat has no mode, effort or context window.
|
|
28
|
+
* - an effort with no value or "Unavailable" is left out.
|
|
29
|
+
* - the context window shows only when it is not the default.
|
|
30
|
+
* - a hidden field (`hidden.mode` etc.) is left out.
|
|
31
|
+
*/
|
|
32
|
+
export function composerSettingsLabelParts({
|
|
33
|
+
model = '',
|
|
34
|
+
locked = false,
|
|
35
|
+
effort = '',
|
|
36
|
+
effortValue = undefined,
|
|
37
|
+
mode = '',
|
|
38
|
+
tier = '',
|
|
39
|
+
tierValue = '',
|
|
40
|
+
cloud = false,
|
|
41
|
+
hidden = {},
|
|
42
|
+
} = {}) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
const modelText = clean(model);
|
|
45
|
+
if (modelText && !hidden.model) {
|
|
46
|
+
parts.push({ field: 'model', text: modelText, locked: locked === true });
|
|
47
|
+
}
|
|
48
|
+
if (!cloud && !hidden.effort) {
|
|
49
|
+
const effortText = clean(effort);
|
|
50
|
+
const effortEmpty = effortValue !== undefined && clean(effortValue) === '';
|
|
51
|
+
if (effortText && !effortEmpty && effortText.toLowerCase() !== UNAVAILABLE_EFFORT) {
|
|
52
|
+
parts.push({ field: 'effort', text: effortText });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!cloud && !hidden.mode) {
|
|
56
|
+
const modeText = clean(mode);
|
|
57
|
+
if (modeText) parts.push({ field: 'mode', text: SHORT_MODE_NAMES.get(modeText.toLowerCase()) || modeText });
|
|
58
|
+
}
|
|
59
|
+
if (!cloud && !hidden.tier) {
|
|
60
|
+
const tierText = clean(tier);
|
|
61
|
+
const value = clean(tierValue).toLowerCase();
|
|
62
|
+
if (tierText && !DEFAULT_TIER_VALUES.has(value)) parts.push({ field: 'tier', text: tierText });
|
|
63
|
+
}
|
|
64
|
+
return parts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A part as it reads in the label: the model gets the lock in front. */
|
|
68
|
+
export function composerSettingsPartText(part) {
|
|
69
|
+
if (!part) return '';
|
|
70
|
+
return part.field === 'model' && part.locked ? `${COMPOSER_LOCK_GLYPH} ${part.text}` : part.text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The whole label as one string, e.g. "🔒 Opus 5.5 · high · Autopilot · 1M". */
|
|
74
|
+
export function composerSettingsLabelText(input = {}) {
|
|
75
|
+
const parts = Array.isArray(input) ? input : composerSettingsLabelParts(input);
|
|
76
|
+
return parts.map(composerSettingsPartText).filter(Boolean).join(COMPOSER_LABEL_SEPARATOR);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The billing pill beside the label, from the billing toggle's state:
|
|
81
|
+
* `{ visible, billing, text }`. Hidden when the toggle is hidden or says
|
|
82
|
+
* no billing.
|
|
83
|
+
*/
|
|
84
|
+
export function composerBillingPill({ toggleVisible = false, billing = '' } = {}) {
|
|
85
|
+
const value = clean(billing).toLowerCase();
|
|
86
|
+
if (!toggleVisible || (value !== 'claude' && value !== 'claude-api')) {
|
|
87
|
+
return { visible: false, billing: '', text: '' };
|
|
88
|
+
}
|
|
89
|
+
return { visible: true, billing: value, text: value === 'claude-api' ? 'API key' : 'Subscription' };
|
|
90
|
+
}
|