diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index c9f97369..fad3cd41 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -102,6 +102,27 @@ const STEP_EVENT_FLUSH_MS = 250; const TELEMETRY_OUTPUT_MAX_CHARS = 1600; const TELEMETRY_MERGE_WINDOW_MS = 20000; const LOCAL_PROVIDER_SET = new Set(LOCAL_PROVIDERS); +const MODEL_PREFLIGHT_CACHE_TTL_MS = 45_000; +let cachedModelCatalog: { expiresAt: number; models: ModelInfo[] } | null = null; + +async function fetchCachedModelCatalog(): Promise { + const now = Date.now(); + + if (cachedModelCatalog && cachedModelCatalog.expiresAt > now) { + return cachedModelCatalog.models; + } + + const response = await fetch('/api/models'); + const data = (await response.json()) as { modelList?: ModelInfo[] }; + const models = data.modelList || []; + + cachedModelCatalog = { + models, + expiresAt: now + MODEL_PREFLIGHT_CACHE_TTL_MS, + }; + + return models; +} const ANSI_ESCAPE_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g; const CARRIAGE_RETURN_RE = /\r+/g; const loadSessionManager = () => import('~/lib/services/sessionManager'); @@ -673,7 +694,10 @@ export const ChatImpl = memo( providerSettings: getProviderSettingsFromCookiesSafe(), selectedProvider: provider.name, selectedModel: model, - files, + files: + hostedRuntimeEnabled && typeof workbenchStore.hostedRuntimeSessionId === 'string' + ? undefined + : files, hostedRuntimeSessionId: hostedRuntimeEnabled ? workbenchStore.hostedRuntimeSessionId : undefined, projectContextId, promptId, @@ -2290,9 +2314,7 @@ Requirements: const resolveModelSelection = useCallback( async (prompt: string, currentModel: string, currentProvider: ProviderInfo) => { try { - const response = await fetch('/api/models'); - const data = (await response.json()) as { modelList: ModelInfo[] }; - const availableModels = data.modelList || []; + const availableModels = await fetchCachedModelCatalog(); const decision = selectModelForPrompt({ prompt, currentModel, @@ -3354,6 +3376,7 @@ CONTINUE IMMEDIATELY: setApiKeysCookie(updatedApiKeys, CHAT_SELECTION_COOKIE_EXPIRY_DAYS); const normalizedKey = apiKey.trim(); + cachedModelCatalog = null; if (!normalizedKey) { return; diff --git a/app/components/header/Shoutbox.client.tsx b/app/components/header/Shoutbox.client.tsx index baf0af5c..d362a066 100644 --- a/app/components/header/Shoutbox.client.tsx +++ b/app/components/header/Shoutbox.client.tsx @@ -38,6 +38,7 @@ export function Shoutbox() { const [content, setContent] = useState(''); const [loading, setLoading] = useState(false); const [enabled, setEnabled] = useState(isShoutboxEnabled); + const [canSend, setCanSend] = useState(false); const [lastReadAt, setLastReadAt] = useState(() => localStorage.getItem(SHOUTBOX_LAST_READ_AT_KEY)); const panelRef = useRef(null); @@ -67,7 +68,7 @@ export function Shoutbox() { const loadMessages = async () => { try { const response = await fetch('/api/shout/messages'); - const payload = (await response.json()) as { messages?: unknown; error?: string }; + const payload = (await response.json()) as { messages?: unknown; error?: string; canSend?: boolean }; if (!response.ok) { throw new Error(payload.error || 'Failed to load shout-out messages.'); @@ -75,6 +76,7 @@ export function Shoutbox() { if (active) { setMessages(normalizeShoutMessages(payload.messages)); + setCanSend(Boolean(payload.canSend)); } } catch (error) { console.error('Failed to load shout-out messages:', error); @@ -225,21 +227,30 @@ export function Shoutbox() {
-