支持部分agent实时更新上下文用量信息
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user