chore(vscode-ide-companion): wip

This commit is contained in:
yiliang114
2025-12-13 22:19:54 +08:00
parent 641dd03689
commit 0f2f1faee5
3 changed files with 166 additions and 23 deletions

View File

@@ -34,6 +34,9 @@ export type { ChatMessage, PlanEntry, ToolCallUpdateData };
* *
* Coordinates various modules and provides unified interface * Coordinates various modules and provides unified interface
*/ */
interface AgentConnectOptions {
autoAuthenticate?: boolean;
}
interface AgentSessionOptions { interface AgentSessionOptions {
autoAuthenticate?: boolean; autoAuthenticate?: boolean;
} }
@@ -189,12 +192,14 @@ export class QwenAgentManager {
async connect( async connect(
workingDir: string, workingDir: string,
cliEntryPath: string, cliEntryPath: string,
options?: AgentConnectOptions,
): Promise<QwenConnectionResult> { ): Promise<QwenConnectionResult> {
this.currentWorkingDir = workingDir; this.currentWorkingDir = workingDir;
return this.connectionHandler.connect( return this.connectionHandler.connect(
this.connection, this.connection,
workingDir, workingDir,
cliEntryPath, cliEntryPath,
options,
); );
} }
@@ -276,9 +281,10 @@ export class QwenAgentManager {
'[QwenAgentManager] Getting session list with version-aware strategy', '[QwenAgentManager] Getting session list with version-aware strategy',
); );
// Prefer ACP method first; fall back to file system if it fails for any reason.
try { try {
console.log('[QwenAgentManager] Attempting to get session list via ACP'); console.log(
'[QwenAgentManager] Attempting to get session list via ACP method',
);
const response = await this.connection.listSessions(); const response = await this.connection.listSessions();
console.log('[QwenAgentManager] ACP session list response:', response); console.log('[QwenAgentManager] ACP session list response:', response);
@@ -288,19 +294,21 @@ export class QwenAgentManager {
const res: unknown = response; const res: unknown = response;
let items: Array<Record<string, unknown>> = []; let items: Array<Record<string, unknown>> = [];
if (Array.isArray(res)) { // Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC
items = res as Array<Record<string, unknown>>; // "result" directly (not the full AcpResponse). Treat it as unknown
} else if (res && typeof res === 'object' && 'items' in res) { // and carefully narrow before accessing `items` to satisfy strict TS.
if (res && typeof res === 'object' && 'items' in res) {
const itemsValue = (res as { items?: unknown }).items; const itemsValue = (res as { items?: unknown }).items;
items = Array.isArray(itemsValue) items = Array.isArray(itemsValue)
? (itemsValue as Array<Record<string, unknown>>) ? (itemsValue as Array<Record<string, unknown>>)
: []; : [];
} }
console.log('[QwenAgentManager] Sessions retrieved via ACP:', { console.log(
count: items.length, '[QwenAgentManager] Sessions retrieved via ACP:',
}); res,
items.length,
);
if (items.length > 0) { if (items.length > 0) {
const sessions = items.map((item) => ({ const sessions = items.map((item) => ({
id: item.sessionId || item.id, id: item.sessionId || item.id,
@@ -314,6 +322,11 @@ export class QwenAgentManager {
filePath: item.filePath, filePath: item.filePath,
cwd: item.cwd, cwd: item.cwd,
})); }));
console.log(
'[QwenAgentManager] Sessions retrieved via ACP:',
sessions.length,
);
return sessions; return sessions;
} }
} catch (error) { } catch (error) {
@@ -376,6 +389,7 @@ export class QwenAgentManager {
}> { }> {
const size = params?.size ?? 20; const size = params?.size ?? 20;
const cursor = params?.cursor; const cursor = params?.cursor;
try { try {
const response = await this.connection.listSessions({ const response = await this.connection.listSessions({
size, size,
@@ -470,7 +484,6 @@ export class QwenAgentManager {
*/ */
async getSessionMessages(sessionId: string): Promise<ChatMessage[]> { async getSessionMessages(sessionId: string): Promise<ChatMessage[]> {
try { try {
// Prefer reading CLI's JSONL if we can find filePath from session/list
try { try {
const list = await this.getSessionList(); const list = await this.getSessionList();
const item = list.find( const item = list.find(
@@ -690,7 +703,9 @@ export class QwenAgentManager {
const planText = planEntries const planText = planEntries
.map( .map(
(entry: Record<string, unknown>, index: number) => (entry: Record<string, unknown>, index: number) =>
`${index + 1}. ${entry.description || entry.title || 'Unnamed step'}`, `${index + 1}. ${
entry.description || entry.title || 'Unnamed step'
}`,
) )
.join('\n'); .join('\n');
msgs.push({ msgs.push({
@@ -969,13 +984,15 @@ export class QwenAgentManager {
sessionId, sessionId,
); );
// Prefer ACP session/load first; fall back to file system on failure.
try { try {
console.log('[QwenAgentManager] Attempting to load session via ACP'); console.log(
'[QwenAgentManager] Attempting to load session via ACP method',
);
await this.loadSessionViaAcp(sessionId); await this.loadSessionViaAcp(sessionId);
console.log('[QwenAgentManager] Session loaded successfully via ACP'); console.log('[QwenAgentManager] Session loaded successfully via ACP');
// After loading via ACP, we still need to get messages from file system.
// In future, we might get them directly from the ACP response. // After loading via ACP, we still need to get messages from file system
// In future, we might get them directly from the ACP response
} catch (error) { } catch (error) {
console.warn( console.warn(
'[QwenAgentManager] ACP session load failed, falling back to file system method:', '[QwenAgentManager] ACP session load failed, falling back to file system method:',
@@ -1094,7 +1111,7 @@ export class QwenAgentManager {
// Let CLI handle authentication - it's the single source of truth // Let CLI handle authentication - it's the single source of truth
await this.connection.authenticate(authMethod); await this.connection.authenticate(authMethod);
// Add a slight delay to ensure auth state is settled // Add a slight delay to ensure auth state is settled
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 300));
await this.connection.newSession(workingDir); await this.connection.newSession(workingDir);
} catch (reauthErr) { } catch (reauthErr) {
console.error( console.error(

View File

@@ -11,6 +11,8 @@
*/ */
import type { AcpConnection } from './acpConnection.js'; import type { AcpConnection } from './acpConnection.js';
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
import { authMethod } from '../types/acpTypes.js';
export interface QwenConnectionResult { export interface QwenConnectionResult {
sessionCreated: boolean; sessionCreated: boolean;
@@ -27,30 +29,153 @@ export class QwenConnectionHandler {
* *
* @param connection - ACP connection instance * @param connection - ACP connection instance
* @param workingDir - Working directory * @param workingDir - Working directory
* @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) * @param cliPath - CLI path (optional, if provided will override the path in configuration)
*/ */
async connect( async connect(
connection: AcpConnection, connection: AcpConnection,
workingDir: string, workingDir: string,
cliEntryPath: string, cliEntryPath: string,
options?: {
autoAuthenticate?: boolean;
},
): Promise<QwenConnectionResult> { ): Promise<QwenConnectionResult> {
const connectId = Date.now(); const connectId = Date.now();
console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`); console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`);
const sessionCreated = false; const autoAuthenticate = options?.autoAuthenticate ?? true;
const requiresAuth = false; let sessionCreated = false;
let requiresAuth = false;
// Build extra CLI arguments (only essential parameters) // Build extra CLI arguments (only essential parameters)
const extraArgs: string[] = []; const extraArgs: string[] = [];
await connection.connect(cliEntryPath, workingDir, extraArgs); await connection.connect(cliEntryPath!, workingDir, extraArgs);
// Note: Session creation is now handled by the caller (QwenAgentManager) // Try to restore existing session or create new session
// This prevents automatic session creation on every connection which was // Note: Auto-restore on connect is disabled to avoid surprising loads
// causing unwanted authentication prompts // when user opens a "New Chat" tab. Restoration is now an explicit action
// (session selector → session/load) or handled by higher-level flows.
const sessionRestored = false;
// Create new session if unable to restore
if (!sessionRestored) {
console.log(
'[QwenAgentManager] no sessionRestored, Creating new session...',
);
try {
console.log(
'[QwenAgentManager] Creating new session (letting CLI handle authentication)...',
);
await this.newSessionWithRetry(
connection,
workingDir,
3,
authMethod,
autoAuthenticate,
);
console.log('[QwenAgentManager] New session created successfully');
sessionCreated = true;
} catch (sessionError) {
const needsAuth =
autoAuthenticate === false &&
isAuthenticationRequiredError(sessionError);
if (needsAuth) {
requiresAuth = true;
console.log(
'[QwenAgentManager] Session creation requires authentication; waiting for user-triggered login.',
);
} else {
console.log(
`\n⚠ [SESSION FAILED] newSessionWithRetry threw error\n`,
);
console.log(`[QwenAgentManager] Error details:`, sessionError);
throw sessionError;
}
}
} else {
sessionCreated = true;
}
console.log(`\n========================================`); console.log(`\n========================================`);
console.log(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`); console.log(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`);
console.log(`========================================\n`); console.log(`========================================\n`);
return { sessionCreated, requiresAuth }; return { sessionCreated, requiresAuth };
} }
/**
* Create new session (with retry)
*
* @param connection - ACP connection instance
* @param workingDir - Working directory
* @param maxRetries - Maximum number of retries
*/
private async newSessionWithRetry(
connection: AcpConnection,
workingDir: string,
maxRetries: number,
authMethod: string,
autoAuthenticate: boolean,
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(
`[QwenAgentManager] Creating session (attempt ${attempt}/${maxRetries})...`,
);
await connection.newSession(workingDir);
console.log('[QwenAgentManager] Session created successfully');
return;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
`[QwenAgentManager] Session creation attempt ${attempt} failed:`,
errorMessage,
);
// If Qwen reports that authentication is required, try to
// authenticate on-the-fly once and retry without waiting.
const requiresAuth = isAuthenticationRequiredError(error);
if (requiresAuth) {
if (!autoAuthenticate) {
console.log(
'[QwenAgentManager] Authentication required but auto-authentication is disabled. Propagating error.',
);
throw error;
}
console.log(
'[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...',
);
try {
await connection.authenticate(authMethod);
// FIXME: @yiliang114 If there is no delay for a while, immediately executing
// newSession may cause the cli authorization jump to be triggered again
// Add a slight delay to ensure auth state is settled
await new Promise((resolve) => setTimeout(resolve, 300));
// Retry immediately after successful auth
await connection.newSession(workingDir);
console.log(
'[QwenAgentManager] Session created successfully after auth',
);
return;
} catch (authErr) {
console.error(
'[QwenAgentManager] Re-authentication failed:',
authErr,
);
// Fall through to retry logic below
}
}
if (attempt === maxRetries) {
throw new Error(
`Session creation failed after ${maxRetries} attempts: ${errorMessage}`,
);
}
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
console.log(`[QwenAgentManager] Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
} }

View File

@@ -576,6 +576,7 @@ export class WebViewProvider {
const connectResult = await this.agentManager.connect( const connectResult = await this.agentManager.connect(
workingDir, workingDir,
bundledCliEntry, bundledCliEntry,
options,
); );
console.log('[WebViewProvider] Agent connected successfully'); console.log('[WebViewProvider] Agent connected successfully');
this.agentInitialized = true; this.agentInitialized = true;