From fab9028b059c8c4956bd8dbfd11486f315059fb1 Mon Sep 17 00:00:00 2001 From: viettranx Date: Tue, 10 Mar 2026 20:32:58 +0700 Subject: [PATCH] feat(sessions): context usage progress bar with token estimation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace channel column with context usage bar showing estimated tokens vs compaction threshold. Token count estimated from messages JSONB (octet_length/4 + 12K system prompt) — no migration needed. Color-coded: green <60%, amber 60-85%, red >85%. Inline compaction count with refresh icon. --- internal/store/pg/sessions_list.go | 22 ++++-- internal/store/session_store.go | 13 ++-- ui/web/src/i18n/locales/en/sessions.json | 5 +- ui/web/src/i18n/locales/vi/sessions.json | 5 +- ui/web/src/i18n/locales/zh/sessions.json | 5 +- ui/web/src/pages/sessions/sessions-page.tsx | 74 +++++++++++++++++++-- ui/web/src/types/session.ts | 3 + 7 files changed, 105 insertions(+), 22 deletions(-) diff --git a/internal/store/pg/sessions_list.go b/internal/store/pg/sessions_list.go index 0fe3a3f5..c669e748 100644 --- a/internal/store/pg/sessions_list.go +++ b/internal/store/pg/sessions_list.go @@ -161,7 +161,10 @@ func (s *PGSessionStore) ListPagedRich(opts store.SessionListOpts) store.Session const richCols = `s.session_key, jsonb_array_length(s.messages), s.created_at, s.updated_at, s.label, s.channel, s.user_id, COALESCE(s.metadata, '{}'), s.model, s.provider, s.input_tokens, s.output_tokens, - COALESCE(a.display_name, '')` + COALESCE(a.display_name, ''), + octet_length(s.messages::text) / 4 + 12000, + COALESCE(a.context_window, 200000), + s.compaction_count` if opts.AgentID != "" { selectQ = `SELECT ` + richCols + ` @@ -191,8 +194,10 @@ func (s *PGSessionStore) ListPagedRich(opts store.SessionListOpts) store.Session var model, provider *string var inputTokens, outputTokens int64 var agentName string + var estimatedTokens, contextWindow, compactionCount int if err := rows.Scan(&key, &msgCount, &createdAt, &updatedAt, &label, &channel, &userID, &metaJSON, - &model, &provider, &inputTokens, &outputTokens, &agentName); err != nil { + &model, &provider, &inputTokens, &outputTokens, &agentName, + &estimatedTokens, &contextWindow, &compactionCount); err != nil { continue } var meta map[string]string @@ -210,11 +215,14 @@ func (s *PGSessionStore) ListPagedRich(opts store.SessionListOpts) store.Session UserID: derefStr(userID), Metadata: meta, }, - Model: derefStr(model), - Provider: derefStr(provider), - InputTokens: inputTokens, - OutputTokens: outputTokens, - AgentName: agentName, + Model: derefStr(model), + Provider: derefStr(provider), + InputTokens: inputTokens, + OutputTokens: outputTokens, + AgentName: agentName, + EstimatedTokens: estimatedTokens, + ContextWindow: contextWindow, + CompactionCount: compactionCount, }) } if result == nil { diff --git a/internal/store/session_store.go b/internal/store/session_store.go index 460e2ee8..a510d8b0 100644 --- a/internal/store/session_store.go +++ b/internal/store/session_store.go @@ -65,11 +65,14 @@ type SessionListResult struct { // SessionInfoRich is an enriched session info for API responses (includes model, tokens, agent name). type SessionInfoRich struct { SessionInfo - Model string `json:"model,omitempty"` - Provider string `json:"provider,omitempty"` - InputTokens int64 `json:"inputTokens,omitempty"` - OutputTokens int64 `json:"outputTokens,omitempty"` - AgentName string `json:"agentName,omitempty"` + Model string `json:"model,omitempty"` + Provider string `json:"provider,omitempty"` + InputTokens int64 `json:"inputTokens,omitempty"` + OutputTokens int64 `json:"outputTokens,omitempty"` + AgentName string `json:"agentName,omitempty"` + EstimatedTokens int `json:"estimatedTokens,omitempty"` // estimated current context tokens (messages bytes/4 + 12k system prompt) + ContextWindow int `json:"contextWindow,omitempty"` // agent's context window size + CompactionCount int `json:"compactionCount,omitempty"` // number of compactions performed } // SessionListRichResult is the paginated result of ListPagedRich. diff --git a/ui/web/src/i18n/locales/en/sessions.json b/ui/web/src/i18n/locales/en/sessions.json index 78a0f0b3..184aa485 100644 --- a/ui/web/src/i18n/locales/en/sessions.json +++ b/ui/web/src/i18n/locales/en/sessions.json @@ -9,10 +9,13 @@ "columns": { "session": "Session", "agent": "Agent", - "channel": "Channel", + "context": "Context", "messages": "Messages", "updated": "Updated" }, + "contextBar": { + "compacted": "Compacted {{count}} time(s)" + }, "detail": { "reset": "Reset", "delete": "Delete", diff --git a/ui/web/src/i18n/locales/vi/sessions.json b/ui/web/src/i18n/locales/vi/sessions.json index d265749f..138fce56 100644 --- a/ui/web/src/i18n/locales/vi/sessions.json +++ b/ui/web/src/i18n/locales/vi/sessions.json @@ -9,10 +9,13 @@ "columns": { "session": "Session", "agent": "Agent", - "channel": "Channel", + "context": "Ngữ cảnh", "messages": "Tin nhắn", "updated": "Cập nhật" }, + "contextBar": { + "compacted": "Đã nén {{count}} lần" + }, "detail": { "reset": "Đặt lại", "delete": "Xóa", diff --git a/ui/web/src/i18n/locales/zh/sessions.json b/ui/web/src/i18n/locales/zh/sessions.json index d0b3476d..677708fd 100644 --- a/ui/web/src/i18n/locales/zh/sessions.json +++ b/ui/web/src/i18n/locales/zh/sessions.json @@ -9,10 +9,13 @@ "columns": { "session": "Session", "agent": "Agent", - "channel": "Channel", + "context": "上下文", "messages": "消息", "updated": "更新时间" }, + "contextBar": { + "compacted": "已压缩 {{count}} 次" + }, "detail": { "reset": "重置", "delete": "删除", diff --git a/ui/web/src/pages/sessions/sessions-page.tsx b/ui/web/src/pages/sessions/sessions-page.tsx index db552827..e9cfa0bb 100644 --- a/ui/web/src/pages/sessions/sessions-page.tsx +++ b/ui/web/src/pages/sessions/sessions-page.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useParams, useNavigate } from "react-router"; import { useTranslation } from "react-i18next"; -import { History } from "lucide-react"; +import { History, RefreshCw } from "lucide-react"; import { PageHeader } from "@/components/shared/page-header"; import { EmptyState } from "@/components/shared/empty-state"; import { SearchInput } from "@/components/shared/search-input"; @@ -12,7 +12,7 @@ import { useDeferredLoading } from "@/hooks/use-deferred-loading"; import { useSessions } from "./hooks/use-sessions"; import { SessionDetailPage } from "./session-detail-page"; import { parseSessionKey } from "@/lib/session-key"; -import { formatRelativeTime } from "@/lib/format"; +import { formatRelativeTime, formatTokens } from "@/lib/format"; import type { SessionInfo } from "@/types/session"; export function SessionsPage() { @@ -87,12 +87,12 @@ export function SessionsPage() { /> ) : (
- +
- + @@ -129,6 +129,7 @@ function SessionRow({ session: SessionInfo; onClick: () => void; }) { + const { t } = useTranslation("sessions"); const parsed = parseSessionKey(session.key); return ( @@ -140,15 +141,23 @@ function SessionRow({
{session.metadata?.chat_title || session.metadata?.display_name || session.label || parsed.scope}
-
+
{session.metadata?.username ? `@${session.metadata.username}` : session.key} + {session.channel && session.channel !== "ws" && ( + {session.channel} + )}
- ); } + +/** Inline context usage progress bar with compaction count. */ +function ContextUsageBar({ + estimatedTokens, + contextWindow, + compactionCount, + t, +}: { + estimatedTokens: number; + contextWindow: number; + compactionCount: number; + t: (key: string, opts?: Record) => string; +}) { + if (contextWindow <= 0) return —; + + const threshold = contextWindow * 0.75; + const pct = Math.min(Math.round((estimatedTokens / threshold) * 100), 100); + + let barColor = "bg-emerald-500"; + if (pct >= 85) barColor = "bg-red-500"; + else if (pct >= 60) barColor = "bg-amber-500"; + + const tooltip = `~${formatTokens(estimatedTokens)} / ${formatTokens(contextWindow)} tokens (${pct}%)`; + + return ( +
+
+
+
+
+
+ {formatTokens(estimatedTokens)} / {formatTokens(contextWindow)} + {compactionCount > 0 && ( + + · {compactionCount} + + )} +
+
+
+ ); +} diff --git a/ui/web/src/types/session.ts b/ui/web/src/types/session.ts index 52554670..f299d3c0 100644 --- a/ui/web/src/types/session.ts +++ b/ui/web/src/types/session.ts @@ -14,6 +14,9 @@ export interface SessionInfo { userID?: string; metadata?: Record; agentName?: string; + estimatedTokens?: number; + contextWindow?: number; + compactionCount?: number; } export interface SessionPreview {
{t("columns.session")} {t("columns.agent")}{t("columns.channel")}{t("columns.context")} {t("columns.messages")} {t("columns.updated")}
{session.agentName || parsed.agentId} - {session.channel || "ws"} + + {session.messageCount} @@ -157,3 +166,54 @@ function SessionRow({