chore(vscode-ide-companion): simplify the implementation of context remaining

This commit is contained in:
yiliang114
2025-12-24 14:29:25 +08:00
parent 660901e1fd
commit 90bf101040
13 changed files with 100 additions and 200 deletions

View File

@@ -357,8 +357,6 @@ class GeminiAgent {
await session.sendAvailableCommandsUpdate();
}, 0);
await session.announceCurrentModel(true);
if (conversation && conversation.messages) {
await session.replayHistory(conversation.messages);
}

View File

@@ -33,7 +33,6 @@ import {
UserPromptEvent,
TodoWriteTool,
ExitPlanModeTool,
tokenLimit,
} from '@qwen-code/qwen-code-core';
import * as acp from '../acp.js';
@@ -53,7 +52,6 @@ import type {
SetModeResponse,
ApprovalModeValue,
CurrentModeUpdate,
CurrentModelUpdate,
} from '../schema.js';
import { isSlashCommand } from '../../ui/utils/commandUtils.js';
@@ -88,10 +86,6 @@ export class Session implements SessionContext {
private readonly toolCallEmitter: ToolCallEmitter;
private readonly planEmitter: PlanEmitter;
private readonly messageEmitter: MessageEmitter;
private lastAnnouncedModel: {
name: string;
contextLimit?: number | null;
} | null = null;
// Implement SessionContext interface
readonly sessionId: string;
@@ -197,8 +191,6 @@ export class Session implements SessionContext {
parts = await this.#resolvePrompt(params.prompt, pendingSend.signal);
}
await this.sendCurrentModelUpdate();
let nextMessage: Content | null = { role: 'user', parts };
while (nextMessage !== null) {
@@ -387,40 +379,6 @@ export class Session implements SessionContext {
await this.sendUpdate(update);
}
async announceCurrentModel(force: boolean = false): Promise<void> {
await this.sendCurrentModelUpdate(force);
}
private async sendCurrentModelUpdate(force: boolean = false): Promise<void> {
const modelName = this.config.getModel();
if (!modelName) {
return;
}
const contextLimit = tokenLimit(modelName);
if (
!force &&
this.lastAnnouncedModel &&
this.lastAnnouncedModel.name === modelName &&
this.lastAnnouncedModel.contextLimit === contextLimit
) {
return;
}
this.lastAnnouncedModel = { name: modelName, contextLimit };
const update: CurrentModelUpdate = {
sessionUpdate: 'current_model_update',
model: {
name: modelName,
contextLimit,
},
};
await this.sendUpdate(update);
}
private async runTool(
abortSignal: AbortSignal,
promptId: string,

View File

@@ -289,11 +289,6 @@ export class AcpConnection {
* @returns Response
*/
async sendPrompt(prompt: string): Promise<AcpResponse> {
// Verify connection is still active before sending request
if (!this.isConnected) {
throw new Error('ACP connection is not active');
}
return this.sessionManager.sendPrompt(
prompt,
this.child,

View File

@@ -231,11 +231,6 @@ export class AcpSessionManager {
throw new Error('No active ACP session');
}
// Check if child process is still alive before sending the request
if (!child || child.killed) {
throw new Error('ACP child process is not available');
}
return await this.sendRequest(
AGENT_METHODS.session_prompt,
{

View File

@@ -227,16 +227,6 @@ export class QwenAgentManager {
* @param message - Message content
*/
async sendMessage(message: string): Promise<void> {
// Validate the current session before sending the message
const isValid = await this.validateCurrentSession();
if (!isValid) {
console.warn(
'[QwenAgentManager] Current session is invalid, creating new session',
);
const workingDir = this.currentWorkingDir;
await this.createNewSession(workingDir);
}
await this.connection.sendPrompt(message);
}

View File

@@ -10,11 +10,7 @@
* Handles session updates from ACP and dispatches them to appropriate callbacks
*/
import type {
AcpSessionUpdate,
ModelInfo,
SessionUpdateMeta,
} from '../types/acpTypes.js';
import type { AcpSessionUpdate, SessionUpdateMeta } from '../types/acpTypes.js';
import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js';
import type {
QwenAgentCallbacks,
@@ -164,11 +160,6 @@ export class QwenSessionUpdateHandler {
break;
}
case 'current_model_update': {
this.emitModelInfo((update as unknown as { model?: ModelInfo }).model);
break;
}
default:
console.log('[QwenAgentManager] Unhandled session update type');
break;
@@ -183,18 +174,8 @@ export class QwenSessionUpdateHandler {
const payload: UsageStatsPayload = {
usage: meta.usage || undefined,
durationMs: meta.durationMs ?? undefined,
model: meta.model ?? undefined,
tokenLimit: meta.tokenLimit ?? undefined,
};
this.callbacks.onUsageUpdate(payload);
}
private emitModelInfo(model?: ModelInfo): void {
if (!model || !this.callbacks.onModelInfo) {
return;
}
this.callbacks.onModelInfo(model);
}
}

View File

@@ -59,8 +59,6 @@ export interface UsageMetadata {
export interface SessionUpdateMeta {
usage?: UsageMetadata | null;
durationMs?: number | null;
model?: string | null;
tokenLimit?: number | null;
}
export interface ModelInfo {
@@ -188,13 +186,6 @@ export interface CurrentModeUpdate extends BaseSessionUpdate {
};
}
export interface CurrentModelUpdate extends BaseSessionUpdate {
update: {
sessionUpdate: 'current_model_update';
model: ModelInfo;
};
}
// Authenticate update (sent by agent during authentication process)
export interface AuthenticateUpdateNotification {
_meta: {
@@ -209,8 +200,7 @@ export type AcpSessionUpdate =
| ToolCallUpdate
| ToolCallStatusUpdate
| PlanUpdate
| CurrentModeUpdate
| CurrentModelUpdate;
| CurrentModeUpdate;
// Permission request (simplified version, use schema.RequestPermissionRequest for validation)
export interface AcpPermissionRequest {

View File

@@ -37,7 +37,6 @@ export interface UsageStatsPayload {
cachedTokens?: number | null;
} | null;
durationMs?: number | null;
model?: string | null;
tokenLimit?: number | null;
}

View File

@@ -74,7 +74,7 @@ export const App: React.FC = () => {
const [planEntries, setPlanEntries] = useState<PlanEntry[]>([]);
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true); // Track if we're still initializing/loading
const [modelInfo] = useState<{
const [modelInfo, setModelInfo] = useState<{
name: string;
contextLimit?: number | null;
} | null>(null);
@@ -175,9 +175,9 @@ export const App: React.FC = () => {
}
const modelName =
(usageStats?.model && typeof usageStats.model === 'string'
? usageStats.model
: undefined) ?? modelInfo?.name;
modelInfo?.name && typeof modelInfo.name === 'string'
? modelInfo.name
: undefined;
const derivedLimit =
modelName && modelName.length > 0 ? tokenLimit(modelName) : undefined;
@@ -200,7 +200,6 @@ export const App: React.FC = () => {
percentLeft,
usedTokens: used,
tokenLimit: limit,
model: modelName ?? undefined,
};
}, [usageStats, modelInfo]);
@@ -293,6 +292,9 @@ export const App: React.FC = () => {
setEditMode,
setIsAuthenticated,
setUsageStats: (stats) => setUsageStats(stats ?? null),
setModelInfo: (info) => {
setModelInfo(info);
},
});
// Auto-scroll handling: keep the view pinned to bottom when new content arrives,

View File

@@ -0,0 +1,88 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Tooltip } from '../Tooltip.js';
interface ContextUsage {
percentLeft: number;
usedTokens: number;
tokenLimit: number;
}
interface ContextIndicatorProps {
contextUsage: ContextUsage | null;
}
export const ContextIndicator: React.FC<ContextIndicatorProps> = ({
contextUsage,
}) => {
if (!contextUsage) {
return null;
}
// Calculate used percentage for the progress indicator
// contextUsage.percentLeft is the percentage remaining, so 100 - percentLeft = percent used
const percentUsed = 100 - contextUsage.percentLeft;
const percentFormatted = Math.max(0, Math.min(100, Math.round(percentUsed)));
const radius = 9;
const circumference = 2 * Math.PI * radius;
// To show the used portion, we need to offset the unused portion
// If 20% is used, we want to show 20% filled, so offset the remaining 80%
const dashOffset = ((100 - percentUsed) / 100) * circumference;
const formatNumber = (value: number) => {
if (value >= 1000) {
return `${(Math.round((value / 1000) * 10) / 10).toFixed(1)}k`;
}
return Math.round(value).toLocaleString();
};
// Create tooltip content with proper formatting
const tooltipContent = (
<div className="flex flex-col gap-1">
<div className="font-medium">
{percentFormatted}% {formatNumber(contextUsage.usedTokens)} /{' '}
{formatNumber(contextUsage.tokenLimit)} context used
</div>
</div>
);
return (
<Tooltip content={tooltipContent} position="top">
<button
className="btn-icon-compact"
aria-label={`${percentFormatted}% • ${formatNumber(contextUsage.usedTokens)} / ${formatNumber(contextUsage.tokenLimit)} context used`}
>
<svg viewBox="0 0 24 24" aria-hidden="true" role="presentation">
<circle
className="context-indicator__track"
cx="12"
cy="12"
r={radius}
fill="none"
stroke="currentColor"
opacity="0.2"
/>
<circle
className="context-indicator__progress"
cx="12"
cy="12"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
style={{
transform: 'rotate(-90deg)',
transformOrigin: '50% 50%',
}}
/>
</svg>
</button>
</Tooltip>
);
};

View File

@@ -21,7 +21,7 @@ import { CompletionMenu } from '../layout/CompletionMenu.js';
import type { CompletionItem } from '../../../types/completionItemTypes.js';
import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js';
import type { ApprovalModeValue } from '../../../types/approvalModeValueTypes.js';
import { Tooltip } from '../Tooltip.js';
import { ContextIndicator } from './ContextIndicator.js';
interface InputFormProps {
inputText: string;
@@ -41,7 +41,6 @@ interface InputFormProps {
percentLeft: number;
usedTokens: number;
tokenLimit: number;
model?: string;
} | null;
onInputChange: (text: string) => void;
onCompositionStart: () => void;
@@ -151,78 +150,6 @@ export const InputForm: React.FC<InputFormProps> = ({
? `${selectedLinesCount} ${selectedLinesCount === 1 ? 'line' : 'lines'} selected`
: '';
const renderContextIndicator = () => {
if (!contextUsage) {
return null;
}
// Calculate used percentage for the progress indicator
// contextUsage.percentLeft is the percentage remaining, so 100 - percentLeft = percent used
const percentUsed = 100 - contextUsage.percentLeft;
const percentFormatted = Math.max(
0,
Math.min(100, Math.round(percentUsed)),
);
const radius = 9;
const circumference = 2 * Math.PI * radius;
// To show the used portion, we need to offset the unused portion
// If 20% is used, we want to show 20% filled, so offset the remaining 80%
const dashOffset = ((100 - percentUsed) / 100) * circumference;
const formatNumber = (value: number) => {
if (value >= 1000) {
return `${(Math.round((value / 1000) * 10) / 10).toFixed(1)}k`;
}
return Math.round(value).toLocaleString();
};
// Create tooltip content with proper formatting
const tooltipContent = (
<div className="flex flex-col gap-1">
<div className="font-medium">
{percentFormatted}% {formatNumber(contextUsage.usedTokens)} /{' '}
{formatNumber(contextUsage.tokenLimit)} context used
</div>
{contextUsage.model && <div>Model: {contextUsage.model}</div>}
</div>
);
return (
<Tooltip content={tooltipContent} position="top">
<button
className="btn-icon-compact"
aria-label={`${percentFormatted}% • ${formatNumber(contextUsage.usedTokens)} / ${formatNumber(contextUsage.tokenLimit)} context used`}
>
<svg viewBox="0 0 24 24" aria-hidden="true" role="presentation">
<circle
className="context-indicator__track"
cx="12"
cy="12"
r={radius}
fill="none"
stroke="currentColor"
opacity="0.2"
/>
<circle
className="context-indicator__progress"
cx="12"
cy="12"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
style={{
transform: 'rotate(-90deg)',
transformOrigin: '50% 50%',
}}
/>
</svg>
</button>
</Tooltip>
);
};
return (
<div className="p-1 px-4 pb-4 absolute bottom-0 left-0 right-0 bg-gradient-to-b from-transparent to-[var(--app-primary-background)]">
<div className="block">
@@ -321,7 +248,7 @@ export const InputForm: React.FC<InputFormProps> = ({
<div className="flex-1 min-w-0" />
{/* Context usage indicator */}
{renderContextIndicator()}
<ContextIndicator contextUsage={contextUsage} />
{/* @yiliang114. closed temporarily */}
{/* Thinking button */}

View File

@@ -376,18 +376,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
data: { timestamp: Date.now() },
});
// Verify agent is connected before sending message
if (!this.agentManager.isConnected) {
throw new Error('Agent is not connected. Please try again.');
}
// Verify there's an active session
if (!this.agentManager.currentSessionId) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
await this.agentManager.createNewSession(workingDir);
}
await this.agentManager.sendMessage(formattedText);
// Save assistant message

View File

@@ -251,18 +251,7 @@ export const useWebViewMessages = ({
case 'usageStats': {
const stats = message.data as UsageStatsPayload | undefined;
if (
stats &&
(!stats.tokenLimit || stats.tokenLimit <= 0) &&
modelInfoRef.current?.contextLimit
) {
handlers.setUsageStats?.({
...stats,
tokenLimit: modelInfoRef.current.contextLimit ?? undefined,
});
} else {
handlers.setUsageStats?.(stats);
}
handlers.setUsageStats?.(stats);
break;
}