mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 12:16:54 +00:00
feat(sessions): context usage progress bar with token estimation
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.
This commit is contained in:
1 parent
a4f2d02a80
commit
fab9028b05
7 files changed
+105
-22
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
"columns": {
|
||||
"session": "Session",
|
||||
"agent": "Agent",
|
||||
"channel": "Channel",
|
||||
"context": "上下文",
|
||||
"messages": "消息",
|
||||
"updated": "更新时间"
|
||||
},
|
||||
"contextBar": {
|
||||
"compacted": "已压缩 {{count}} 次"
|
||||
},
|
||||
"detail": {
|
||||
"reset": "重置",
|
||||
"delete": "删除",
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<table className="w-full min-w-[600px]">
|
||||
<table className="w-full min-w-[750px]">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">{t("columns.session")}</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">{t("columns.agent")}</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">{t("columns.channel")}</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">{t("columns.context")}</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium">{t("columns.messages")}</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium">{t("columns.updated")}</th>
|
||||
</tr>
|
||||
@@ -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({
|
||||
<div className="text-sm font-medium">
|
||||
{session.metadata?.chat_title || session.metadata?.display_name || session.label || parsed.scope}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{session.metadata?.username ? `@${session.metadata.username}` : session.key}
|
||||
{session.channel && session.channel !== "ws" && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0">{session.channel}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline">{session.agentName || parsed.agentId}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{session.channel || "ws"}
|
||||
<td className="px-4 py-3">
|
||||
<ContextUsageBar
|
||||
estimatedTokens={session.estimatedTokens ?? 0}
|
||||
contextWindow={session.contextWindow ?? 0}
|
||||
compactionCount={session.compactionCount ?? 0}
|
||||
t={t}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm">{session.messageCount}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-muted-foreground">
|
||||
@@ -157,3 +166,54 @@ function SessionRow({
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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, unknown>) => string;
|
||||
}) {
|
||||
if (contextWindow <= 0) return <span className="text-xs text-muted-foreground">—</span>;
|
||||
|
||||
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 (
|
||||
<div className="flex items-center gap-2 min-w-[120px]">
|
||||
<div className="flex-1">
|
||||
<div
|
||||
className="h-2 w-full rounded-full bg-muted overflow-hidden"
|
||||
title={tooltip}
|
||||
>
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${barColor}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<span>{formatTokens(estimatedTokens)} / {formatTokens(contextWindow)}</span>
|
||||
{compactionCount > 0 && (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5"
|
||||
title={t("contextBar.compacted", { count: compactionCount })}
|
||||
>
|
||||
· <RefreshCw className="h-2.5 w-2.5" />{compactionCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,9 @@ export interface SessionInfo {
|
||||
userID?: string;
|
||||
metadata?: Record<string, string>;
|
||||
agentName?: string;
|
||||
estimatedTokens?: number;
|
||||
contextWindow?: number;
|
||||
compactionCount?: number;
|
||||
}
|
||||
|
||||
export interface SessionPreview {
|
||||
|
||||
Reference in new issue
Block a user