@oar-sh/oar 0.9.15 → 0.9.17
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 +14 -87
- package/CHANGELOG.md +305 -0
- package/README.md +12 -11
- package/package.json +2 -1
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
- package/server/claude-worker/claude-session-process.mjs +131 -33
- package/server/claude-worker/claude-session-worker-main.mjs +266 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +282 -13
- package/server/claude-worker/claude-usage-totals.mjs +108 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
- package/server/db-schema.mjs +115 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
- package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
- package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
- package/server/migrations/0022-relay-schedules.mjs +142 -0
- package/server/migrations/0023-relay-asks.mjs +141 -0
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/public/app/api-client.js +53 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/bootstrap.js +162 -16
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +17 -0
- package/server/public/app/conversation-group-order.mjs +35 -0
- package/server/public/app/conversation-provider-indicator.mjs +7 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +367 -0
- package/server/public/app/inbox-view.mjs +370 -0
- package/server/public/app/journal-view.js +54 -5
- package/server/public/app/model-selector-options.mjs +11 -0
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +4 -1
- package/server/public/app/relay-error-ctas.mjs +8 -0
- package/server/public/app/relay-event-subscriptions-ui.js +4 -2
- package/server/public/app/relay-event-view.mjs +33 -7
- package/server/public/app/relay-schedule-view.mjs +119 -0
- package/server/public/app/relay-schedules-ui.js +172 -0
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +37 -3
- package/server/public/app/store.js +18 -5
- package/server/public/index.html +567 -35
- package/server/relay-tools.md +18 -3
- package/server/repositories/conversation-group-repository.mjs +110 -17
- package/server/repositories/remote-relay-repository.mjs +20 -2
- package/server/repositories/session-repository.mjs +7 -1
- package/server/routes/ask-user-routes.mjs +57 -3
- package/server/routes/cache-routes.mjs +6 -0
- package/server/routes/conversation-group-routes.mjs +19 -3
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +195 -18
- package/server/routes/relay-ask-routes.mjs +387 -0
- package/server/routes/relay-event-routes.mjs +3 -1
- package/server/routes/relay-schedule-routes.mjs +57 -0
- package/server/routes/sessions-routes.mjs +932 -97
- package/server/server-runtime.mjs +203 -11
- package/server/services/about-service.mjs +1 -0
- package/server/services/cache-rebuild-service.mjs +19 -2
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/conversation-preferences-service.mjs +17 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +431 -0
- package/server/services/provider-model-selection.mjs +1 -0
- package/server/services/provider-reasoning-effort.mjs +15 -2
- package/server/services/relay-ask-link-service.mjs +174 -0
- package/server/services/relay-ask-service.mjs +604 -0
- package/server/services/relay-event-service.mjs +589 -73
- package/server/services/relay-schedule-service.mjs +596 -0
- package/server/services/remote-relay-caller-context.mjs +89 -20
- package/server/services/remote-relay-dispatcher.mjs +958 -31
- package/server/services/session-worker-launch-service.mjs +233 -82
- package/server/services/worker-secret-redaction.mjs +318 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +2 -2
- package/shared/relay-asks.mjs +145 -0
- package/shared/relay-events.mjs +268 -18
- package/shared/relay-schedules.mjs +128 -0
- package/shared/remote-relay-contract.mjs +554 -26
- package/shared/remote-relay-tool-core.mjs +16 -5
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// The Inbox, as DOM: every pending question across this relay and its paired
|
|
2
|
+
// relays (GET /api/inbox), grouped by relay, newest first; what was answered
|
|
3
|
+
// or dismissed in the last day under a divider. And the card a question that
|
|
4
|
+
// does not block (remote_relay `ask`) shows in its own session.
|
|
5
|
+
//
|
|
6
|
+
// Every text here is written by an agent, some on other relays: it goes into
|
|
7
|
+
// the page through textContent only, never as HTML or a link. Buttons carry
|
|
8
|
+
// what they act on in data attributes; inbox-ui.js handles the clicks.
|
|
9
|
+
//
|
|
10
|
+
// The browser cannot import shared/ (only server/public is served): the few
|
|
11
|
+
// values of shared/relay-asks.mjs this needs are kept here, and
|
|
12
|
+
// inbox-view.test.mjs asserts they match.
|
|
13
|
+
|
|
14
|
+
export const RELAY_ASK_SOCKET_EVENT = 'relay_ask_updated';
|
|
15
|
+
export const INBOX_ANSWERED_KEEP_MS = 24 * 60 * 60_000;
|
|
16
|
+
export const INBOX_FILTERS = Object.freeze(['all', 'mine', 'agents']);
|
|
17
|
+
|
|
18
|
+
const CLOSED_STATUSES = new Set(['answered', 'dismissed']);
|
|
19
|
+
|
|
20
|
+
function toText(value) {
|
|
21
|
+
return String(value ?? '').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function el(doc, tag, className = '', text = null) {
|
|
25
|
+
const node = doc.createElement(tag);
|
|
26
|
+
if (className) node.className = className;
|
|
27
|
+
if (text !== null && text !== undefined) node.textContent = String(text);
|
|
28
|
+
return node;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function button(doc, className, label, data = {}) {
|
|
32
|
+
const node = el(doc, 'button', className, label);
|
|
33
|
+
node.type = 'button';
|
|
34
|
+
for (const [key, value] of Object.entries(data)) {
|
|
35
|
+
if (value !== undefined && value !== null && value !== '') node.dataset[key] = String(value);
|
|
36
|
+
}
|
|
37
|
+
return node;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** "just now", "5 min", "3 h", "2 d" since `createdAt`. */
|
|
41
|
+
export function formatInboxAge(createdAt, now = Date.now()) {
|
|
42
|
+
const at = Date.parse(toText(createdAt));
|
|
43
|
+
if (!Number.isFinite(at)) return '';
|
|
44
|
+
const minutes = Math.max(0, Math.floor((now - at) / 60_000));
|
|
45
|
+
if (minutes < 1) return 'just now';
|
|
46
|
+
if (minutes < 60) return `${minutes} min`;
|
|
47
|
+
const hours = Math.floor(minutes / 60);
|
|
48
|
+
if (hours < 48) return `${hours} h`;
|
|
49
|
+
return `${Math.floor(hours / 24)} d`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A link that opens one session in a relay's web UI (remoteConversationUrl's copy). */
|
|
53
|
+
export function inboxSessionUrl(relayUrl, sessionId) {
|
|
54
|
+
const base = toText(relayUrl).replace(/\/+$/, '');
|
|
55
|
+
const id = toText(sessionId);
|
|
56
|
+
if (!base || !/^https?:\/\//i.test(base)) return '';
|
|
57
|
+
return id ? `${base}/?conv=${encodeURIComponent(id)}` : `${base}/`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The filter: "mine" are the questions in sessions the owner started,
|
|
62
|
+
* "agents" those in sessions an agent started.
|
|
63
|
+
*/
|
|
64
|
+
export function inboxFilterMatches(item, filter = 'all') {
|
|
65
|
+
if (filter === 'mine') return item?.session?.startedBy !== 'agent';
|
|
66
|
+
if (filter === 'agents') return item?.session?.startedBy === 'agent';
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isClosed(item) {
|
|
71
|
+
return CLOSED_STATUSES.has(toText(item?.status));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function newestFirst(a, b) {
|
|
75
|
+
return toText(b?.createdAt).localeCompare(toText(a?.createdAt));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The Inbox's groups: per relay (this one first, then the paired ones as
|
|
80
|
+
* listed), its pending items newest first and its items closed in the last
|
|
81
|
+
* day; a relay that did not answer shows as such, with nothing listed.
|
|
82
|
+
*/
|
|
83
|
+
export function inboxSections(relays, { filter = 'all', now = Date.now() } = {}) {
|
|
84
|
+
return (Array.isArray(relays) ? relays : []).map((relay) => {
|
|
85
|
+
const items = (Array.isArray(relay?.items) ? relay.items : []).filter((item) => item && toText(item.id) && inboxFilterMatches(item, filter));
|
|
86
|
+
const pending = items.filter((item) => toText(item.status) === 'pending').sort(newestFirst);
|
|
87
|
+
const closed = items
|
|
88
|
+
.filter((item) => isClosed(item) && now - (Date.parse(toText(item.answeredAt || item.createdAt)) || 0) <= INBOX_ANSWERED_KEEP_MS)
|
|
89
|
+
.sort((a, b) => toText(b.answeredAt || b.createdAt).localeCompare(toText(a.answeredAt || a.createdAt)));
|
|
90
|
+
return {
|
|
91
|
+
relay: {
|
|
92
|
+
id: toText(relay?.id),
|
|
93
|
+
name: toText(relay?.name) || toText(relay?.id),
|
|
94
|
+
self: relay?.self === true,
|
|
95
|
+
url: toText(relay?.url),
|
|
96
|
+
reachable: relay?.reachable !== false,
|
|
97
|
+
error: toText(relay?.error),
|
|
98
|
+
},
|
|
99
|
+
pending,
|
|
100
|
+
closed,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A load that never runs twice at once, and never misses a change: a call
|
|
107
|
+
* while one runs is answered by that one and asks for one more after it
|
|
108
|
+
* (once, however many calls came meanwhile). Resolves with the last load's
|
|
109
|
+
* result.
|
|
110
|
+
*/
|
|
111
|
+
export function createCoalescedRefresh(load) {
|
|
112
|
+
let running = null;
|
|
113
|
+
let again = false;
|
|
114
|
+
function refresh() {
|
|
115
|
+
if (running) {
|
|
116
|
+
again = true;
|
|
117
|
+
return running;
|
|
118
|
+
}
|
|
119
|
+
running = (async () => {
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
do {
|
|
123
|
+
again = false;
|
|
124
|
+
result = await load();
|
|
125
|
+
} while (again);
|
|
126
|
+
return result;
|
|
127
|
+
} finally {
|
|
128
|
+
running = null;
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
return running;
|
|
132
|
+
}
|
|
133
|
+
return refresh;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* This relay's asks out of an Inbox load (GET /api/inbox), by id: what the
|
|
138
|
+
* session cards show. Built anew from each load, so an ask that was
|
|
139
|
+
* withdrawn or cancelled (no longer listed) drops out.
|
|
140
|
+
*/
|
|
141
|
+
export function sessionAsksFromInbox(relays) {
|
|
142
|
+
const own = (Array.isArray(relays) ? relays : []).find((relay) => relay?.self === true);
|
|
143
|
+
const asks = new Map();
|
|
144
|
+
for (const item of Array.isArray(own?.items) ? own.items : []) {
|
|
145
|
+
if (item?.kind === 'ask' && toText(item.id)) asks.set(item.id, { ...item, conversationId: item.session?.id });
|
|
146
|
+
}
|
|
147
|
+
return asks;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Pending items across every relay (the sidebar's badge), whatever the filter. */
|
|
151
|
+
export function inboxPendingCount(relays) {
|
|
152
|
+
return (Array.isArray(relays) ? relays : [])
|
|
153
|
+
.reduce((sum, relay) => sum + (Array.isArray(relay?.items) ? relay.items : []).filter((item) => toText(item?.status) === 'pending').length, 0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function headLine(doc, item, { relay, now, label }) {
|
|
157
|
+
const head = el(doc, 'div', 'inbox-item-head');
|
|
158
|
+
head.append(el(doc, 'span', 'inbox-item-kind', label));
|
|
159
|
+
if (item.urgency === 'now') head.append(el(doc, 'span', 'inbox-item-urgent', 'urgent'));
|
|
160
|
+
const age = formatInboxAge(item.createdAt, now);
|
|
161
|
+
if (age) head.append(el(doc, 'span', 'inbox-item-age', age));
|
|
162
|
+
if (relay && !relay.self) head.append(el(doc, 'span', 'inbox-item-relay', relay.name));
|
|
163
|
+
return head;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sessionLine(doc, item, relay, { withLink = true } = {}) {
|
|
167
|
+
const line = el(doc, 'div', 'inbox-item-session');
|
|
168
|
+
const title = toText(item?.session?.title) || 'Untitled session';
|
|
169
|
+
if (withLink && relay?.self) {
|
|
170
|
+
line.append(button(doc, 'inbox-open-session', title, { inboxAction: 'open', sessionId: item?.session?.id }));
|
|
171
|
+
} else if (withLink && relay && !relay.self) {
|
|
172
|
+
const url = inboxSessionUrl(relay.url, item?.session?.id);
|
|
173
|
+
if (url) {
|
|
174
|
+
const link = el(doc, 'a', 'inbox-open-there', title);
|
|
175
|
+
link.href = url;
|
|
176
|
+
link.target = '_blank';
|
|
177
|
+
link.rel = 'noopener noreferrer';
|
|
178
|
+
line.append(link);
|
|
179
|
+
} else {
|
|
180
|
+
line.append(el(doc, 'span', 'inbox-session-title', title));
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
line.append(el(doc, 'span', 'inbox-session-title', title));
|
|
184
|
+
}
|
|
185
|
+
const asker = item?.askedBy;
|
|
186
|
+
if (item?.onBehalf && asker) {
|
|
187
|
+
const who = [toText(asker.title) || toText(asker.conversationId), toText(asker.relay) ? `on ${toText(asker.relay)}` : ''].filter(Boolean).join(' ');
|
|
188
|
+
if (who) line.append(el(doc, 'span', 'inbox-item-asker', `asked by ${who}`));
|
|
189
|
+
}
|
|
190
|
+
return line;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function outcomeLine(doc, item) {
|
|
194
|
+
if (item.status === 'dismissed') return el(doc, 'div', 'inbox-item-outcome', 'Dismissed');
|
|
195
|
+
if (item.status !== 'answered') return null;
|
|
196
|
+
const answer = toText(item.answer) || toText(item.choice);
|
|
197
|
+
const via = toText(item.answeredVia);
|
|
198
|
+
return el(doc, 'div', 'inbox-item-outcome', `Answered: ${answer}${via ? ` (from ${via})` : ''}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A question that does not block, as a card: in the Inbox (`mode` "inbox",
|
|
203
|
+
* with its session and relay) or in its session ("session"). `answerable`:
|
|
204
|
+
* the buttons answer it (this relay's, or a paired relay's ask).
|
|
205
|
+
*/
|
|
206
|
+
export function buildAskCard(doc, item, { relay = null, mode = 'inbox', now = Date.now(), answerable = true } = {}) {
|
|
207
|
+
const card = el(doc, 'article', `inbox-item inbox-ask${mode === 'session' ? ' relay-ask-card' : ''}`);
|
|
208
|
+
card.dataset.itemId = toText(item.id);
|
|
209
|
+
card.dataset.kind = 'ask';
|
|
210
|
+
if (relay?.id) card.dataset.relayId = relay.id;
|
|
211
|
+
card.dataset.status = toText(item.status) || 'pending';
|
|
212
|
+
card.append(headLine(doc, item, { relay, now, label: mode === 'session' ? 'Question for you · does not block this session' : 'Question' }));
|
|
213
|
+
if (mode === 'inbox') card.append(sessionLine(doc, item, relay));
|
|
214
|
+
else if (item.onBehalf && item.askedBy) card.append(sessionLine(doc, item, relay, { withLink: false }));
|
|
215
|
+
card.append(el(doc, 'p', 'inbox-item-question', item.question || item.prompt || ''));
|
|
216
|
+
const options = Array.isArray(item.options) ? item.options.map(toText).filter(Boolean) : [];
|
|
217
|
+
const recommendation = toText(item.recommendation);
|
|
218
|
+
const recommendedOption = options.find((option) => option === recommendation) || '';
|
|
219
|
+
if (recommendation && !recommendedOption) card.append(el(doc, 'p', 'inbox-item-recommendation', `Recommended: ${recommendation}`));
|
|
220
|
+
if (toText(item.detail)) {
|
|
221
|
+
const details = el(doc, 'details', 'inbox-item-detail');
|
|
222
|
+
details.append(el(doc, 'summary', '', 'Details'));
|
|
223
|
+
// Plain text, line breaks kept: never parsed as markup.
|
|
224
|
+
details.append(el(doc, 'pre', 'inbox-item-detail-text', item.detail));
|
|
225
|
+
card.append(details);
|
|
226
|
+
}
|
|
227
|
+
const outcome = outcomeLine(doc, item);
|
|
228
|
+
if (outcome) card.append(outcome);
|
|
229
|
+
if (item.status === 'pending' && answerable) {
|
|
230
|
+
const data = { relayId: relay?.id || 'self', itemId: item.id };
|
|
231
|
+
if (options.length) {
|
|
232
|
+
const row = el(doc, 'div', 'inbox-item-options');
|
|
233
|
+
for (const option of options) {
|
|
234
|
+
const choice = button(doc, `inbox-choice${option === recommendedOption ? ' recommended' : ''}`, option, { ...data, inboxAction: 'choice', choice: option });
|
|
235
|
+
if (option === recommendedOption) {
|
|
236
|
+
choice.append(el(doc, 'span', 'inbox-recommended-tag', 'recommended'));
|
|
237
|
+
choice.setAttribute('aria-label', `${option} (recommended)`);
|
|
238
|
+
}
|
|
239
|
+
row.append(choice);
|
|
240
|
+
}
|
|
241
|
+
card.append(row);
|
|
242
|
+
}
|
|
243
|
+
const reply = el(doc, 'div', 'inbox-item-reply');
|
|
244
|
+
const field = el(doc, 'textarea', 'inbox-answer-text');
|
|
245
|
+
field.rows = 2;
|
|
246
|
+
field.placeholder = options.length ? 'Or type an answer…' : 'Your answer…';
|
|
247
|
+
field.setAttribute('aria-label', 'Answer');
|
|
248
|
+
field.dataset.itemId = toText(item.id);
|
|
249
|
+
reply.append(field);
|
|
250
|
+
const actions = el(doc, 'div', 'inbox-item-actions');
|
|
251
|
+
actions.append(button(doc, 'inbox-answer', 'Answer', { ...data, inboxAction: 'answer' }));
|
|
252
|
+
actions.append(button(doc, 'inbox-dismiss', 'Dismiss', { ...data, inboxAction: 'dismiss' }));
|
|
253
|
+
reply.append(actions);
|
|
254
|
+
card.append(reply);
|
|
255
|
+
}
|
|
256
|
+
return card;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A card a turn waits on (ask_user) or an approval card, in the Inbox. Here:
|
|
261
|
+
* a plain one is answered like in its session (choices, or typed text); one
|
|
262
|
+
* with fields or several picks opens its session. A paired relay's is never
|
|
263
|
+
* answered from here: "Open there".
|
|
264
|
+
*/
|
|
265
|
+
export function buildCardItem(doc, item, { relay = null, now = Date.now() } = {}) {
|
|
266
|
+
const approval = item.kind === 'approval';
|
|
267
|
+
const card = el(doc, 'article', `inbox-item inbox-card${approval ? ' inbox-approval' : ''}`);
|
|
268
|
+
card.dataset.itemId = toText(item.id);
|
|
269
|
+
card.dataset.kind = toText(item.kind) || 'ask_user';
|
|
270
|
+
if (relay?.id) card.dataset.relayId = relay.id;
|
|
271
|
+
card.dataset.status = toText(item.status) || 'pending';
|
|
272
|
+
card.append(headLine(doc, item, { relay, now, label: approval ? 'Approval · the session waits' : 'Question · the session waits' }));
|
|
273
|
+
card.append(sessionLine(doc, item, relay));
|
|
274
|
+
if (toText(item.header)) card.append(el(doc, 'div', 'inbox-item-header', item.header));
|
|
275
|
+
if (toText(item.question)) card.append(el(doc, 'p', 'inbox-item-question', item.question));
|
|
276
|
+
else if (approval) card.append(el(doc, 'p', 'inbox-item-question', 'An Allow / Deny card on that relay.'));
|
|
277
|
+
const outcome = outcomeLine(doc, item);
|
|
278
|
+
if (outcome) card.append(outcome);
|
|
279
|
+
if (item.status !== 'pending') return card;
|
|
280
|
+
if (!relay?.self) {
|
|
281
|
+
const url = inboxSessionUrl(relay?.url, item?.session?.id);
|
|
282
|
+
const actions = el(doc, 'div', 'inbox-item-actions');
|
|
283
|
+
if (url) {
|
|
284
|
+
const link = el(doc, 'a', 'inbox-open-there inbox-open-there-button', 'Open there');
|
|
285
|
+
link.href = url;
|
|
286
|
+
link.target = '_blank';
|
|
287
|
+
link.rel = 'noopener noreferrer';
|
|
288
|
+
actions.append(link);
|
|
289
|
+
} else {
|
|
290
|
+
actions.append(el(doc, 'span', 'inbox-item-note', 'Answer it on that relay.'));
|
|
291
|
+
}
|
|
292
|
+
card.append(actions);
|
|
293
|
+
return card;
|
|
294
|
+
}
|
|
295
|
+
const options = Array.isArray(item.options) ? item.options.map(toText).filter(Boolean) : [];
|
|
296
|
+
const data = { relayId: 'self', itemId: item.id, sessionId: item?.session?.id, sdkSessionId: item.sdkSessionId || '' };
|
|
297
|
+
if (item.structured || item.multiSelect) {
|
|
298
|
+
const actions = el(doc, 'div', 'inbox-item-actions');
|
|
299
|
+
actions.append(button(doc, 'inbox-open-session-button', 'Open the session to answer', { inboxAction: 'open', sessionId: item?.session?.id }));
|
|
300
|
+
card.append(actions);
|
|
301
|
+
return card;
|
|
302
|
+
}
|
|
303
|
+
if (options.length) {
|
|
304
|
+
const row = el(doc, 'div', 'inbox-item-options');
|
|
305
|
+
for (const option of options) row.append(button(doc, 'inbox-choice', option, { ...data, inboxAction: 'card-choice', choice: option }));
|
|
306
|
+
card.append(row);
|
|
307
|
+
}
|
|
308
|
+
if (item.allowFreeform !== false || !options.length) {
|
|
309
|
+
const reply = el(doc, 'div', 'inbox-item-reply');
|
|
310
|
+
const field = el(doc, 'textarea', 'inbox-answer-text');
|
|
311
|
+
field.rows = 2;
|
|
312
|
+
field.placeholder = 'Your answer…';
|
|
313
|
+
field.setAttribute('aria-label', 'Answer');
|
|
314
|
+
field.dataset.itemId = toText(item.id);
|
|
315
|
+
reply.append(field);
|
|
316
|
+
const actions = el(doc, 'div', 'inbox-item-actions');
|
|
317
|
+
actions.append(button(doc, 'inbox-answer', 'Answer', { ...data, inboxAction: 'card-answer' }));
|
|
318
|
+
reply.append(actions);
|
|
319
|
+
card.append(reply);
|
|
320
|
+
}
|
|
321
|
+
return card;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function buildItem(doc, item, relay, now) {
|
|
325
|
+
if (item.kind === 'ask') return buildAskCard(doc, item, { relay, now, mode: 'inbox', answerable: true });
|
|
326
|
+
return buildCardItem(doc, item, { relay, now });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The whole Inbox: the filter, then one group per relay. */
|
|
330
|
+
export function renderInbox(doc, { relays = [], filter = 'all', now = Date.now(), loading = false } = {}) {
|
|
331
|
+
const root = el(doc, 'section', 'status-view inbox-view');
|
|
332
|
+
root.setAttribute('aria-label', 'Inbox');
|
|
333
|
+
const bar = el(doc, 'div', 'inbox-filter');
|
|
334
|
+
bar.setAttribute('role', 'group');
|
|
335
|
+
bar.setAttribute('aria-label', 'Show');
|
|
336
|
+
for (const [value, label] of [['all', 'All'], ['mine', 'Mine'], ['agents', 'Agents']]) {
|
|
337
|
+
const option = button(doc, `inbox-filter-option${filter === value ? ' active' : ''}`, label, { inboxFilter: value });
|
|
338
|
+
option.setAttribute('aria-pressed', filter === value ? 'true' : 'false');
|
|
339
|
+
bar.append(option);
|
|
340
|
+
}
|
|
341
|
+
root.append(bar);
|
|
342
|
+
const sections = inboxSections(relays, { filter, now });
|
|
343
|
+
let anything = false;
|
|
344
|
+
for (const section of sections) {
|
|
345
|
+
const group = el(doc, 'section', 'inbox-relay');
|
|
346
|
+
group.dataset.relayId = section.relay.id;
|
|
347
|
+
const heading = el(doc, 'h2', 'inbox-relay-name', section.relay.self ? `${section.relay.name} (this relay)` : section.relay.name);
|
|
348
|
+
group.append(heading);
|
|
349
|
+
if (!section.relay.reachable) {
|
|
350
|
+
group.append(el(doc, 'p', 'inbox-relay-unreachable', `Not reachable${section.relay.error ? `: ${section.relay.error}` : ''}`));
|
|
351
|
+
root.append(group);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (!section.pending.length && !section.closed.length) {
|
|
355
|
+
if (!section.relay.self) continue;
|
|
356
|
+
group.append(el(doc, 'p', 'inbox-empty', loading ? 'Loading…' : 'Nothing waits for you here.'));
|
|
357
|
+
}
|
|
358
|
+
for (const item of section.pending) group.append(buildItem(doc, item, section.relay, now));
|
|
359
|
+
if (section.closed.length) {
|
|
360
|
+
const divider = el(doc, 'div', 'inbox-divider', 'Answered in the last day');
|
|
361
|
+
divider.setAttribute('role', 'separator');
|
|
362
|
+
group.append(divider);
|
|
363
|
+
for (const item of section.closed) group.append(buildItem(doc, item, section.relay, now));
|
|
364
|
+
}
|
|
365
|
+
anything = anything || section.pending.length > 0 || section.closed.length > 0;
|
|
366
|
+
root.append(group);
|
|
367
|
+
}
|
|
368
|
+
if (!anything && filter !== 'all') root.append(el(doc, 'p', 'inbox-empty', 'Nothing matches this filter.'));
|
|
369
|
+
return root;
|
|
370
|
+
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
setSummaryModalLeaveHandler,
|
|
30
30
|
conversationGroups,
|
|
31
31
|
mergeConversationGroups,
|
|
32
|
+
forgetKeptEmptyConversationGroups,
|
|
32
33
|
readConversationGroupFoldChoices,
|
|
33
34
|
pruneConversationGroups,
|
|
34
35
|
hasSessionWorkerStatus,
|
|
@@ -71,6 +72,7 @@ import {
|
|
|
71
72
|
loadClaudeCloudBranches,
|
|
72
73
|
loadClaudeCloudRepos,
|
|
73
74
|
loadGrokSettings,
|
|
75
|
+
loadDeepSeekSettings,
|
|
74
76
|
loadOpenAISettings,
|
|
75
77
|
} from './api-client.js';
|
|
76
78
|
import { isSetupRequired, updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
@@ -113,6 +115,7 @@ import {
|
|
|
113
115
|
conversationProviderIndicatorLabel,
|
|
114
116
|
} from './conversation-provider-indicator.mjs';
|
|
115
117
|
import { leaveStatusView } from './status-view.mjs';
|
|
118
|
+
import { leaveInboxView } from './inbox-ui.js';
|
|
116
119
|
import {
|
|
117
120
|
CLAUDE_CLOUD_PROVIDER,
|
|
118
121
|
buildCloudSourceWarnings,
|
|
@@ -132,6 +135,7 @@ import {
|
|
|
132
135
|
} from './claude-cloud-ui.mjs';
|
|
133
136
|
import { renderConversationOriginMarkerHtml } from './remote-relay-origin-view.mjs';
|
|
134
137
|
import { relayEventWatchedMarkerHtml } from './relay-event-subscriptions-ui.js';
|
|
138
|
+
import { relayScheduleMarkerHtml } from './relay-schedules-ui.js';
|
|
135
139
|
import {
|
|
136
140
|
normalizeConversationFilter,
|
|
137
141
|
conversationMatchesFilter,
|
|
@@ -186,6 +190,7 @@ let newConversationOpenAISettingsCache = null;
|
|
|
186
190
|
let newConversationClaudeSettingsCache = null;
|
|
187
191
|
let newConversationCursorSettingsCache = null;
|
|
188
192
|
let newConversationGrokSettingsCache = null;
|
|
193
|
+
let newConversationDeepSeekSettingsCache = null;
|
|
189
194
|
let newConversationClaudeCloudSettingsCache = null;
|
|
190
195
|
// Claude Cloud fields of the New Chat modal: what the selected folder's git
|
|
191
196
|
// remote said, and what was written into Repository/Branch from it (so a later
|
|
@@ -378,6 +383,9 @@ export async function refreshConversations(options = {}) {
|
|
|
378
383
|
if (!knownConversationIds.has(id)) delete conversations[id];
|
|
379
384
|
}
|
|
380
385
|
}
|
|
386
|
+
// The owner's kept empty groups come with the first page only: the page
|
|
387
|
+
// names the ones still kept.
|
|
388
|
+
forgetKeptEmptyConversationGroups();
|
|
381
389
|
mergeConversationGroups(r.groups);
|
|
382
390
|
applyConversationPage(r.conversations || []);
|
|
383
391
|
conversationRowsFetchedSinceRefresh = Array.isArray(r.conversations) ? r.conversations.length : 0;
|
|
@@ -613,6 +621,8 @@ function paintConvList() {
|
|
|
613
621
|
const items = arrangeConversationList(inView, conversationGroups, {
|
|
614
622
|
matches: activeFilter ? (row) => conversationMatchesFilter(row, activeFilter) : null,
|
|
615
623
|
groupNameMatches: activeFilter ? (info) => conversationMatchesFilter({ title: info?.displayName || info?.name }, activeFilter) : null,
|
|
624
|
+
// An owner's group an agent emptied shows in the live list, empty.
|
|
625
|
+
emptyGroups: !conversationListArchivedView,
|
|
616
626
|
isOpen: (groupId, group) => {
|
|
617
627
|
const described = describeGroupForHead(group, viewOf);
|
|
618
628
|
const holdsOpenConversation = !!currentConvId && group.members.some((row) => row.id === currentConvId);
|
|
@@ -690,6 +700,8 @@ function paintConvList() {
|
|
|
690
700
|
: renderConversationOriginMarkerHtml(c, { conversations });
|
|
691
701
|
// "watched": a session another conversation watches by id (relay events).
|
|
692
702
|
const watchedMarkerHtml = relayEventWatchedMarkerHtml(c.id);
|
|
703
|
+
// "scheduled": a schedule fires into this session (remote_relay schedule).
|
|
704
|
+
const scheduledMarkerHtml = relayScheduleMarkerHtml(c.id);
|
|
693
705
|
const groupClass = item.kind === 'head' ? ' conv-group-head' : (item.kind === 'member' ? ' conv-group-member' : '');
|
|
694
706
|
const groupAttr = item.kind === 'head' || item.kind === 'member' ? ` data-group-id="${escHtml(item.group.id)}"` : '';
|
|
695
707
|
// A head row shows the name the group was given by hand; until then the
|
|
@@ -700,7 +712,7 @@ function paintConvList() {
|
|
|
700
712
|
return `
|
|
701
713
|
<div class="conv-item worker-ui-${view.visualState}${view.working ? ' conv-working' : ''}${c.id === currentConvId ? ' active' : ''}${c.archived ? ' archived' : ''}${groupClass}" data-conversation-id="${c.id}"${groupAttr} onclick="openConversation('${c.id}')">
|
|
702
714
|
<div class="conv-title"${titleAttr}>${item.kind === 'head' ? foldButtonHtml(item) : ''}${escHtml(rowTitle)}${pendingByConversation[c.id] ? ` <span class="conv-open-questions">${pendingByConversation[c.id]} open</span>` : ''}</div>
|
|
703
|
-
<div class="conv-meta"><span class="conv-meta-primary">${fmtDate(c.updatedAt)} · ${c.messageCount} msg${c.messageCount !== 1 ? 's' : ''}</span>${item.kind === 'head' ? groupPillHtml(item) : ''}${originMarkerHtml}${watchedMarkerHtml}${providerIndicatorHtml}</div>
|
|
715
|
+
<div class="conv-meta"><span class="conv-meta-primary">${fmtDate(c.updatedAt)} · ${c.messageCount} msg${c.messageCount !== 1 ? 's' : ''}</span>${item.kind === 'head' ? groupPillHtml(item) : ''}${originMarkerHtml}${watchedMarkerHtml}${scheduledMarkerHtml}${providerIndicatorHtml}</div>
|
|
704
716
|
${c.archived
|
|
705
717
|
? `<button class="conv-archive" onclick="unarchiveConv(event,'${c.id}')" title="Unarchive" aria-label="Unarchive">📂</button>`
|
|
706
718
|
: `<button class="conv-archive" onclick="archiveConv(event,'${c.id}')" title="Archive" aria-label="Archive">🗄</button>`}
|
|
@@ -711,6 +723,15 @@ function paintConvList() {
|
|
|
711
723
|
// archived or deleted): a head line of its own, with the name and the pill.
|
|
712
724
|
const syntheticHeadHtml = (item) => {
|
|
713
725
|
const name = groupDisplayName(item.group.id);
|
|
726
|
+
// An owner's group an agent emptied: no members, nothing to fold; its
|
|
727
|
+
// menu (right-click or long-press) renames or dissolves it.
|
|
728
|
+
if (item.group.empty) {
|
|
729
|
+
return `
|
|
730
|
+
<div class="conv-item conv-group-head conv-group-synthetic conv-group-empty worker-ui-offline" data-group-id="${escHtml(item.group.id)}" title="An agent moved the last session out of this group; rename or dissolve it from its menu.">
|
|
731
|
+
<div class="conv-title">${escHtml(name)}</div>
|
|
732
|
+
<div class="conv-meta"><span class="conv-meta-primary">group · empty</span></div>
|
|
733
|
+
</div>`;
|
|
734
|
+
}
|
|
714
735
|
const described = groupDescriptions.get(item.group.id) || describeGroupForHead(item.group, viewOf);
|
|
715
736
|
return `
|
|
716
737
|
<div class="conv-item conv-group-head conv-group-synthetic worker-ui-${described.summary.strongest === 'waiting' ? 'question' : 'offline'}" data-group-id="${escHtml(item.group.id)}" onclick="toggleConversationGroupFold(event,'${escHtml(item.group.id)}')">
|
|
@@ -1059,7 +1080,9 @@ function applyFreshOverCachedConversation(id, response, { cached, keepTranscript
|
|
|
1059
1080
|
}
|
|
1060
1081
|
|
|
1061
1082
|
export async function openConversation(id, options = {}) {
|
|
1062
|
-
|
|
1083
|
+
// The Inbox owns the transcript like the status view does.
|
|
1084
|
+
const didLeaveInboxView = leaveInboxView();
|
|
1085
|
+
const didLeaveStatusView = leaveStatusView() || didLeaveInboxView;
|
|
1063
1086
|
document.getElementById('input-area')?.removeAttribute('hidden');
|
|
1064
1087
|
const previousConversationId = String(currentConvId || '').trim();
|
|
1065
1088
|
const nextConversationId = String(id || '').trim();
|
|
@@ -1216,6 +1239,7 @@ function normalizeNewConversationProviderType(value = '') {
|
|
|
1216
1239
|
if (normalized === CLAUDE_CLOUD_PROVIDER) return CLAUDE_CLOUD_PROVIDER;
|
|
1217
1240
|
if (normalized === 'cursor') return 'cursor';
|
|
1218
1241
|
if (normalized === 'grok') return 'grok';
|
|
1242
|
+
if (normalized === 'deepseek') return 'deepseek';
|
|
1219
1243
|
return 'github';
|
|
1220
1244
|
}
|
|
1221
1245
|
|
|
@@ -1253,6 +1277,7 @@ function modelMatchesNewConversationProvider(catalog = {}, modelId = '', provide
|
|
|
1253
1277
|
const hasClaude = providers.includes('claude');
|
|
1254
1278
|
const hasCursor = providers.includes('cursor');
|
|
1255
1279
|
const hasGrok = providers.includes('grok');
|
|
1280
|
+
const hasDeepSeek = providers.includes('deepseek');
|
|
1256
1281
|
if (wantsOpenAI) {
|
|
1257
1282
|
if (hasOpenAIByok) return true;
|
|
1258
1283
|
const settingsModel = String(newConversationOpenAISettingsCache?.model || '').trim();
|
|
@@ -1288,16 +1313,26 @@ function modelMatchesNewConversationProvider(catalog = {}, modelId = '', provide
|
|
|
1288
1313
|
: [];
|
|
1289
1314
|
return normalizedModelId === grokModel || grokModels.includes(normalizedModelId);
|
|
1290
1315
|
}
|
|
1316
|
+
if (normalizedProvider === 'deepseek') {
|
|
1317
|
+
if (hasDeepSeek) return true;
|
|
1318
|
+
const deepseekModel = String(newConversationDeepSeekSettingsCache?.model || '').trim();
|
|
1319
|
+
const deepseekModels = Array.isArray(newConversationDeepSeekSettingsCache?.models)
|
|
1320
|
+
? newConversationDeepSeekSettingsCache.models.map((entry) => String(entry || '').trim()).filter(Boolean)
|
|
1321
|
+
: [];
|
|
1322
|
+
return normalizedModelId === deepseekModel || deepseekModels.includes(normalizedModelId);
|
|
1323
|
+
}
|
|
1291
1324
|
const claudeOnly = hasClaude && providers.every((provider) => provider === 'claude');
|
|
1292
1325
|
if (claudeOnly) return false;
|
|
1293
1326
|
const cursorOnly = hasCursor && providers.every((provider) => provider === 'cursor');
|
|
1294
1327
|
if (cursorOnly) return false;
|
|
1295
1328
|
const grokOnly = hasGrok && providers.every((provider) => provider === 'grok');
|
|
1296
1329
|
if (grokOnly) return false;
|
|
1330
|
+
const deepseekOnly = hasDeepSeek && providers.every((provider) => provider === 'deepseek');
|
|
1331
|
+
if (deepseekOnly) return false;
|
|
1297
1332
|
// Should a relay ever tag catalog rows for Claude Cloud, they are no more
|
|
1298
1333
|
// Copilot's than the other runtimes' rows are.
|
|
1299
1334
|
const exclusiveOnly = providers.length > 0 && providers.every((provider) => (
|
|
1300
|
-
provider === 'claude' || provider === 'cursor' || provider === 'grok' || provider === CLAUDE_CLOUD_PROVIDER
|
|
1335
|
+
provider === 'claude' || provider === 'cursor' || provider === 'grok' || provider === 'deepseek' || provider === CLAUDE_CLOUD_PROVIDER
|
|
1301
1336
|
));
|
|
1302
1337
|
if (exclusiveOnly) return false;
|
|
1303
1338
|
return providers.some((provider) => (
|
|
@@ -1305,6 +1340,7 @@ function modelMatchesNewConversationProvider(catalog = {}, modelId = '', provide
|
|
|
1305
1340
|
&& provider !== 'claude'
|
|
1306
1341
|
&& provider !== 'cursor'
|
|
1307
1342
|
&& provider !== 'grok'
|
|
1343
|
+
&& provider !== 'deepseek'
|
|
1308
1344
|
&& provider !== CLAUDE_CLOUD_PROVIDER
|
|
1309
1345
|
)) || !hasOpenAIByok;
|
|
1310
1346
|
}
|
|
@@ -1422,8 +1458,10 @@ async function populateNewConversationReasoningSelect(selectedModel = '') {
|
|
|
1422
1458
|
if (optionTitle) option.title = optionTitle;
|
|
1423
1459
|
select.appendChild(option);
|
|
1424
1460
|
}
|
|
1461
|
+
// A DeepSeek chat with nothing remembered starts at DeepSeek's own default.
|
|
1462
|
+
const storedEffort = localStorage.getItem(REASONING_STORAGE_KEY);
|
|
1425
1463
|
const preferred = resolvePreferredReasoningEffort(efforts, [
|
|
1426
|
-
|
|
1464
|
+
storedEffort || (provider === 'deepseek' ? 'high' : ''),
|
|
1427
1465
|
]);
|
|
1428
1466
|
select.value = preferred || efforts[0];
|
|
1429
1467
|
select.disabled = false;
|
|
@@ -1466,6 +1504,10 @@ function updateNewConversationProviderHelp(provider = 'github') {
|
|
|
1466
1504
|
help.textContent = "Grok chats use the host machine's logged-in Grok credentials (grok login or XAI_API_KEY).";
|
|
1467
1505
|
return;
|
|
1468
1506
|
}
|
|
1507
|
+
if (normalizedProvider === 'deepseek') {
|
|
1508
|
+
help.textContent = "DeepSeek chats run Claude Code on the relay host against DeepSeek's API, billed to your saved DeepSeek key.";
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1469
1511
|
help.textContent = 'Copilot models use your GitHub Copilot runtime.';
|
|
1470
1512
|
}
|
|
1471
1513
|
|
|
@@ -2073,7 +2115,7 @@ function applyNewConversationSetupState(setupRequired) {
|
|
|
2073
2115
|
}
|
|
2074
2116
|
|
|
2075
2117
|
async function openNewConversationModelModal() {
|
|
2076
|
-
const [catalog, settings, claudeSettings, cursorSettings, grokSettings, claudeCloudSettings] = await Promise.all([
|
|
2118
|
+
const [catalog, settings, claudeSettings, cursorSettings, grokSettings, claudeCloudSettings, deepseekSettings] = await Promise.all([
|
|
2077
2119
|
loadModelCatalog(),
|
|
2078
2120
|
loadOpenAISettings(),
|
|
2079
2121
|
loadClaudeSettings(),
|
|
@@ -2081,6 +2123,8 @@ async function openNewConversationModelModal() {
|
|
|
2081
2123
|
loadGrokSettings(),
|
|
2082
2124
|
// null on a relay without Claude Cloud: the option is simply not offered.
|
|
2083
2125
|
loadClaudeCloudSettings(),
|
|
2126
|
+
// The same for a relay without DeepSeek.
|
|
2127
|
+
loadDeepSeekSettings(),
|
|
2084
2128
|
]);
|
|
2085
2129
|
newConversationCatalogCache = catalog || null;
|
|
2086
2130
|
newConversationOpenAISettingsCache = settings || null;
|
|
@@ -2088,6 +2132,7 @@ async function openNewConversationModelModal() {
|
|
|
2088
2132
|
newConversationCursorSettingsCache = cursorSettings || null;
|
|
2089
2133
|
newConversationGrokSettingsCache = grokSettings || null;
|
|
2090
2134
|
newConversationClaudeCloudSettingsCache = claudeCloudSettings || null;
|
|
2135
|
+
newConversationDeepSeekSettingsCache = deepseekSettings || null;
|
|
2091
2136
|
// Fresh answers for the readiness question, shared with the empty screen
|
|
2092
2137
|
// and the composer. With no provider known to be ready the modal opens in
|
|
2093
2138
|
// its setup state: Settings is the main button, Start stays as "anyway".
|
|
@@ -2097,6 +2142,7 @@ async function openNewConversationModelModal() {
|
|
|
2097
2142
|
claudeCloud: claudeCloudSettings || null,
|
|
2098
2143
|
cursor: cursorSettings || null,
|
|
2099
2144
|
grok: grokSettings || null,
|
|
2145
|
+
deepseek: deepseekSettings || null,
|
|
2100
2146
|
catalog: catalog || null,
|
|
2101
2147
|
});
|
|
2102
2148
|
applyNewConversationSetupState(isSetupRequired(readiness));
|
|
@@ -2122,6 +2168,9 @@ async function openNewConversationModelModal() {
|
|
|
2122
2168
|
if (grokSettings?.enabled === true) {
|
|
2123
2169
|
options.push({ value: 'grok', label: 'Grok' });
|
|
2124
2170
|
}
|
|
2171
|
+
if (deepseekSettings?.enabled === true) {
|
|
2172
|
+
options.push({ value: 'deepseek', label: 'DeepSeek' });
|
|
2173
|
+
}
|
|
2125
2174
|
providerSelect.innerHTML = '';
|
|
2126
2175
|
for (const option of options) {
|
|
2127
2176
|
const entry = document.createElement('option');
|
|
@@ -16,11 +16,13 @@ export function composerPlaceholderFor({ modelId = '', providerType = '' } = {})
|
|
|
16
16
|
if (provider === 'grok') return 'Message Grok…';
|
|
17
17
|
if (provider === 'openai' || provider === 'openai-byok') return 'Message OpenAI…';
|
|
18
18
|
if (provider === 'cursor') return 'Message Cursor…';
|
|
19
|
+
if (provider === 'deepseek') return 'Message DeepSeek…';
|
|
19
20
|
return 'Message Copilot…';
|
|
20
21
|
};
|
|
21
22
|
if (!id || id === 'auto') return providerFallback();
|
|
22
23
|
if (id.startsWith('claude-')) return 'Message Claude…';
|
|
23
24
|
if (id.startsWith('grok-')) return 'Message Grok…';
|
|
25
|
+
if (id.startsWith('deepseek-')) return 'Message DeepSeek…';
|
|
24
26
|
if (id.startsWith('gpt-')) return 'Message GPT…';
|
|
25
27
|
if (id.startsWith('gemini-')) return 'Message Gemini…';
|
|
26
28
|
// Composer is Cursor's house model family.
|
|
@@ -65,6 +67,15 @@ export function humanizeModelLabel(modelId = '') {
|
|
|
65
67
|
const label = words.map((word) => word.text).join(' ');
|
|
66
68
|
return suffixMatch ? `${label} [${suffixMatch[1]}]` : label;
|
|
67
69
|
}
|
|
70
|
+
// "deepseek-flash" → "DeepSeek Flash", "deepseek-v4-pro" → "DeepSeek V4 Pro".
|
|
71
|
+
if (/^deepseek-/i.test(text)) {
|
|
72
|
+
return `DeepSeek ${text
|
|
73
|
+
.replace(/^deepseek-/i, '')
|
|
74
|
+
.split('-')
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.map((part) => (/^v\d/i.test(part) ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
77
|
+
.join(' ')}`;
|
|
78
|
+
}
|
|
68
79
|
if (/^gemini-/i.test(text)) {
|
|
69
80
|
return text
|
|
70
81
|
.replace(/^gemini-/i, 'Gemini ')
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { isSetupRequired } from './provider-readiness.mjs';
|
|
11
11
|
|
|
12
12
|
export const SETUP_HEADING = 'Set up a provider';
|
|
13
|
-
export const SETUP_TEXT = 'Turn on Claude, Cursor, OpenAI or
|
|
13
|
+
export const SETUP_TEXT = 'Turn on Claude, Cursor, OpenAI, Grok or DeepSeek in Settings → Providers, or sign in to the Copilot CLI on the relay host.';
|
|
14
14
|
|
|
15
15
|
const ACTIONS = Object.freeze({
|
|
16
16
|
openSettings: (primary) => ({ id: 'empty-state-open-settings', action: 'open-settings', label: primary ? 'Open Settings' : 'Settings', primary, link: !primary }),
|