feat: rename info to status, add /status slash command

Rename session info endpoint from /sessions/:id/info to /sessions/:id/status
and update frontend tab label accordingly. Add /status slash command that
displays current session metrics (message count, context usage, cache hit
rate, used skills) as formatted text in any channel.
This commit is contained in:
Acbox
2026-04-02 03:28:48 +08:00
parent b3c783fb0b
commit 33b57ee345
14 changed files with 154 additions and 28 deletions
@@ -289,11 +289,11 @@
/>
</div>
<div
v-if="activeRightTab === 'info'"
v-if="activeRightTab === 'status'"
class="absolute inset-0"
>
<SessionInfoPanel
:visible="activeRightTab === 'info'"
:visible="activeRightTab === 'status'"
:override-model-id="overrideModelId"
/>
</div>
@@ -398,7 +398,7 @@ const reasoningPopoverOpen = ref(false)
// ---- Right sidebar panel ----
type RightTabId = 'terminal' | 'files' | 'info'
type RightTabId = 'terminal' | 'files' | 'status'
interface RightTab {
id: RightTabId
@@ -409,7 +409,7 @@ interface RightTab {
const rightTabs = computed<RightTab[]>(() => [
{ id: 'terminal', label: 'Terminal', icon: TerminalSquare },
{ id: 'files', label: t('chat.files'), icon: FolderOpen },
{ id: 'info', label: 'Info', icon: BarChart3 },
{ id: 'status', label: 'Status', icon: BarChart3 },
])
const activeRightTab = ref<RightTabId | null>(null)
@@ -104,7 +104,7 @@ import { storeToRefs } from 'pinia'
import { useQuery } from '@pinia/colada'
import { Sparkles, ExternalLink } from 'lucide-vue-next'
import { ScrollArea } from '@memohai/ui'
import { getBotsByBotIdSessionsBySessionIdInfo } from '@memohai/sdk'
import { getBotsByBotIdSessionsBySessionIdStatus } from '@memohai/sdk'
import type { HandlersSessionInfoResponse } from '@memohai/sdk'
import { useChatStore } from '@/store/chat-list'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
@@ -119,9 +119,9 @@ const { currentBotId, sessionId } = storeToRefs(chatStore)
const openInFileManager = inject(openInFileManagerKey, undefined)
const { data: info } = useQuery({
key: () => ['session-info', currentBotId.value ?? '', sessionId.value ?? '', props.overrideModelId ?? ''],
key: () => ['session-status', currentBotId.value ?? '', sessionId.value ?? '', props.overrideModelId ?? ''],
query: async () => {
const { data } = await getBotsByBotIdSessionsBySessionIdInfo({
const { data } = await getBotsByBotIdSessionsBySessionIdStatus({
path: {
bot_id: currentBotId.value!,
session_id: sessionId.value!,
+9
View File
@@ -22,6 +22,15 @@ FROM bot_history_messages m
WHERE m.session_id = sqlc.arg(session_id)
AND m.usage IS NOT NULL;
-- name: GetLatestSessionIDByBot :one
SELECT s.id
FROM bot_sessions s
WHERE s.bot_id = sqlc.arg(bot_id)
AND s.type = 'chat'
AND s.deleted_at IS NULL
ORDER BY s.updated_at DESC
LIMIT 1;
-- name: GetSessionUsedSkills :many
SELECT DISTINCT
(part->'input'->>'skillName')::text AS skill_name
+1
View File
@@ -15,5 +15,6 @@ func (h *Handler) buildRegistry() *Registry {
r.RegisterGroup(h.buildHeartbeatGroup())
r.RegisterGroup(h.buildSkillGroup())
r.RegisterGroup(h.buildFSGroup())
r.RegisterGroup(h.buildStatusGroup())
return r
}
+99
View File
@@ -0,0 +1,99 @@
package command
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/jackc/pgx/v5"
)
func (h *Handler) buildStatusGroup() *CommandGroup {
g := newCommandGroup("status", "View current session status")
g.DefaultAction = "show"
g.Register(SubCommand{
Name: "show",
Usage: "show - Show current session status",
Handler: func(cc CommandContext) (string, error) {
botUUID, err := parseBotUUID(cc.BotID)
if err != nil {
return "", err
}
sessionID, err := h.queries.GetLatestSessionIDByBot(cc.Ctx, botUUID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return "No active session found.", nil
}
return "", err
}
msgCount, err := h.queries.CountMessagesBySession(cc.Ctx, sessionID)
if err != nil {
return "", fmt.Errorf("count messages: %w", err)
}
var usedTokens int64
latestUsage, err := h.queries.GetLatestAssistantUsage(cc.Ctx, sessionID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return "", fmt.Errorf("get usage: %w", err)
}
if err == nil {
usedTokens = latestUsage
}
cacheRow, err := h.queries.GetSessionCacheStats(cc.Ctx, sessionID)
if err != nil {
return "", fmt.Errorf("get cache: %w", err)
}
var contextWindowStr string
if h.settingsService != nil {
s, sErr := h.settingsService.GetBot(cc.Ctx, cc.BotID)
if sErr == nil && s.ChatModelID != "" && h.modelsService != nil {
m, mErr := h.modelsService.GetByID(cc.Ctx, s.ChatModelID)
if mErr == nil && m.Config.ContextWindow != nil {
contextWindowStr = formatTokens(int64(*m.Config.ContextWindow))
}
}
}
var cacheHitRate float64
if cacheRow.TotalInputTokens > 0 {
cacheHitRate = float64(cacheRow.CacheReadTokens) / float64(cacheRow.TotalInputTokens) * 100
}
skills, _ := h.queries.GetSessionUsedSkills(cc.Ctx, sessionID)
var b strings.Builder
b.WriteString("Session Status:\n\n")
fmt.Fprintf(&b, "- Messages: %d\n", msgCount)
if contextWindowStr != "" {
fmt.Fprintf(&b, "- Context: %s / %s\n", formatTokens(usedTokens), contextWindowStr)
} else {
fmt.Fprintf(&b, "- Context: %s\n", formatTokens(usedTokens))
}
fmt.Fprintf(&b, "- Cache Hit Rate: %.1f%%\n", cacheHitRate)
fmt.Fprintf(&b, "- Cache Read: %s\n", formatTokens(cacheRow.CacheReadTokens))
fmt.Fprintf(&b, "- Cache Write: %s\n", formatTokens(cacheRow.CacheWriteTokens))
if len(skills) > 0 {
fmt.Fprintf(&b, "- Skills: %s\n", strings.Join(skills, ", "))
}
return strings.TrimRight(b.String(), "\n"), nil
},
})
return g
}
func formatTokens(n int64) string {
if n >= 1_000_000 {
return fmt.Sprintf("%.1fM", float64(n)/1_000_000)
}
if n >= 1_000 {
return fmt.Sprintf("%.1fK", float64(n)/1_000)
}
return strconv.FormatInt(n, 10)
}
+17
View File
@@ -42,6 +42,23 @@ func (q *Queries) GetLatestAssistantUsage(ctx context.Context, sessionID pgtype.
return input_tokens, err
}
const getLatestSessionIDByBot = `-- name: GetLatestSessionIDByBot :one
SELECT s.id
FROM bot_sessions s
WHERE s.bot_id = $1
AND s.type = 'chat'
AND s.deleted_at IS NULL
ORDER BY s.updated_at DESC
LIMIT 1
`
func (q *Queries) GetLatestSessionIDByBot(ctx context.Context, botID pgtype.UUID) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, getLatestSessionIDByBot, botID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const getSessionCacheStats = `-- name: GetSessionCacheStats :one
SELECT
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS total_input_tokens,
+2 -2
View File
@@ -38,7 +38,7 @@ func NewSessionInfoHandler(log *slog.Logger, queries *sqlc.Queries, botService *
}
func (h *SessionInfoHandler) Register(e *echo.Echo) {
e.GET("/bots/:bot_id/sessions/:session_id/info", h.GetSessionInfo)
e.GET("/bots/:bot_id/sessions/:session_id/status", h.GetSessionInfo)
}
type SessionInfoResponse struct {
@@ -71,7 +71,7 @@ type CacheStats struct {
// @Failure 400 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /bots/{bot_id}/sessions/{session_id}/info [get].
// @Router /bots/{bot_id}/sessions/{session_id}/status [get].
func (h *SessionInfoHandler) GetSessionInfo(c echo.Context) error {
userID, err := RequireChannelIdentityID(c)
if err != nil {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -5256,7 +5256,7 @@ export type PatchBotsByBotIdSessionsBySessionIdResponses = {
export type PatchBotsByBotIdSessionsBySessionIdResponse = PatchBotsByBotIdSessionsBySessionIdResponses[keyof PatchBotsByBotIdSessionsBySessionIdResponses];
export type GetBotsByBotIdSessionsBySessionIdInfoData = {
export type GetBotsByBotIdSessionsBySessionIdStatusData = {
body?: never;
path: {
/**
@@ -5274,10 +5274,10 @@ export type GetBotsByBotIdSessionsBySessionIdInfoData = {
*/
model_id?: string;
};
url: '/bots/{bot_id}/sessions/{session_id}/info';
url: '/bots/{bot_id}/sessions/{session_id}/status';
};
export type GetBotsByBotIdSessionsBySessionIdInfoErrors = {
export type GetBotsByBotIdSessionsBySessionIdStatusErrors = {
/**
* Bad Request
*/
@@ -5292,16 +5292,16 @@ export type GetBotsByBotIdSessionsBySessionIdInfoErrors = {
500: HandlersErrorResponse;
};
export type GetBotsByBotIdSessionsBySessionIdInfoError = GetBotsByBotIdSessionsBySessionIdInfoErrors[keyof GetBotsByBotIdSessionsBySessionIdInfoErrors];
export type GetBotsByBotIdSessionsBySessionIdStatusError = GetBotsByBotIdSessionsBySessionIdStatusErrors[keyof GetBotsByBotIdSessionsBySessionIdStatusErrors];
export type GetBotsByBotIdSessionsBySessionIdInfoResponses = {
export type GetBotsByBotIdSessionsBySessionIdStatusResponses = {
/**
* OK
*/
200: HandlersSessionInfoResponse;
};
export type GetBotsByBotIdSessionsBySessionIdInfoResponse = GetBotsByBotIdSessionsBySessionIdInfoResponses[keyof GetBotsByBotIdSessionsBySessionIdInfoResponses];
export type GetBotsByBotIdSessionsBySessionIdStatusResponse = GetBotsByBotIdSessionsBySessionIdStatusResponses[keyof GetBotsByBotIdSessionsBySessionIdStatusResponses];
export type DeleteBotsByBotIdSettingsData = {
body?: never;
+1 -1
View File
@@ -4395,7 +4395,7 @@ const docTemplate = `{
}
}
},
"/bots/{bot_id}/sessions/{session_id}/info": {
"/bots/{bot_id}/sessions/{session_id}/status": {
"get": {
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
"tags": [
+1 -1
View File
@@ -4386,7 +4386,7 @@
}
}
},
"/bots/{bot_id}/sessions/{session_id}/info": {
"/bots/{bot_id}/sessions/{session_id}/status": {
"get": {
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
"tags": [
+1 -1
View File
@@ -5686,7 +5686,7 @@ paths:
summary: Update a session
tags:
- sessions
/bots/{bot_id}/sessions/{session_id}/info:
/bots/{bot_id}/sessions/{session_id}/status:
get:
description: Get aggregated info for a chat session including message count,
context usage, cache stats, and used skills