@oar-sh/oar 0.9.1 → 0.9.3
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/extension.mjs +0 -3
- package/.github/extensions/web-relay/polling/heartbeat.mjs +2 -55
- package/.github/extensions/web-relay/polling/polling-loop.mjs +0 -1
- package/.github/extensions/web-relay/runtime/api-client.mjs +2 -34
- package/.github/extensions/web-relay/runtime/config-loader.mjs +2 -104
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +2 -170
- package/.github/extensions/web-relay/runtime/session-io.mjs +1 -71
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +2 -325
- package/.github/extensions/web-relay/skills/prompt-context.mjs +2 -160
- package/CHANGELOG.md +269 -0
- package/README.md +407 -424
- package/bin/oar.js +142 -11
- package/package.json +8 -4
- package/server/README.md +59 -22
- package/server/claude-worker/claude-sdk-adapter.mjs +7 -2
- package/server/claude-worker/claude-session-process.mjs +1013 -45
- package/server/claude-worker/claude-session-worker.mjs +54 -9
- package/server/claude-worker/claude-turn-publisher.mjs +110 -19
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/copilot-sdk-runtime.mjs +12 -3
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-attachments.mjs +35 -2
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-model-switch.mjs +425 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +20 -8
- package/server/copilot-worker/copilot-question-bridge.mjs +129 -16
- package/server/copilot-worker/copilot-sdk-adapter.mjs +59 -11
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +136 -12
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2753 -527
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +94 -25
- package/server/cursor-worker/cursor-session-worker.mjs +5 -5
- package/server/cursor-worker/cursor-turn-runner.mjs +50 -4
- package/server/db-schema.mjs +61 -1
- package/server/features.mjs +155 -41
- package/server/grok-worker/grok-session-worker.mjs +5 -5
- package/server/grok-worker/grok-turn-runner.mjs +45 -4
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/public/app/annotate-core.mjs +181 -0
- package/server/public/app/annotate-editor.js +617 -0
- package/server/public/app/api-client.js +86 -5
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +172 -3
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +34 -19
- package/server/public/app/bootstrap.js +153 -69
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/context-tier-options.mjs +24 -4
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-preferences.mjs +47 -10
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +1032 -218
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/journal-view.js +190 -24
- package/server/public/app/model-selector-options.mjs +185 -1
- package/server/public/app/new-conversation-model-choice.mjs +29 -9
- package/server/public/app/pwa-install.js +64 -136
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/reasoning-effort-labels.mjs +13 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +26 -3
- package/server/public/app/router.js +120 -11
- package/server/public/app/settings-modal.js +225 -20
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +47 -4
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +159 -16
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/tool-activity-emoji.mjs +36 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/app/update-ui.js +194 -0
- package/server/public/index.html +483 -24
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +2 -0
- package/server/relay.mjs +23 -21
- package/server/repositories/message-repository.mjs +78 -21
- package/server/repositories/question-repository.mjs +19 -1
- package/server/repositories/session-repository.mjs +7 -31
- package/server/routes/ask-user-routes.mjs +10 -0
- package/server/routes/messages-routes.mjs +689 -159
- package/server/routes/sessions-routes.mjs +574 -80
- package/server/server-runtime.mjs +393 -925
- package/server/services/claude-auth-service.mjs +7 -1
- package/server/services/cli-install-service.mjs +8 -27
- package/server/services/cli-process-runner.mjs +30 -0
- package/server/services/copilot-model-discovery-service.mjs +202 -0
- package/server/services/data-dir-resolution.mjs +47 -0
- package/server/services/grok-auth-service.mjs +9 -1
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/model-variant-catalog-service.mjs +1183 -0
- package/server/services/oar-cli-helpers.mjs +6 -2
- package/server/services/relay-prompt-sanitizer.mjs +30 -3
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/sdk-session-import-service.mjs +144 -16
- package/server/services/sdk-session-sync-service.mjs +84 -27
- package/server/services/session-worker-registry-service.mjs +116 -3
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +119 -24
- package/server/services/status-event-service.mjs +8 -2
- package/server/services/steer-resend.mjs +45 -0
- package/server/services/update-check-service.mjs +220 -0
- package/server/services/update-install-service.mjs +279 -0
- package/server/services/windows-autostart-service.mjs +42 -6
- package/server/services/windows-boot-autostart-service.mjs +487 -0
- package/shared/ask-user-bridge.mjs +37 -5
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/claude-model-catalog.mjs +49 -0
- package/shared/context-window-fallbacks.mjs +1 -0
- package/shared/control-poller.mjs +4 -1
- package/shared/copilot-model-order.mjs +182 -0
- package/shared/media-embed-instructions.mjs +35 -0
- package/shared/model-descriptors.mjs +181 -30
- package/shared/model-id.mjs +6 -0
- package/shared/pwa-app-name.mjs +44 -0
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- package/shared/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/update-semver.mjs +50 -0
- package/shared/worker-crash-guard.mjs +40 -14
- package/shared/worker-runtime/api-client.mjs +34 -0
- package/shared/worker-runtime/config-loader.mjs +104 -0
- package/shared/worker-runtime/heartbeat.mjs +109 -0
- package/shared/worker-runtime/prompt-context.mjs +160 -0
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +170 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +436 -0
- package/server/claude-worker/claude-session-test-harness.mjs +0 -236
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -329
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.sh +0 -62
- package/server/services/fixtures/cli-install-stub.sh +0 -75
- package/server/services/fixtures/grok-stub.sh +0 -86
|
@@ -43,6 +43,8 @@ function insertEmojiAtCaret(emoji) {
|
|
|
43
43
|
input.selectionEnd = nextCaret;
|
|
44
44
|
autoResize(input);
|
|
45
45
|
input.focus();
|
|
46
|
+
// Runs the composer's input pipeline, which saves the draft.
|
|
47
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function handleEmojiChoice(emoji) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { captureTranscriptAnchor, restoreTranscriptAnchor } from './transcript-viewport-anchor.mjs';
|
|
2
|
+
|
|
1
3
|
const FONT_SCALE_STORAGE_KEY = 'copilot_font_scale';
|
|
2
4
|
const FONT_SCALE_MIN = 0.5;
|
|
3
5
|
const FONT_SCALE_MAX = 1.5;
|
|
@@ -53,41 +55,6 @@ function normalizeFontScaleSelectValue(raw = '') {
|
|
|
53
55
|
return clampFontScale(numeric);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
function getMessageViewportAnchor() {
|
|
57
|
-
const container = document.getElementById('messages');
|
|
58
|
-
if (!container) return null;
|
|
59
|
-
const containerRect = container.getBoundingClientRect();
|
|
60
|
-
const messages = Array.from(container.querySelectorAll('.msg[data-message-id]'));
|
|
61
|
-
for (const message of messages) {
|
|
62
|
-
const rect = message.getBoundingClientRect();
|
|
63
|
-
if (rect.bottom <= containerRect.top || rect.top >= containerRect.bottom) continue;
|
|
64
|
-
const messageId = String(message.dataset.messageId || '').trim();
|
|
65
|
-
if (!messageId) continue;
|
|
66
|
-
return {
|
|
67
|
-
messageId,
|
|
68
|
-
offsetTop: rect.top - containerRect.top,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function restoreMessageViewportAnchor(anchor) {
|
|
75
|
-
if (!anchor?.messageId) return;
|
|
76
|
-
const container = document.getElementById('messages');
|
|
77
|
-
if (!container) return;
|
|
78
|
-
const messageId = String(anchor.messageId || '').trim();
|
|
79
|
-
if (!messageId) return;
|
|
80
|
-
const message = Array.from(container.querySelectorAll('.msg[data-message-id]'))
|
|
81
|
-
.find((node) => String(node?.dataset?.messageId || '').trim() === messageId);
|
|
82
|
-
if (!message) return;
|
|
83
|
-
const containerRect = container.getBoundingClientRect();
|
|
84
|
-
const rect = message.getBoundingClientRect();
|
|
85
|
-
const delta = rect.top - containerRect.top - Number(anchor.offsetTop || 0);
|
|
86
|
-
if (Math.abs(delta) > 0.5) {
|
|
87
|
-
container.scrollTop += delta;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
58
|
export function syncFontScaleSelect() {
|
|
92
59
|
const select = document.getElementById('font-scale-select');
|
|
93
60
|
if (!select) return;
|
|
@@ -117,7 +84,7 @@ export function setFontScale(nextScale, { persist = true, preserveMessageAnchor
|
|
|
117
84
|
syncFontScaleSelect();
|
|
118
85
|
return normalized;
|
|
119
86
|
}
|
|
120
|
-
const anchor = preserveMessageAnchor ?
|
|
87
|
+
const anchor = preserveMessageAnchor ? captureTranscriptAnchor(document.getElementById('messages')) : null;
|
|
121
88
|
fontScaleValue = normalized;
|
|
122
89
|
document.documentElement.style.setProperty('--font-scale', normalized.toFixed(4));
|
|
123
90
|
document.documentElement.style.setProperty('--font-scale-percent', `${Math.round(normalized * 100)}%`);
|
|
@@ -125,7 +92,7 @@ export function setFontScale(nextScale, { persist = true, preserveMessageAnchor
|
|
|
125
92
|
syncFontScaleSelect();
|
|
126
93
|
if (anchor) {
|
|
127
94
|
requestAnimationFrame(() => {
|
|
128
|
-
|
|
95
|
+
restoreTranscriptAnchor(document.getElementById('messages'), anchor);
|
|
129
96
|
});
|
|
130
97
|
}
|
|
131
98
|
return normalized;
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
loadGrokSettings,
|
|
37
37
|
loadOpenAISettings,
|
|
38
38
|
} from './api-client.js';
|
|
39
|
-
import { renderMessages, restoreInFlightThinking, focusConversationMessageById, flushConversationDraft, hydrateConversationDraft } from './conversation-view.js';
|
|
39
|
+
import { renderMessages, restoreInFlightThinking, focusConversationMessageById, flushConversationDraft, hydrateConversationDraft, beginConversationDraftSwitch } from './conversation-view.js';
|
|
40
40
|
import { setBackgroundTasksConversation, setConversationBackgroundTasks } from './background-tasks-view.mjs';
|
|
41
41
|
import { mergeConversationPreviews } from './preview-cards.mjs';
|
|
42
42
|
import { loadRelayQuestions, getPendingQuestionCountsByConversation } from './ask-user-view.js';
|
|
@@ -47,15 +47,23 @@ import { shouldApplyConversationLoad } from './activity-replay-state.mjs';
|
|
|
47
47
|
import { createInfiniteLoader } from './infinite-loader.js';
|
|
48
48
|
import {
|
|
49
49
|
buildNewConversationModelChoices,
|
|
50
|
+
newConversationContextTierState,
|
|
50
51
|
reasoningChoicesForProviderModel,
|
|
51
52
|
resolvePreferredReasoningEffort,
|
|
52
53
|
} from './new-conversation-model-choice.mjs';
|
|
54
|
+
import { buildCatalogModelOptions } from './model-selector-options.mjs';
|
|
53
55
|
import { isReasoningOffUnsupported, reasoningEffortOptionLabel, reasoningEffortOptionTitle } from './reasoning-effort-labels.mjs';
|
|
54
56
|
import {
|
|
55
57
|
conversationProviderIndicatorKey,
|
|
56
58
|
conversationProviderIndicatorLabel,
|
|
57
59
|
} from './conversation-provider-indicator.mjs';
|
|
58
60
|
import { leaveStatusView } from './status-view.mjs';
|
|
61
|
+
import {
|
|
62
|
+
normalizeConversationFilter,
|
|
63
|
+
filterConversations,
|
|
64
|
+
drainRemainingPages,
|
|
65
|
+
describeFilterMatchCount,
|
|
66
|
+
} from './conversation-list-filter.mjs';
|
|
59
67
|
|
|
60
68
|
const PROCESSING_DOT_FRAMES = [' ', '. ', '.. ', '...'];
|
|
61
69
|
const PROCESSING_DOT_INTERVAL_MS = 1000;
|
|
@@ -72,9 +80,13 @@ const CLAUDE_LONG_CONTEXT_PATTERN = /\[1m\]$/i;
|
|
|
72
80
|
const OPENAI_IMAGE_SIZE_STORAGE_KEY = 'copilot_openai_image_size';
|
|
73
81
|
const NEW_CHAT_CWD_STORAGE_KEY = 'copilot_new_chat_cwd';
|
|
74
82
|
const NEW_CHAT_CUSTOM_CWD_VALUE = '__custom__';
|
|
83
|
+
const CONV_FILTER_DEBOUNCE_MS = 220;
|
|
75
84
|
let processingDotFrame = 0;
|
|
76
85
|
let processingDotTimer = null;
|
|
77
86
|
let lastConvListHtml = '';
|
|
87
|
+
let conversationListFilterText = '';
|
|
88
|
+
let conversationFilterDrainActive = false;
|
|
89
|
+
let conversationFilterGeneration = 0;
|
|
78
90
|
let openConversationVersion = 0;
|
|
79
91
|
let newConversationInFlight = false;
|
|
80
92
|
|
|
@@ -271,6 +283,11 @@ export async function refreshConversations(options = {}) {
|
|
|
271
283
|
});
|
|
272
284
|
renderConvList();
|
|
273
285
|
updateCompactButton();
|
|
286
|
+
// A reset bumps the loader version, which aborts any in-flight filter drain;
|
|
287
|
+
// restart it so an active filter keeps covering the unloaded pages.
|
|
288
|
+
if (normalizeConversationFilter(conversationListFilterText)) {
|
|
289
|
+
void drainConversationListForFilter();
|
|
290
|
+
}
|
|
274
291
|
}
|
|
275
292
|
|
|
276
293
|
export function renderConvList() {
|
|
@@ -280,9 +297,19 @@ export function renderConvList() {
|
|
|
280
297
|
if (updatedAtDelta !== 0) return updatedAtDelta;
|
|
281
298
|
return String(b?.id || '').trim().localeCompare(String(a?.id || '').trim());
|
|
282
299
|
});
|
|
300
|
+
const activeFilter = normalizeConversationFilter(conversationListFilterText);
|
|
301
|
+
const visible = activeFilter ? filterConversations(sorted, activeFilter) : sorted;
|
|
283
302
|
const pendingByConversation = getPendingQuestionCountsByConversation();
|
|
284
303
|
let hasProcessingConversation = false;
|
|
285
304
|
const footerHtml = (() => {
|
|
305
|
+
if (activeFilter) {
|
|
306
|
+
// While a filter is active the drain loads older pages by itself; a
|
|
307
|
+
// "scroll for more" hint would point at a gesture that does nothing, and
|
|
308
|
+
// "Searching…" must only show while something is actually loading.
|
|
309
|
+
return (conversationFilterDrainActive || conversationListPaginationState.isLoading)
|
|
310
|
+
? '<div class="conv-list-footer">Searching older conversations…</div>'
|
|
311
|
+
: '';
|
|
312
|
+
}
|
|
286
313
|
if (conversationListPaginationState.isLoading) {
|
|
287
314
|
return '<div class="conv-list-footer">Loading older conversations…</div>';
|
|
288
315
|
}
|
|
@@ -291,10 +318,14 @@ export function renderConvList() {
|
|
|
291
318
|
}
|
|
292
319
|
return '';
|
|
293
320
|
})();
|
|
294
|
-
if (
|
|
321
|
+
if (visible.length === 0) {
|
|
295
322
|
ensureProcessingDotTimer(false);
|
|
296
|
-
|
|
323
|
+
const emptyText = activeFilter && sorted.length > 0
|
|
324
|
+
? 'No conversations match your filter'
|
|
325
|
+
: 'No conversations yet';
|
|
326
|
+
list.innerHTML = `<div style="padding:12px;color:var(--muted);font-size:0.85rem;text-align:center">${emptyText}</div>${footerHtml}`;
|
|
297
327
|
lastConvListHtml = '';
|
|
328
|
+
scheduleConversationListBoundaryCheck();
|
|
298
329
|
return;
|
|
299
330
|
}
|
|
300
331
|
const conversationView = (conversation) => {
|
|
@@ -310,7 +341,7 @@ export function renderConvList() {
|
|
|
310
341
|
if (processing) hasProcessingConversation = true;
|
|
311
342
|
return { visualState, processing };
|
|
312
343
|
};
|
|
313
|
-
const listHtml = `${
|
|
344
|
+
const listHtml = `${visible.map((c) => {
|
|
314
345
|
const view = conversationView(c);
|
|
315
346
|
const processingDots = view.processing ? PROCESSING_DOT_FRAMES[processingDotFrame] : '';
|
|
316
347
|
const providerIndicatorLabel = conversationProviderIndicatorLabel(c);
|
|
@@ -425,6 +456,7 @@ export async function openConversation(id, options = {}) {
|
|
|
425
456
|
await flushConversationDraft(previousConversationId);
|
|
426
457
|
window.clearImageEditTarget?.();
|
|
427
458
|
}
|
|
459
|
+
const switchingConversation = !!nextConversationId && previousConversationId !== nextConversationId;
|
|
428
460
|
const capturedVersion = ++openConversationVersion;
|
|
429
461
|
setCurrentConv(id);
|
|
430
462
|
if (repoBrowserState.activeRoot === 'workspace') {
|
|
@@ -438,7 +470,13 @@ export async function openConversation(id, options = {}) {
|
|
|
438
470
|
repoBrowserState.error = '';
|
|
439
471
|
}
|
|
440
472
|
closeSidebar();
|
|
441
|
-
|
|
473
|
+
// Reopening the conversation on screen (session bind, clicking it again, a
|
|
474
|
+
// push or search jump) must leave its composer alone: emptied attachments
|
|
475
|
+
// would read as a user edit and be saved, dropping them from the draft.
|
|
476
|
+
if (switchingConversation) {
|
|
477
|
+
clearAttachments();
|
|
478
|
+
beginConversationDraftSwitch(nextConversationId);
|
|
479
|
+
}
|
|
442
480
|
document.getElementById('chat-title').textContent = conversations[id]?.title || id;
|
|
443
481
|
if (didLeaveStatusView) {
|
|
444
482
|
restoreInFlightThinking(null);
|
|
@@ -640,6 +678,32 @@ function populateNewConversationSizeSelect(providerType = 'github', selectedMode
|
|
|
640
678
|
if (status) status.textContent = 'Image size used for generated outputs in this chat.';
|
|
641
679
|
}
|
|
642
680
|
|
|
681
|
+
// Read-only: the composer chip is the only place a tier is chosen (see
|
|
682
|
+
// newConversationContextTierState), so this just tells the user what the
|
|
683
|
+
// picked model gives them before the chat exists.
|
|
684
|
+
function populateNewConversationContextRow(providerType = 'github', selectedModel = '') {
|
|
685
|
+
const select = document.getElementById('new-conversation-context-select');
|
|
686
|
+
const row = document.getElementById('new-conversation-context-row');
|
|
687
|
+
if (!select || !row) return;
|
|
688
|
+
const { visible, options } = newConversationContextTierState(newConversationCatalogCache || {}, {
|
|
689
|
+
provider: normalizeNewConversationProviderType(providerType),
|
|
690
|
+
modelId: selectedModel,
|
|
691
|
+
});
|
|
692
|
+
select.innerHTML = '';
|
|
693
|
+
if (!visible) {
|
|
694
|
+
row.style.display = 'none';
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
for (const tier of options) {
|
|
698
|
+
const option = document.createElement('option');
|
|
699
|
+
option.value = tier.value;
|
|
700
|
+
option.textContent = tier.label;
|
|
701
|
+
select.appendChild(option);
|
|
702
|
+
}
|
|
703
|
+
select.value = options[0].value;
|
|
704
|
+
row.style.display = 'block';
|
|
705
|
+
}
|
|
706
|
+
|
|
643
707
|
async function populateNewConversationReasoningSelect(selectedModel = '') {
|
|
644
708
|
const select = document.getElementById('new-conversation-reasoning-select');
|
|
645
709
|
const status = document.getElementById('new-conversation-reasoning-status');
|
|
@@ -727,29 +791,25 @@ async function populateNewConversationModelSelect(providerType = 'github') {
|
|
|
727
791
|
const target = document.getElementById('new-conversation-model-select');
|
|
728
792
|
if (!target) return false;
|
|
729
793
|
target.innerHTML = '';
|
|
730
|
-
const source = document.getElementById('model-select');
|
|
731
|
-
const sourceLabelByValue = new Map(
|
|
732
|
-
Array.from(source?.options || []).map((option) => [
|
|
733
|
-
String(option.value || '').trim(),
|
|
734
|
-
String(option.textContent || option.value || '').trim(),
|
|
735
|
-
]),
|
|
736
|
-
);
|
|
737
794
|
const catalog = newConversationCatalogCache || await loadModelCatalog();
|
|
738
795
|
if (!catalog || !Array.isArray(catalog.models)) return false;
|
|
739
796
|
const normalizedProvider = normalizeNewConversationProviderType(providerType);
|
|
797
|
+
const eligibleModels = catalog.models
|
|
798
|
+
.map((modelId) => String(modelId || '').trim())
|
|
799
|
+
.filter((modelId) => modelMatchesNewConversationProvider(catalog, modelId, normalizedProvider))
|
|
800
|
+
.filter((modelId) => (
|
|
801
|
+
normalizedProvider !== 'openai-image' || isOpenAIImageModelId(modelId)
|
|
802
|
+
));
|
|
803
|
+
// The same builder the composer's #model-select uses, on the same catalog,
|
|
804
|
+
// so the two pickers agree on order and wording by construction. The modal
|
|
805
|
+
// used to borrow labels from the composer, which is scoped to the OPEN
|
|
806
|
+
// conversation's provider and so knew nothing about the models of another.
|
|
807
|
+
const hasAuto = eligibleModels.some((modelId) => modelId.toLowerCase() === 'auto');
|
|
740
808
|
const choices = buildNewConversationModelChoices(
|
|
741
|
-
|
|
742
|
-
.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
))
|
|
746
|
-
.map((modelId) => {
|
|
747
|
-
const value = String(modelId || '').trim();
|
|
748
|
-
return {
|
|
749
|
-
value,
|
|
750
|
-
label: sourceLabelByValue.get(value) || value,
|
|
751
|
-
};
|
|
752
|
-
}),
|
|
809
|
+
buildCatalogModelOptions(eligibleModels, {
|
|
810
|
+
metadataByModel: catalog.modelMetadataByModel || {},
|
|
811
|
+
annotateContextWindow: normalizedProvider !== 'claude',
|
|
812
|
+
}).filter((option) => hasAuto || option.value.toLowerCase() !== 'auto'),
|
|
753
813
|
);
|
|
754
814
|
for (const choice of choices) {
|
|
755
815
|
const option = document.createElement('option');
|
|
@@ -767,6 +827,7 @@ async function populateNewConversationModelSelect(providerType = 'github') {
|
|
|
767
827
|
updateNewConversationProviderHelp(normalizedProvider);
|
|
768
828
|
syncNewConversationReasoningLabel(normalizedProvider);
|
|
769
829
|
populateNewConversationSizeSelect(normalizedProvider, target.value);
|
|
830
|
+
populateNewConversationContextRow(normalizedProvider, target.value);
|
|
770
831
|
await populateNewConversationReasoningSelect(target.value);
|
|
771
832
|
return true;
|
|
772
833
|
}
|
|
@@ -939,6 +1000,7 @@ async function openNewConversationModelModal() {
|
|
|
939
1000
|
modelSelect.addEventListener('change', () => {
|
|
940
1001
|
const provider = String(document.getElementById('new-conversation-provider-select')?.value || '').trim();
|
|
941
1002
|
populateNewConversationSizeSelect(provider, modelSelect.value);
|
|
1003
|
+
populateNewConversationContextRow(provider, modelSelect.value);
|
|
942
1004
|
void populateNewConversationReasoningSelect(modelSelect.value);
|
|
943
1005
|
});
|
|
944
1006
|
}
|
|
@@ -1119,11 +1181,115 @@ export function initConversationListLazyLoading() {
|
|
|
1119
1181
|
scheduleConversationListBoundaryCheck();
|
|
1120
1182
|
}
|
|
1121
1183
|
|
|
1184
|
+
// A title filter has to search conversations that only exist on unloaded pages,
|
|
1185
|
+
// so an active filter drains the remaining pages in the background. Each page
|
|
1186
|
+
// re-renders via applyPage, so matches appear as they load.
|
|
1187
|
+
// A filter change aborts the running drain and starts a fresh one, so the new
|
|
1188
|
+
// filter gets a full retry budget.
|
|
1189
|
+
async function drainConversationListForFilter() {
|
|
1190
|
+
if (conversationFilterDrainActive) return;
|
|
1191
|
+
conversationFilterDrainActive = true;
|
|
1192
|
+
const generation = conversationFilterGeneration;
|
|
1193
|
+
renderConvList();
|
|
1194
|
+
try {
|
|
1195
|
+
await drainRemainingPages(
|
|
1196
|
+
conversationListLoader,
|
|
1197
|
+
() => generation === conversationFilterGeneration
|
|
1198
|
+
&& !!normalizeConversationFilter(conversationListFilterText),
|
|
1199
|
+
);
|
|
1200
|
+
} finally {
|
|
1201
|
+
conversationFilterDrainActive = false;
|
|
1202
|
+
const filterActive = !!normalizeConversationFilter(conversationListFilterText);
|
|
1203
|
+
if (filterActive && generation !== conversationFilterGeneration) {
|
|
1204
|
+
void drainConversationListForFilter();
|
|
1205
|
+
} else {
|
|
1206
|
+
// The "Searching older conversations…" footer keys off the drain flag.
|
|
1207
|
+
renderConvList();
|
|
1208
|
+
if (filterActive) announceConversationFilterResults();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Announced only when the (debounced) filter applies and when its drain ends,
|
|
1214
|
+
// not on every page render, so screen readers are not flooded while typing.
|
|
1215
|
+
// Unchanged text is not rewritten: refresh-triggered drains (session bind,
|
|
1216
|
+
// reconnect) must not re-announce without a user action.
|
|
1217
|
+
function announceConversationFilterResults() {
|
|
1218
|
+
const status = document.getElementById('conv-filter-status');
|
|
1219
|
+
if (!status) return;
|
|
1220
|
+
const activeFilter = normalizeConversationFilter(conversationListFilterText);
|
|
1221
|
+
let text = '';
|
|
1222
|
+
if (activeFilter) {
|
|
1223
|
+
const count = filterConversations(Object.values(conversations), activeFilter).length;
|
|
1224
|
+
if (!conversationListLoader.getState().hasMore) {
|
|
1225
|
+
text = describeFilterMatchCount(count);
|
|
1226
|
+
} else if (conversationFilterDrainActive) {
|
|
1227
|
+
text = `${describeFilterMatchCount(count)} so far, searching older conversations`;
|
|
1228
|
+
} else {
|
|
1229
|
+
// The drain gave up with pages left, so the search is not complete.
|
|
1230
|
+
text = describeFilterMatchCount(count, { loadedOnly: true });
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (status.textContent !== text) status.textContent = text;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function setConversationListFilter(text) {
|
|
1237
|
+
const next = String(text ?? '');
|
|
1238
|
+
if (next === conversationListFilterText) return;
|
|
1239
|
+
conversationListFilterText = next;
|
|
1240
|
+
conversationFilterGeneration += 1;
|
|
1241
|
+
const list = getConversationListElement();
|
|
1242
|
+
if (list) list.scrollTop = 0;
|
|
1243
|
+
renderConvList();
|
|
1244
|
+
if (normalizeConversationFilter(next)) void drainConversationListForFilter();
|
|
1245
|
+
announceConversationFilterResults();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
export function initConversationFilter() {
|
|
1249
|
+
const input = document.getElementById('conv-filter-input');
|
|
1250
|
+
const clearButton = document.getElementById('conv-filter-clear');
|
|
1251
|
+
if (!input || input.dataset.filterBound === '1') return;
|
|
1252
|
+
input.dataset.filterBound = '1';
|
|
1253
|
+
let debounceTimer = null;
|
|
1254
|
+
const syncClearButton = () => {
|
|
1255
|
+
if (clearButton) clearButton.hidden = input.value === '';
|
|
1256
|
+
};
|
|
1257
|
+
const applyFilterNow = () => {
|
|
1258
|
+
if (debounceTimer) {
|
|
1259
|
+
clearTimeout(debounceTimer);
|
|
1260
|
+
debounceTimer = null;
|
|
1261
|
+
}
|
|
1262
|
+
syncClearButton();
|
|
1263
|
+
setConversationListFilter(input.value);
|
|
1264
|
+
};
|
|
1265
|
+
input.addEventListener('input', () => {
|
|
1266
|
+
syncClearButton();
|
|
1267
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1268
|
+
debounceTimer = setTimeout(applyFilterNow, CONV_FILTER_DEBOUNCE_MS);
|
|
1269
|
+
});
|
|
1270
|
+
input.addEventListener('keydown', (event) => {
|
|
1271
|
+
if (event.key !== 'Escape' || input.value === '') return;
|
|
1272
|
+
event.stopPropagation();
|
|
1273
|
+
input.value = '';
|
|
1274
|
+
applyFilterNow();
|
|
1275
|
+
});
|
|
1276
|
+
clearButton?.addEventListener('click', () => {
|
|
1277
|
+
input.value = '';
|
|
1278
|
+
applyFilterNow();
|
|
1279
|
+
input.focus();
|
|
1280
|
+
});
|
|
1281
|
+
syncClearButton();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1122
1284
|
export async function deleteConv(e, id) {
|
|
1123
1285
|
e.stopPropagation();
|
|
1124
1286
|
if (!confirm('Delete this conversation?')) return;
|
|
1125
1287
|
const result = await deleteConversationApi(id);
|
|
1126
1288
|
if (!result) return;
|
|
1289
|
+
if (result.ok === false) {
|
|
1290
|
+
showTransientRelayNotice(String(result.message || '').trim() || 'This conversation could not be deleted.', 6000);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1127
1293
|
delete conversations[id];
|
|
1128
1294
|
renderConvList();
|
|
1129
1295
|
if (currentConvId === id) {
|
|
@@ -1,6 +1,152 @@
|
|
|
1
|
+
import { tokenLabel } from './context-tier-options.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The composer placeholder, decided by the MODEL FAMILY of the current
|
|
5
|
+
* selection (maintainer's rule: the hint names who answers, whatever runtime serves
|
|
6
|
+
* it — claude-* through Cursor's catalog still reads "Message Claude…").
|
|
7
|
+
* Provider only breaks ties: Auto/empty follows the conversation's bound
|
|
8
|
+
* provider, unknown families on the OpenAI BYOK provider read OpenAI, and
|
|
9
|
+
* everything else falls back to the historical Copilot text.
|
|
10
|
+
*/
|
|
11
|
+
export function composerPlaceholderFor({ modelId = '', providerType = '' } = {}) {
|
|
12
|
+
const provider = String(providerType || '').trim().toLowerCase();
|
|
13
|
+
const id = String(modelId || '').trim().toLowerCase().replace(/\[[^\]]*\]$/, '');
|
|
14
|
+
const providerFallback = () => {
|
|
15
|
+
if (provider === 'claude') return 'Message Claude…';
|
|
16
|
+
if (provider === 'grok') return 'Message Grok…';
|
|
17
|
+
if (provider === 'openai' || provider === 'openai-byok') return 'Message OpenAI…';
|
|
18
|
+
if (provider === 'cursor') return 'Message Cursor…';
|
|
19
|
+
return 'Message Copilot…';
|
|
20
|
+
};
|
|
21
|
+
if (!id || id === 'auto') return providerFallback();
|
|
22
|
+
if (id.startsWith('claude-')) return 'Message Claude…';
|
|
23
|
+
if (id.startsWith('grok-')) return 'Message Grok…';
|
|
24
|
+
if (id.startsWith('gpt-')) return 'Message GPT…';
|
|
25
|
+
if (id.startsWith('gemini-')) return 'Message Gemini…';
|
|
26
|
+
// Composer is Cursor's house model family.
|
|
27
|
+
if (id.startsWith('composer-')) return 'Message Cursor…';
|
|
28
|
+
return providerFallback();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Human label for a model id, tuned to fit narrow composer selects.
|
|
33
|
+
*
|
|
34
|
+
* Claude ids drop the redundant "claude-" prefix (Opus/Sonnet/Haiku/Fable are
|
|
35
|
+
* unmistakably Claude), join hyphenated version parts with dots, and drop
|
|
36
|
+
* trailing -YYYYMMDD snapshot dates: `claude-fable-5-1` -> "Fable 5.1",
|
|
37
|
+
* `claude-haiku-4-5-20251001` -> "Haiku 4.5". A bracketed capability suffix
|
|
38
|
+
* survives verbatim: `claude-opus-5[1m]` -> "Opus 5 [1m]".
|
|
39
|
+
*/
|
|
40
|
+
export function humanizeModelLabel(modelId = '') {
|
|
41
|
+
const text = String(modelId || '').trim();
|
|
42
|
+
if (!text) return '';
|
|
43
|
+
if (/^gpt-/i.test(text)) {
|
|
44
|
+
return text
|
|
45
|
+
.replace(/^gpt-/i, 'GPT-')
|
|
46
|
+
.replace(/-codex$/i, ' Codex')
|
|
47
|
+
.replace(/-mini$/i, ' Mini');
|
|
48
|
+
}
|
|
49
|
+
if (/^claude-/i.test(text)) {
|
|
50
|
+
const suffixMatch = /\[([^\]]+)\]$/.exec(text);
|
|
51
|
+
const base = suffixMatch ? text.slice(0, suffixMatch.index) : text;
|
|
52
|
+
const parts = base
|
|
53
|
+
.replace(/^claude-/i, '')
|
|
54
|
+
.split('-')
|
|
55
|
+
.filter((part, index, all) => !(index === all.length - 1 && /^\d{8}$/.test(part)));
|
|
56
|
+
const words = [];
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
const isNumeric = /^\d+(\.\d+)?$/.test(part);
|
|
59
|
+
if (isNumeric && words.length && words[words.length - 1].isNumeric) {
|
|
60
|
+
words[words.length - 1].text += `.${part}`;
|
|
61
|
+
} else {
|
|
62
|
+
words.push({ text: isNumeric ? part : part.charAt(0).toUpperCase() + part.slice(1), isNumeric });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const label = words.map((word) => word.text).join(' ');
|
|
66
|
+
return suffixMatch ? `${label} [${suffixMatch[1]}]` : label;
|
|
67
|
+
}
|
|
68
|
+
if (/^gemini-/i.test(text)) {
|
|
69
|
+
return text
|
|
70
|
+
.replace(/^gemini-/i, 'Gemini ')
|
|
71
|
+
.split('-')
|
|
72
|
+
.map((part) => (/^\d+(\.\d+)?$/.test(part) ? part : (part.charAt(0).toUpperCase() + part.slice(1))))
|
|
73
|
+
.join(' ');
|
|
74
|
+
}
|
|
75
|
+
return text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const VARIANT_EFFORT_SUFFIX = /^(.*)-(none|minimal|low|medium|high|xhigh|max)$/i;
|
|
79
|
+
|
|
80
|
+
// "gpt-5-high" style ids carry the effort as a suffix; the label shows it in
|
|
81
|
+
// parentheses and the metadata lookup uses the base id.
|
|
82
|
+
export function splitModelVariantId(modelVariantId = '') {
|
|
83
|
+
const value = String(modelVariantId || '').trim();
|
|
84
|
+
if (!value) return { baseModelId: '', reasoningEffort: null };
|
|
85
|
+
const match = value.match(VARIANT_EFFORT_SUFFIX);
|
|
86
|
+
if (!match) return { baseModelId: value, reasoningEffort: null };
|
|
87
|
+
return {
|
|
88
|
+
baseModelId: String(match[1] || '').trim(),
|
|
89
|
+
reasoningEffort: String(match[2] || '').trim().toLowerCase(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Server catalogs key metadata by the id they publish; stored preferences and
|
|
94
|
+
// variant ids may differ only in case.
|
|
95
|
+
export function modelMetadataFor(modelId = '', metadataByModel = {}) {
|
|
96
|
+
const map = metadataByModel && typeof metadataByModel === 'object' ? metadataByModel : {};
|
|
97
|
+
const { baseModelId } = splitModelVariantId(modelId);
|
|
98
|
+
if (!baseModelId) return null;
|
|
99
|
+
const direct = map[baseModelId];
|
|
100
|
+
if (direct && typeof direct === 'object') return direct;
|
|
101
|
+
const lower = baseModelId.toLowerCase();
|
|
102
|
+
const key = Object.keys(map).find((candidate) => candidate.toLowerCase() === lower);
|
|
103
|
+
return key && map[key] && typeof map[key] === 'object' ? map[key] : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Number(null) is 0, which would put every unindexed model first.
|
|
107
|
+
function finiteIndexOrNull(value) {
|
|
108
|
+
if (value === null || value === undefined || value === '') return null;
|
|
109
|
+
const index = Number(value);
|
|
110
|
+
return Number.isFinite(index) && index >= 0 ? index : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function catalogOrderFor(modelId = '', metadataByModel = {}) {
|
|
114
|
+
return finiteIndexOrNull(modelMetadataFor(modelId, metadataByModel)?.catalogIndex);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Runtime display name when the catalog has one ("GPT-5.4 mini"), otherwise
|
|
118
|
+
// the compact humanized id; variant ids keep their "(effort)" tail.
|
|
119
|
+
export function catalogModelLabel(modelId = '', metadataByModel = {}, { autoValue = 'auto' } = {}) {
|
|
120
|
+
const value = String(modelId || '').trim();
|
|
121
|
+
if (value.toLowerCase() === String(autoValue || 'auto').trim().toLowerCase()) return 'Auto';
|
|
122
|
+
const { baseModelId, reasoningEffort } = splitModelVariantId(value);
|
|
123
|
+
if (!baseModelId) return value;
|
|
124
|
+
const displayName = String(modelMetadataFor(value, metadataByModel)?.displayName || '').trim();
|
|
125
|
+
const baseLabel = displayName || humanizeModelLabel(baseModelId);
|
|
126
|
+
return reasoningEffort ? `${baseLabel} (${reasoningEffort})` : baseLabel;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function contextWindowSuffix(modelId = '', metadataByModel = {}) {
|
|
130
|
+
const metadata = modelMetadataFor(modelId, metadataByModel);
|
|
131
|
+
if (!metadata) return '';
|
|
132
|
+
const real = Number(metadata.contextWindowTokens);
|
|
133
|
+
const tokens = Number.isFinite(real) && real > 0 ? real : Number(metadata.defaultContextLimitTokens);
|
|
134
|
+
return Number.isFinite(tokens) && tokens > 0 ? ` · ${tokenLabel(tokens)}` : '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Auto first, then the server's catalog order where it supplies one
|
|
139
|
+
* (orderFor → finite index), then everything else alphabetically by bare
|
|
140
|
+
* label. Copilot's canonical order therefore wins for Copilot models while
|
|
141
|
+
* SDK-only catalogs (no index) keep the alphabetical list they always had.
|
|
142
|
+
* suffixFor is applied after sorting and after collision detection so the
|
|
143
|
+
* sort key and the "same label" check never see the window annotation.
|
|
144
|
+
*/
|
|
1
145
|
export function normalizeModelSelectorOptions(models = [], {
|
|
2
146
|
autoValue = 'auto',
|
|
3
147
|
labelFor = (modelId) => modelId,
|
|
148
|
+
orderFor = () => null,
|
|
149
|
+
suffixFor = () => '',
|
|
4
150
|
} = {}) {
|
|
5
151
|
const normalizedAuto = String(autoValue || 'auto').trim() || 'auto';
|
|
6
152
|
const values = Array.from(new Set(
|
|
@@ -8,7 +154,10 @@ export function normalizeModelSelectorOptions(models = [], {
|
|
|
8
154
|
.map((modelId) => String(modelId || '').trim())
|
|
9
155
|
.filter(Boolean),
|
|
10
156
|
)).filter((modelId) => modelId.toLowerCase() !== normalizedAuto.toLowerCase());
|
|
157
|
+
const orderOf = (modelId) => finiteIndexOrNull(orderFor(modelId)) ?? Number.POSITIVE_INFINITY;
|
|
11
158
|
values.sort((left, right) => {
|
|
159
|
+
const indexOrder = orderOf(left) - orderOf(right);
|
|
160
|
+
if (indexOrder) return indexOrder;
|
|
12
161
|
const labelOrder = String(labelFor(left) || left).localeCompare(
|
|
13
162
|
String(labelFor(right) || right),
|
|
14
163
|
undefined,
|
|
@@ -16,10 +165,45 @@ export function normalizeModelSelectorOptions(models = [], {
|
|
|
16
165
|
);
|
|
17
166
|
return labelOrder || left.localeCompare(right);
|
|
18
167
|
});
|
|
19
|
-
|
|
168
|
+
const options = [
|
|
20
169
|
{ value: normalizedAuto, label: String(labelFor(normalizedAuto) || normalizedAuto) },
|
|
21
170
|
...values.map((value) => ({ value, label: String(labelFor(value) || value) })),
|
|
22
171
|
];
|
|
172
|
+
// Date-stripping in humanizeModelLabel can collapse an alias and its dated
|
|
173
|
+
// snapshot ("claude-fable-5-1" + "claude-fable-5-1-20251103") into the same
|
|
174
|
+
// label; two indistinguishable rows are worse than one ugly one, so
|
|
175
|
+
// colliding labels fall back to the raw id.
|
|
176
|
+
const labelCounts = new Map();
|
|
177
|
+
for (const option of options) {
|
|
178
|
+
labelCounts.set(option.label, (labelCounts.get(option.label) || 0) + 1);
|
|
179
|
+
}
|
|
180
|
+
return options.map((option) => {
|
|
181
|
+
const label = labelCounts.get(option.label) > 1 ? option.value : option.label;
|
|
182
|
+
const suffix = option.value === normalizedAuto ? '' : String(suffixFor(option.value) || '');
|
|
183
|
+
return { value: option.value, label: `${label}${suffix}` };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The one option list both pickers render (composer #model-select and the
|
|
189
|
+
* New Chat modal), so they cannot disagree on order or wording. Callers filter
|
|
190
|
+
* by provider afterwards; annotateContextWindow is off for Claude SDK
|
|
191
|
+
* conversations, whose window the merged metadata does not know (see
|
|
192
|
+
* buildContextTierOptions).
|
|
193
|
+
*/
|
|
194
|
+
export function buildCatalogModelOptions(models = [], {
|
|
195
|
+
autoValue = 'auto',
|
|
196
|
+
metadataByModel = {},
|
|
197
|
+
annotateContextWindow = true,
|
|
198
|
+
} = {}) {
|
|
199
|
+
return normalizeModelSelectorOptions(models, {
|
|
200
|
+
autoValue,
|
|
201
|
+
labelFor: (modelId) => catalogModelLabel(modelId, metadataByModel, { autoValue }),
|
|
202
|
+
orderFor: (modelId) => catalogOrderFor(modelId, metadataByModel),
|
|
203
|
+
suffixFor: annotateContextWindow
|
|
204
|
+
? (modelId) => contextWindowSuffix(modelId, metadataByModel)
|
|
205
|
+
: () => '',
|
|
206
|
+
});
|
|
23
207
|
}
|
|
24
208
|
|
|
25
209
|
export function modelSelectorOptionsEqual(currentOptions = [], nextOptions = []) {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { nearestSupportedReasoningEffort } from './conversation-preferences.mjs';
|
|
2
|
+
import { buildContextTierOptions, UNKNOWN_WINDOW_LABEL } from './context-tier-options.mjs';
|
|
3
|
+
import { modelMetadataFor } from './model-selector-options.mjs';
|
|
4
|
+
|
|
1
5
|
export function shouldPromptForNewConversationModel({ provider = '' } = {}) {
|
|
2
6
|
const normalizedProvider = String(provider || '').trim().toLowerCase();
|
|
3
7
|
return normalizedProvider === 'openai'
|
|
@@ -54,14 +58,30 @@ export function reasoningChoicesForProviderModel(catalog = {}, {
|
|
|
54
58
|
return normalizeReasoningEfforts(catalog?.reasoningByModel?.[normalizedModelId] || []);
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
// Same clamp as the composer (nearestSupportedReasoningEffort) so a remembered
|
|
62
|
+
// tier lands on the same rung whichever picker starts the chat.
|
|
57
63
|
export function resolvePreferredReasoningEffort(efforts = [], preferredValues = []) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
return nearestSupportedReasoningEffort(
|
|
65
|
+
Array.isArray(preferredValues) ? preferredValues : [preferredValues],
|
|
66
|
+
normalizeReasoningEfforts(efforts),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The read-only context row of the New Chat modal. Only Copilot chats show it:
|
|
72
|
+
* the modal has no tier preference to persist (the composer's long_context
|
|
73
|
+
* choice is stored only as a Claude "[1m]" id), so it mirrors what the composer
|
|
74
|
+
* chip will read for the chosen model and hides when no window is known
|
|
75
|
+
* ("auto", stale metadata).
|
|
76
|
+
*/
|
|
77
|
+
export function newConversationContextTierState(catalog = {}, { provider = '', modelId = '' } = {}) {
|
|
78
|
+
const providerKey = normalizeProviderKey(provider);
|
|
79
|
+
if (providerKey !== 'github') return { visible: false, options: [] };
|
|
80
|
+
const options = buildContextTierOptions({
|
|
81
|
+
modelId,
|
|
82
|
+
providerType: providerKey,
|
|
83
|
+
metadata: modelMetadataFor(modelId, catalog?.modelMetadataByModel || {}) || {},
|
|
84
|
+
});
|
|
85
|
+
const known = options.filter((option) => option.label !== UNKNOWN_WINDOW_LABEL);
|
|
86
|
+
return { visible: known.length > 0, options: known };
|
|
67
87
|
}
|