支持部分agent实时更新上下文用量信息

This commit is contained in:
xintaofei
2026-03-08 23:48:47 +08:00
parent 53186c4ab5
commit 2b4f00484d
7 changed files with 116 additions and 3 deletions

1
src-tauri/Cargo.lock generated
View File

@@ -751,6 +751,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
name = "codeg" name = "codeg"
version = "0.0.16" version = "0.0.16"
dependencies = [ dependencies = [
"agent-client-protocol-schema",
"base64 0.22.1", "base64 0.22.1",
"bzip2", "bzip2",
"chrono", "chrono",

View File

@@ -46,6 +46,7 @@ sea-orm-migration = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio
toml = "0.8" toml = "0.8"
notify = "6" notify = "6"
base64 = "0.22" base64 = "0.22"
agent-client-protocol-schema = { version = "0.10", features = ["unstable_session_usage"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-window-state = "2" tauri-plugin-window-state = "2"

View File

@@ -1653,6 +1653,10 @@ fn emit_conversation_update(
update: SessionUpdate, update: SessionUpdate,
) { ) {
match update { match update {
SessionUpdate::UserMessageChunk(_) => {
// User echo chunks are informational for transcript sync and
// currently not rendered in live ACP UI.
}
SessionUpdate::AgentMessageChunk(ContentChunk { SessionUpdate::AgentMessageChunk(ContentChunk {
content: ContentBlock::Text(text), content: ContentBlock::Text(text),
.. ..
@@ -1665,6 +1669,9 @@ fn emit_conversation_update(
}, },
); );
} }
SessionUpdate::AgentMessageChunk(_) => {
// Non-text chunks are currently not surfaced in live streaming UI.
}
SessionUpdate::AgentThoughtChunk(ContentChunk { SessionUpdate::AgentThoughtChunk(ContentChunk {
content: ContentBlock::Text(text), content: ContentBlock::Text(text),
.. ..
@@ -1677,6 +1684,9 @@ fn emit_conversation_update(
}, },
); );
} }
SessionUpdate::AgentThoughtChunk(_) => {
// Non-text thought chunks are currently ignored.
}
SessionUpdate::ToolCall(tc) => { SessionUpdate::ToolCall(tc) => {
let content = serialize_tool_call_content(&tc.content); let content = serialize_tool_call_content(&tc.content);
let raw_input = json_value_to_text(&tc.raw_input); let raw_input = json_value_to_text(&tc.raw_input);
@@ -1762,6 +1772,16 @@ fn emit_conversation_update(
}, },
); );
} }
SessionUpdate::UsageUpdate(update) => {
let _ = app_handle.emit(
"acp://event",
AcpEvent::UsageUpdate {
connection_id: connection_id.into(),
used: update.used,
size: update.size,
},
);
}
other => { other => {
// Log unhandled update types for debugging // Log unhandled update types for debugging
eprintln!("[ACP] Unhandled SessionUpdate: {:?}", other); eprintln!("[ACP] Unhandled SessionUpdate: {:?}", other);

View File

@@ -129,6 +129,12 @@ pub enum AcpEvent {
connection_id: String, connection_id: String,
commands: Vec<AvailableCommandInfo>, commands: Vec<AvailableCommandInfo>,
}, },
/// Session usage/context window updated during conversation
UsageUpdate {
connection_id: String,
used: u64,
size: u64,
},
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -1,8 +1,10 @@
"use client" "use client"
import { useCallback, useSyncExternalStore } from "react"
import { Coins } from "lucide-react" import { Coins } from "lucide-react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { useSessionStats } from "@/contexts/session-stats-context" import { useSessionStats } from "@/contexts/session-stats-context"
import { useConnectionStore } from "@/contexts/acp-connections-context"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -31,13 +33,49 @@ function formatPercent(percent: number | null): string {
export function StatusBarTokens() { export function StatusBarTokens() {
const t = useTranslations("Folder.statusBar.tokens") const t = useTranslations("Folder.statusBar.tokens")
const store = useConnectionStore()
const { sessionStats } = useSessionStats() const { sessionStats } = useSessionStats()
const usage = sessionStats?.total_usage const usage = sessionStats?.total_usage
const contextUsed = sessionStats?.context_window_used_tokens ?? null const subscribeActiveKey = useCallback(
const contextMax = sessionStats?.context_window_max_tokens ?? null (cb: () => void) => store.subscribeActiveKey(cb),
[store]
)
const getActiveKey = useCallback(() => store.getActiveKey(), [store])
const activeKey = useSyncExternalStore(
subscribeActiveKey,
getActiveKey,
getActiveKey
)
const subscribeConn = useCallback(
(cb: () => void) => {
if (!activeKey) return () => {}
return store.subscribeKey(activeKey, cb)
},
[store, activeKey]
)
const getConnSnapshot = useCallback(
() => (activeKey ? store.getConnection(activeKey) : undefined),
[store, activeKey]
)
const activeConn = useSyncExternalStore(
subscribeConn,
getConnSnapshot,
getConnSnapshot
)
const liveContextUsed = activeConn?.usage?.used ?? null
const liveContextMax = activeConn?.usage?.size ?? null
const contextUsed =
liveContextUsed ?? sessionStats?.context_window_used_tokens ?? null
const contextMax =
liveContextMax ?? sessionStats?.context_window_max_tokens ?? null
const contextPercentRaw = const contextPercentRaw =
sessionStats?.context_window_usage_percent ?? (liveContextUsed != null && liveContextMax != null && liveContextMax > 0
? (liveContextUsed / liveContextMax) * 100
: sessionStats?.context_window_usage_percent) ??
(contextUsed != null && contextMax != null && contextMax > 0 (contextUsed != null && contextMax != null && contextMax > 0
? (contextUsed / contextMax) * 100 ? (contextUsed / contextMax) * 100
: null) : null)

View File

@@ -33,6 +33,7 @@ import type {
PermissionOptionInfo, PermissionOptionInfo,
SessionConfigOptionInfo, SessionConfigOptionInfo,
SessionModeStateInfo, SessionModeStateInfo,
SessionUsageUpdateInfo,
FixAction, FixAction,
PromptCapabilitiesInfo, PromptCapabilitiesInfo,
PromptInputBlock, PromptInputBlock,
@@ -88,6 +89,7 @@ export interface ConnectionState {
modes: SessionModeStateInfo | null modes: SessionModeStateInfo | null
configOptions: SessionConfigOptionInfo[] | null configOptions: SessionConfigOptionInfo[] | null
availableCommands: AvailableCommandInfo[] | null availableCommands: AvailableCommandInfo[] | null
usage: SessionUsageUpdateInfo | null
liveMessage: LiveMessage | null liveMessage: LiveMessage | null
pendingPermission: PendingPermission | null pendingPermission: PendingPermission | null
error: string | null error: string | null
@@ -177,6 +179,11 @@ type Action =
contextKey: string contextKey: string
commands: AvailableCommandInfo[] commands: AvailableCommandInfo[]
} }
| {
type: "USAGE_UPDATE"
contextKey: string
usage: SessionUsageUpdateInfo
}
type StreamingAction = type StreamingAction =
| { type: "CONTENT_DELTA"; contextKey: string; text: string } | { type: "CONTENT_DELTA"; contextKey: string; text: string }
@@ -441,6 +448,7 @@ function connectionsReducer(
modes: null, modes: null,
configOptions: null, configOptions: null,
availableCommands: null, availableCommands: null,
usage: null,
liveMessage: null, liveMessage: null,
pendingPermission: null, pendingPermission: null,
error: null, error: null,
@@ -883,6 +891,23 @@ function connectionsReducer(
return next return next
} }
case "USAGE_UPDATE": {
const conn = state.get(action.contextKey)
if (!conn) return state
if (
conn.usage?.used === action.usage.used &&
conn.usage?.size === action.usage.size
) {
return state
}
const next = new Map(state)
next.set(action.contextKey, {
...conn,
usage: action.usage,
})
return next
}
default: default:
return state return state
} }
@@ -1395,6 +1420,17 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
commands: e.commands, commands: e.commands,
}) })
break break
case "usage_update":
flushStreamingQueue()
dispatch({
type: "USAGE_UPDATE",
contextKey,
usage: {
used: e.used,
size: e.size,
},
})
break
} }
}, },
[dispatch, enqueueStreamingAction, flushStreamingQueue, t] [dispatch, enqueueStreamingAction, flushStreamingQueue, t]

View File

@@ -402,6 +402,11 @@ export interface AvailableCommandInfo {
input_hint?: string | null input_hint?: string | null
} }
export interface SessionUsageUpdateInfo {
used: number
size: number
}
// ACP events pushed from Rust backend (discriminated by "type" field) // ACP events pushed from Rust backend (discriminated by "type" field)
export type AcpEvent = export type AcpEvent =
| { type: "content_delta"; connection_id: string; text: string } | { type: "content_delta"; connection_id: string; text: string }
@@ -481,6 +486,12 @@ export type AcpEvent =
connection_id: string connection_id: string
commands: AvailableCommandInfo[] commands: AvailableCommandInfo[]
} }
| {
type: "usage_update"
connection_id: string
used: number
size: number
}
// Connection info returned by acp_list_connections // Connection info returned by acp_list_connections
export interface ConnectionInfo { export interface ConnectionInfo {