diff --git a/apps/server/src/routes/chats.ts b/apps/server/src/routes/chats.ts index 5d0c45f..c7a072d 100644 --- a/apps/server/src/routes/chats.ts +++ b/apps/server/src/routes/chats.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import type { Sql } from '../db.js'; import type { Broker } from '../services/broker.js'; import type { Chat, Message } from '../types/api.js'; +import { getModelContext } from '../services/model-context.js'; const CreateBody = z.object({ name: z.string().min(1).max(200).optional(), @@ -60,7 +61,20 @@ export function registerChatRoutes( WHERE c.session_id = ${req.params.id} AND c.status = ${status} ORDER BY c.updated_at DESC `; - return rows; + // v1.11.5: enrich each chat with its model's context window so the + // ContextBar can render a zero-state (and the auto-compaction threshold + // tooltip) before the first assistant message lands. All chats in a + // session share the session's model, so we do ONE getModelContext + // lookup and apply the result to the whole list. Failed lookups + // (model unknown, llama-swap down) yield null and the frontend falls + // through to the "model context unknown" placeholder. + const sessRow = await sql<{ model: string | null }[]>` + SELECT model FROM sessions WHERE id = ${req.params.id} + `; + const sessionModel = sessRow[0]?.model ?? null; + const mctx = sessionModel ? await getModelContext(sessionModel) : null; + const modelContextLimit = mctx?.n_ctx ?? null; + return rows.map((r) => ({ ...r, model_context_limit: modelContextLimit })); } ); diff --git a/apps/server/src/types/api.ts b/apps/server/src/types/api.ts index 54f5a04..38cb781 100644 --- a/apps/server/src/types/api.ts +++ b/apps/server/src/types/api.ts @@ -89,6 +89,12 @@ export interface Chat { message_count?: number; last_message_preview?: string | null; effective_context_tokens?: number | null; + // v1.11.5: model's full context window (from llama-swap props), threaded + // to the frontend so ContextBar can render a zero-state + the auto- + // compaction threshold tooltip before any assistant message lands. + // Shared across all chats in a session (chats inherit session.model). + // null when the upstream lookup failed (model unknown, llama-swap down). + model_context_limit?: number | null; } // KEEP IN SYNC: apps/server/src/schema.sql messages_role_chk / messages_status_chk diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts index 9784382..cc359c9 100644 --- a/apps/web/src/api/types.ts +++ b/apps/web/src/api/types.ts @@ -80,6 +80,12 @@ export interface Chat { message_count?: number; last_message_preview?: string | null; effective_context_tokens?: number | null; + // v1.11.5: model's full context window from llama-swap /props. Used by + // ContextBar to render the zero-state + auto-compaction threshold tooltip + // before any assistant message exists in the chat. null when upstream + // lookup failed (model unknown, llama-swap unreachable) — UI degrades + // to a "model context unknown" placeholder. + model_context_limit?: number | null; } export type MessageRole = 'user' | 'assistant' | 'tool' | 'system'; diff --git a/apps/web/src/components/ChatContextPopover.tsx b/apps/web/src/components/ChatContextPopover.tsx deleted file mode 100644 index a08cc9d..0000000 --- a/apps/web/src/components/ChatContextPopover.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type { ChatContextStats } from '@/hooks/useChatContextStats'; - -interface Props { - stats: ChatContextStats | null; -} - -/** - * Formats a token count into a compact k/m-suffix string. - * - < 1_000 → raw integer (e.g. "42") - * - 1_000–999_999 → "Nk" or "N.Nk" (e.g. "30k", "12.5k", "100k") - * - >= 1_000_000 → "Nm" or "N.Nm" (e.g. "1m", "1.5m", "100m") - * - * Drops a trailing ".0" so we get "30k" instead of "30.0k". - */ -function formatTokens(n: number): string { - if (n < 1000) return String(n); - if (n < 1_000_000) { - const k = n / 1000; - return k >= 100 ? `${Math.round(k)}k` : `${k.toFixed(1).replace(/\.0$/, '')}k`; - } - const m = n / 1_000_000; - return m >= 100 ? `${Math.round(m)}m` : `${m.toFixed(1).replace(/\.0$/, '')}m`; -} - -/** - * Color thresholds: - * - > 85% → text-destructive - * - >= 60% → text-amber-500 - * - else → text-muted-foreground - * (85% itself falls into the amber band.) - */ -function percentColorClass(percent: number): string { - if (percent > 85) return 'text-destructive'; - if (percent >= 60) return 'text-amber-500'; - return 'text-muted-foreground'; -} - -export function ChatContextPopover({ stats }: Props) { - if (!stats) return null; - return ( -