mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat(vscode-ide-companion/auth): deduplicate concurrent authentication calls
Prevent multiple simultaneous authentication flows by: - Adding static authInFlight promise tracking in AcpConnection - Implementing runExclusiveAuth method in AuthStateManager - Adding sessionCreateInFlight tracking in QwenAgentManager - Ensuring only one auth flow runs at a time across different components This prevents race conditions and duplicate login prompts when multiple components request authentication simultaneously.
This commit is contained in:
@@ -8,7 +8,11 @@ import * as vscode from 'vscode';
|
|||||||
import { IDEServer } from './ide-server.js';
|
import { IDEServer } from './ide-server.js';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
||||||
import { createLogger } from './utils/logger.js';
|
import {
|
||||||
|
createLogger,
|
||||||
|
getConsoleLogger,
|
||||||
|
initSharedConsoleLogger,
|
||||||
|
} from './utils/logger.js';
|
||||||
import {
|
import {
|
||||||
detectIdeFromEnv,
|
detectIdeFromEnv,
|
||||||
IDE_DEFINITIONS,
|
IDE_DEFINITIONS,
|
||||||
@@ -105,6 +109,8 @@ async function checkForUpdates(
|
|||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
logger = vscode.window.createOutputChannel('Qwen Code Companion');
|
logger = vscode.window.createOutputChannel('Qwen Code Companion');
|
||||||
|
initSharedConsoleLogger(context);
|
||||||
|
const consoleLog = getConsoleLogger();
|
||||||
log = createLogger(context, logger);
|
log = createLogger(context, logger);
|
||||||
log('Extension activated');
|
log('Extension activated');
|
||||||
|
|
||||||
@@ -142,18 +148,18 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
webviewPanel: vscode.WebviewPanel,
|
webviewPanel: vscode.WebviewPanel,
|
||||||
state: unknown,
|
state: unknown,
|
||||||
) {
|
) {
|
||||||
console.log(
|
consoleLog(
|
||||||
'[Extension] Deserializing WebView panel with state:',
|
'[Extension] Deserializing WebView panel with state:',
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a new provider for the restored panel
|
// Create a new provider for the restored panel
|
||||||
const provider = createWebViewProvider();
|
const provider = createWebViewProvider();
|
||||||
console.log('[Extension] Provider created for deserialization');
|
consoleLog('[Extension] Provider created for deserialization');
|
||||||
|
|
||||||
// Restore state if available BEFORE restoring the panel
|
// Restore state if available BEFORE restoring the panel
|
||||||
if (state && typeof state === 'object') {
|
if (state && typeof state === 'object') {
|
||||||
console.log('[Extension] Restoring state:', state);
|
consoleLog('[Extension] Restoring state:', state);
|
||||||
provider.restoreState(
|
provider.restoreState(
|
||||||
state as {
|
state as {
|
||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
@@ -161,11 +167,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('[Extension] No state to restore or invalid state');
|
consoleLog('[Extension] No state to restore or invalid state');
|
||||||
}
|
}
|
||||||
|
|
||||||
await provider.restorePanel(webviewPanel);
|
await provider.restorePanel(webviewPanel);
|
||||||
console.log('[Extension] Panel restore completed');
|
consoleLog('[Extension] Panel restore completed');
|
||||||
|
|
||||||
log('WebView panel restored from serialization');
|
log('WebView panel restored from serialization');
|
||||||
},
|
},
|
||||||
@@ -206,7 +212,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[Extension] Auto-allow on diff.accept failed:', err);
|
console.warn('[Extension] Auto-allow on diff.accept failed:', err);
|
||||||
}
|
}
|
||||||
console.log('[Extension] Diff accepted');
|
|
||||||
}),
|
}),
|
||||||
vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => {
|
vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => {
|
||||||
const docUri = uri ?? vscode.window.activeTextEditor?.document.uri;
|
const docUri = uri ?? vscode.window.activeTextEditor?.document.uri;
|
||||||
@@ -223,7 +228,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[Extension] Auto-reject on diff.cancel failed:', err);
|
console.warn('[Extension] Auto-reject on diff.cancel failed:', err);
|
||||||
}
|
}
|
||||||
console.log('[Extension] Diff cancelled');
|
|
||||||
})),
|
})),
|
||||||
vscode.commands.registerCommand('qwen.diff.closeAll', async () => {
|
vscode.commands.registerCommand('qwen.diff.closeAll', async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export class AcpConnection {
|
|||||||
private child: ChildProcess | null = null;
|
private child: ChildProcess | null = null;
|
||||||
private pendingRequests = new Map<number, PendingRequest<unknown>>();
|
private pendingRequests = new Map<number, PendingRequest<unknown>>();
|
||||||
private nextRequestId = { value: 0 };
|
private nextRequestId = { value: 0 };
|
||||||
|
// Deduplicate concurrent authenticate calls (across retry paths)
|
||||||
|
private static authInFlight: Promise<AcpResponse> | null = null;
|
||||||
// Remember the working dir provided at connect() so later ACP calls
|
// Remember the working dir provided at connect() so later ACP calls
|
||||||
// that require cwd (e.g. session/list) can include it.
|
// that require cwd (e.g. session/list) can include it.
|
||||||
private workingDir: string = process.cwd();
|
private workingDir: string = process.cwd();
|
||||||
@@ -271,12 +273,23 @@ export class AcpConnection {
|
|||||||
* @returns Authentication response
|
* @returns Authentication response
|
||||||
*/
|
*/
|
||||||
async authenticate(methodId?: string): Promise<AcpResponse> {
|
async authenticate(methodId?: string): Promise<AcpResponse> {
|
||||||
return this.sessionManager.authenticate(
|
if (AcpConnection.authInFlight) {
|
||||||
methodId,
|
return AcpConnection.authInFlight;
|
||||||
this.child,
|
}
|
||||||
this.pendingRequests,
|
|
||||||
this.nextRequestId,
|
const p = this.sessionManager
|
||||||
);
|
.authenticate(
|
||||||
|
methodId,
|
||||||
|
this.child,
|
||||||
|
this.pendingRequests,
|
||||||
|
this.nextRequestId,
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
AcpConnection.authInFlight = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AcpConnection.authInFlight = p;
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as vscode from 'vscode';
|
import type * as vscode from 'vscode';
|
||||||
|
import { createConsoleLogger, getConsoleLogger } from '../utils/logger.js';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
@@ -21,6 +22,9 @@ export class AuthStateManager {
|
|||||||
private static context: vscode.ExtensionContext | null = null;
|
private static context: vscode.ExtensionContext | null = null;
|
||||||
private static readonly AUTH_STATE_KEY = 'qwen.authState';
|
private static readonly AUTH_STATE_KEY = 'qwen.authState';
|
||||||
private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
private static consoleLog: (...args: unknown[]) => void = getConsoleLogger();
|
||||||
|
// Deduplicate concurrent auth flows (e.g., multiple tabs prompting login)
|
||||||
|
private static authFlowInFlight: Promise<unknown> | null = null;
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,11 +38,37 @@ export class AuthStateManager {
|
|||||||
// If a context is provided, update the static context
|
// If a context is provided, update the static context
|
||||||
if (context) {
|
if (context) {
|
||||||
AuthStateManager.context = context;
|
AuthStateManager.context = context;
|
||||||
|
AuthStateManager.consoleLog = createConsoleLogger(
|
||||||
|
context,
|
||||||
|
'AuthStateManager',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AuthStateManager.instance;
|
return AuthStateManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an auth-related flow exclusively. If another flow is already running,
|
||||||
|
* return the same promise to prevent duplicate login prompts.
|
||||||
|
*/
|
||||||
|
static runExclusiveAuth<T>(task: () => Promise<T>): Promise<T> {
|
||||||
|
if (AuthStateManager.authFlowInFlight) {
|
||||||
|
return AuthStateManager.authFlowInFlight as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = Promise.resolve()
|
||||||
|
.then(task)
|
||||||
|
.finally(() => {
|
||||||
|
// Clear only if this promise is still the active one
|
||||||
|
if (AuthStateManager.authFlowInFlight === p) {
|
||||||
|
AuthStateManager.authFlowInFlight = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AuthStateManager.authFlowInFlight = p;
|
||||||
|
return p as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if there's a valid cached authentication
|
* Check if there's a valid cached authentication
|
||||||
*/
|
*/
|
||||||
@@ -46,17 +76,19 @@ export class AuthStateManager {
|
|||||||
const state = await this.getAuthState();
|
const state = await this.getAuthState();
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
console.log('[AuthStateManager] No cached auth state found');
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] No cached auth state found',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AuthStateManager] Found cached auth state:', {
|
AuthStateManager.consoleLog('[AuthStateManager] Found cached auth state:', {
|
||||||
workingDir: state.workingDir,
|
workingDir: state.workingDir,
|
||||||
authMethod: state.authMethod,
|
authMethod: state.authMethod,
|
||||||
timestamp: new Date(state.timestamp).toISOString(),
|
timestamp: new Date(state.timestamp).toISOString(),
|
||||||
isAuthenticated: state.isAuthenticated,
|
isAuthenticated: state.isAuthenticated,
|
||||||
});
|
});
|
||||||
console.log('[AuthStateManager] Checking against:', {
|
AuthStateManager.consoleLog('[AuthStateManager] Checking against:', {
|
||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
authMethod,
|
||||||
});
|
});
|
||||||
@@ -67,8 +99,8 @@ export class AuthStateManager {
|
|||||||
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
|
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
|
||||||
|
|
||||||
if (isExpired) {
|
if (isExpired) {
|
||||||
console.log('[AuthStateManager] Cached auth expired');
|
AuthStateManager.consoleLog('[AuthStateManager] Cached auth expired');
|
||||||
console.log(
|
AuthStateManager.consoleLog(
|
||||||
'[AuthStateManager] Cache age:',
|
'[AuthStateManager] Cache age:',
|
||||||
Math.floor((now - state.timestamp) / 1000 / 60),
|
Math.floor((now - state.timestamp) / 1000 / 60),
|
||||||
'minutes',
|
'minutes',
|
||||||
@@ -82,15 +114,29 @@ export class AuthStateManager {
|
|||||||
state.workingDir === workingDir && state.authMethod === authMethod;
|
state.workingDir === workingDir && state.authMethod === authMethod;
|
||||||
|
|
||||||
if (!isSameContext) {
|
if (!isSameContext) {
|
||||||
console.log('[AuthStateManager] Working dir or auth method changed');
|
AuthStateManager.consoleLog(
|
||||||
console.log('[AuthStateManager] Cached workingDir:', state.workingDir);
|
'[AuthStateManager] Working dir or auth method changed',
|
||||||
console.log('[AuthStateManager] Current workingDir:', workingDir);
|
);
|
||||||
console.log('[AuthStateManager] Cached authMethod:', state.authMethod);
|
AuthStateManager.consoleLog(
|
||||||
console.log('[AuthStateManager] Current authMethod:', authMethod);
|
'[AuthStateManager] Cached workingDir:',
|
||||||
|
state.workingDir,
|
||||||
|
);
|
||||||
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] Current workingDir:',
|
||||||
|
workingDir,
|
||||||
|
);
|
||||||
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] Cached authMethod:',
|
||||||
|
state.authMethod,
|
||||||
|
);
|
||||||
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] Current authMethod:',
|
||||||
|
authMethod,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AuthStateManager] Valid cached auth found');
|
AuthStateManager.consoleLog('[AuthStateManager] Valid cached auth found');
|
||||||
return state.isAuthenticated;
|
return state.isAuthenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +146,10 @@ export class AuthStateManager {
|
|||||||
*/
|
*/
|
||||||
async debugAuthState(): Promise<void> {
|
async debugAuthState(): Promise<void> {
|
||||||
const state = await this.getAuthState();
|
const state = await this.getAuthState();
|
||||||
console.log('[AuthStateManager] DEBUG - Current auth state:', state);
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] DEBUG - Current auth state:',
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -108,9 +157,16 @@ export class AuthStateManager {
|
|||||||
const isExpired =
|
const isExpired =
|
||||||
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
|
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
|
||||||
|
|
||||||
console.log('[AuthStateManager] DEBUG - Auth state age:', age, 'minutes');
|
AuthStateManager.consoleLog(
|
||||||
console.log('[AuthStateManager] DEBUG - Auth state expired:', isExpired);
|
'[AuthStateManager] DEBUG - Auth state age:',
|
||||||
console.log(
|
age,
|
||||||
|
'minutes',
|
||||||
|
);
|
||||||
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] DEBUG - Auth state expired:',
|
||||||
|
isExpired,
|
||||||
|
);
|
||||||
|
AuthStateManager.consoleLog(
|
||||||
'[AuthStateManager] DEBUG - Auth state valid:',
|
'[AuthStateManager] DEBUG - Auth state valid:',
|
||||||
state.isAuthenticated,
|
state.isAuthenticated,
|
||||||
);
|
);
|
||||||
@@ -135,7 +191,7 @@ export class AuthStateManager {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[AuthStateManager] Saving auth state:', {
|
AuthStateManager.consoleLog('[AuthStateManager] Saving auth state:', {
|
||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
authMethod,
|
||||||
timestamp: new Date(state.timestamp).toISOString(),
|
timestamp: new Date(state.timestamp).toISOString(),
|
||||||
@@ -145,11 +201,14 @@ export class AuthStateManager {
|
|||||||
AuthStateManager.AUTH_STATE_KEY,
|
AuthStateManager.AUTH_STATE_KEY,
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
console.log('[AuthStateManager] Auth state saved');
|
AuthStateManager.consoleLog('[AuthStateManager] Auth state saved');
|
||||||
|
|
||||||
// Verify the state was saved correctly
|
// Verify the state was saved correctly
|
||||||
const savedState = await this.getAuthState();
|
const savedState = await this.getAuthState();
|
||||||
console.log('[AuthStateManager] Verified saved state:', savedState);
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] Verified saved state:',
|
||||||
|
savedState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,9 +222,9 @@ export class AuthStateManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AuthStateManager] Clearing auth state');
|
AuthStateManager.consoleLog('[AuthStateManager] Clearing auth state');
|
||||||
const currentState = await this.getAuthState();
|
const currentState = await this.getAuthState();
|
||||||
console.log(
|
AuthStateManager.consoleLog(
|
||||||
'[AuthStateManager] Current state before clearing:',
|
'[AuthStateManager] Current state before clearing:',
|
||||||
currentState,
|
currentState,
|
||||||
);
|
);
|
||||||
@@ -174,11 +233,14 @@ export class AuthStateManager {
|
|||||||
AuthStateManager.AUTH_STATE_KEY,
|
AuthStateManager.AUTH_STATE_KEY,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
console.log('[AuthStateManager] Auth state cleared');
|
AuthStateManager.consoleLog('[AuthStateManager] Auth state cleared');
|
||||||
|
|
||||||
// Verify the state was cleared
|
// Verify the state was cleared
|
||||||
const newState = await this.getAuthState();
|
const newState = await this.getAuthState();
|
||||||
console.log('[AuthStateManager] State after clearing:', newState);
|
AuthStateManager.consoleLog(
|
||||||
|
'[AuthStateManager] State after clearing:',
|
||||||
|
newState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,17 +249,15 @@ export class AuthStateManager {
|
|||||||
private async getAuthState(): Promise<AuthState | undefined> {
|
private async getAuthState(): Promise<AuthState | undefined> {
|
||||||
// Ensure we have a valid context
|
// Ensure we have a valid context
|
||||||
if (!AuthStateManager.context) {
|
if (!AuthStateManager.context) {
|
||||||
console.log(
|
AuthStateManager.consoleLog(
|
||||||
'[AuthStateManager] No context available for getting auth state',
|
'[AuthStateManager] No context available for getting auth state',
|
||||||
);
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = AuthStateManager.context.globalState.get<AuthState>(
|
return AuthStateManager.context.globalState.get<AuthState>(
|
||||||
AuthStateManager.AUTH_STATE_KEY,
|
AuthStateManager.AUTH_STATE_KEY,
|
||||||
);
|
);
|
||||||
console.log('[AuthStateManager] Auth state:', a);
|
|
||||||
return a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
|
|||||||
import { CliContextManager } from '../cli/cliContextManager.js';
|
import { CliContextManager } from '../cli/cliContextManager.js';
|
||||||
import { authMethod } from '../types/acpTypes.js';
|
import { authMethod } from '../types/acpTypes.js';
|
||||||
import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js';
|
import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js';
|
||||||
|
import { getConsoleLogger } from '../utils/logger.js';
|
||||||
|
|
||||||
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
|
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
|
||||||
|
|
||||||
@@ -45,11 +46,15 @@ export class QwenAgentManager {
|
|||||||
// Cache the last used AuthStateManager so internal calls (e.g. fallback paths)
|
// Cache the last used AuthStateManager so internal calls (e.g. fallback paths)
|
||||||
// can reuse it and avoid forcing a fresh authentication unnecessarily.
|
// can reuse it and avoid forcing a fresh authentication unnecessarily.
|
||||||
private defaultAuthStateManager?: AuthStateManager;
|
private defaultAuthStateManager?: AuthStateManager;
|
||||||
|
// Deduplicate concurrent session/new attempts
|
||||||
|
private sessionCreateInFlight: Promise<string | null> | null = null;
|
||||||
|
|
||||||
// Callback storage
|
// Callback storage
|
||||||
private callbacks: QwenAgentCallbacks = {};
|
private callbacks: QwenAgentCallbacks = {};
|
||||||
|
private consoleLog: (...args: unknown[]) => void;
|
||||||
|
|
||||||
constructor() {
|
constructor(consoleLogger = getConsoleLogger()) {
|
||||||
|
this.consoleLog = consoleLogger;
|
||||||
this.connection = new AcpConnection();
|
this.connection = new AcpConnection();
|
||||||
this.sessionReader = new QwenSessionReader();
|
this.sessionReader = new QwenSessionReader();
|
||||||
this.sessionManager = new QwenSessionManager();
|
this.sessionManager = new QwenSessionManager();
|
||||||
@@ -76,7 +81,7 @@ export class QwenAgentManager {
|
|||||||
).update;
|
).update;
|
||||||
const text = update?.content?.text || '';
|
const text = update?.content?.text || '';
|
||||||
if (update?.sessionUpdate === 'user_message_chunk' && text) {
|
if (update?.sessionUpdate === 'user_message_chunk' && text) {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Rehydration: routing user message chunk',
|
'[QwenAgentManager] Rehydration: routing user message chunk',
|
||||||
);
|
);
|
||||||
this.callbacks.onMessage?.({
|
this.callbacks.onMessage?.({
|
||||||
@@ -87,7 +92,7 @@ export class QwenAgentManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (update?.sessionUpdate === 'agent_message_chunk' && text) {
|
if (update?.sessionUpdate === 'agent_message_chunk' && text) {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Rehydration: routing agent message chunk',
|
'[QwenAgentManager] Rehydration: routing agent message chunk',
|
||||||
);
|
);
|
||||||
this.callbacks.onMessage?.({
|
this.callbacks.onMessage?.({
|
||||||
@@ -98,7 +103,7 @@ export class QwenAgentManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// For other types during rehydration, fall through to normal handler
|
// For other types during rehydration, fall through to normal handler
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Rehydration: non-text update, forwarding to handler',
|
'[QwenAgentManager] Rehydration: non-text update, forwarding to handler',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -257,7 +262,7 @@ export class QwenAgentManager {
|
|||||||
* @returns Session list
|
* @returns Session list
|
||||||
*/
|
*/
|
||||||
async getSessionList(): Promise<Array<Record<string, unknown>>> {
|
async getSessionList(): Promise<Array<Record<string, unknown>>> {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Getting session list with version-aware strategy',
|
'[QwenAgentManager] Getting session list with version-aware strategy',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -265,7 +270,7 @@ export class QwenAgentManager {
|
|||||||
const cliContextManager = CliContextManager.getInstance();
|
const cliContextManager = CliContextManager.getInstance();
|
||||||
const supportsSessionList = cliContextManager.supportsSessionList();
|
const supportsSessionList = cliContextManager.supportsSessionList();
|
||||||
|
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] CLI supports session/list:',
|
'[QwenAgentManager] CLI supports session/list:',
|
||||||
supportsSessionList,
|
supportsSessionList,
|
||||||
);
|
);
|
||||||
@@ -273,11 +278,14 @@ export class QwenAgentManager {
|
|||||||
// Try ACP method first if supported
|
// Try ACP method first if supported
|
||||||
if (supportsSessionList) {
|
if (supportsSessionList) {
|
||||||
try {
|
try {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Attempting to get session list via ACP method',
|
'[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);
|
this.consoleLog(
|
||||||
|
'[QwenAgentManager] ACP session list response:',
|
||||||
|
response,
|
||||||
|
);
|
||||||
|
|
||||||
// sendRequest resolves with the JSON-RPC "result" directly
|
// sendRequest resolves with the JSON-RPC "result" directly
|
||||||
// Newer CLI returns an object: { items: [...], nextCursor?, hasMore }
|
// Newer CLI returns an object: { items: [...], nextCursor?, hasMore }
|
||||||
@@ -295,7 +303,7 @@ export class QwenAgentManager {
|
|||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Sessions retrieved via ACP:',
|
'[QwenAgentManager] Sessions retrieved via ACP:',
|
||||||
res,
|
res,
|
||||||
items.length,
|
items.length,
|
||||||
@@ -314,7 +322,7 @@ export class QwenAgentManager {
|
|||||||
cwd: item.cwd,
|
cwd: item.cwd,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Sessions retrieved via ACP:',
|
'[QwenAgentManager] Sessions retrieved via ACP:',
|
||||||
sessions.length,
|
sessions.length,
|
||||||
);
|
);
|
||||||
@@ -330,9 +338,11 @@ export class QwenAgentManager {
|
|||||||
|
|
||||||
// Always fall back to file system method
|
// Always fall back to file system method
|
||||||
try {
|
try {
|
||||||
console.log('[QwenAgentManager] Getting session list from file system');
|
this.consoleLog(
|
||||||
|
'[QwenAgentManager] Getting session list from file system',
|
||||||
|
);
|
||||||
const sessions = await this.sessionReader.getAllSessions(undefined, true);
|
const sessions = await this.sessionReader.getAllSessions(undefined, true);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Session list from file system (all projects):',
|
'[QwenAgentManager] Session list from file system (all projects):',
|
||||||
sessions.length,
|
sessions.length,
|
||||||
);
|
);
|
||||||
@@ -350,7 +360,7 @@ export class QwenAgentManager {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Sessions retrieved from file system:',
|
'[QwenAgentManager] Sessions retrieved from file system:',
|
||||||
result.length,
|
result.length,
|
||||||
);
|
);
|
||||||
@@ -490,7 +500,7 @@ export class QwenAgentManager {
|
|||||||
const item = list.find(
|
const item = list.find(
|
||||||
(s) => s.sessionId === sessionId || s.id === sessionId,
|
(s) => s.sessionId === sessionId || s.id === sessionId,
|
||||||
);
|
);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Session list item for filePath lookup:',
|
'[QwenAgentManager] Session list item for filePath lookup:',
|
||||||
item,
|
item,
|
||||||
);
|
);
|
||||||
@@ -561,7 +571,7 @@ export class QwenAgentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Simple linear reconstruction: filter user/assistant and sort by timestamp
|
// Simple linear reconstruction: filter user/assistant and sort by timestamp
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] JSONL records read:',
|
'[QwenAgentManager] JSONL records read:',
|
||||||
records.length,
|
records.length,
|
||||||
filePath,
|
filePath,
|
||||||
@@ -718,7 +728,7 @@ export class QwenAgentManager {
|
|||||||
// Handle other types if needed
|
// Handle other types if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] JSONL messages reconstructed:',
|
'[QwenAgentManager] JSONL messages reconstructed:',
|
||||||
msgs.length,
|
msgs.length,
|
||||||
);
|
);
|
||||||
@@ -856,7 +866,7 @@ export class QwenAgentManager {
|
|||||||
tag: string,
|
tag: string,
|
||||||
): Promise<{ success: boolean; message?: string }> {
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
try {
|
try {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Saving session via /chat save command:',
|
'[QwenAgentManager] Saving session via /chat save command:',
|
||||||
sessionId,
|
sessionId,
|
||||||
'with tag:',
|
'with tag:',
|
||||||
@@ -867,7 +877,9 @@ export class QwenAgentManager {
|
|||||||
// The CLI will handle this as a special command
|
// The CLI will handle this as a special command
|
||||||
await this.connection.sendPrompt(`/chat save "${tag}"`);
|
await this.connection.sendPrompt(`/chat save "${tag}"`);
|
||||||
|
|
||||||
console.log('[QwenAgentManager] /chat save command sent successfully');
|
this.consoleLog(
|
||||||
|
'[QwenAgentManager] /chat save command sent successfully',
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Session saved with tag: ${tag}`,
|
message: `Session saved with tag: ${tag}`,
|
||||||
@@ -914,14 +926,14 @@ export class QwenAgentManager {
|
|||||||
conversationId: string,
|
conversationId: string,
|
||||||
): Promise<{ success: boolean; tag?: string; message?: string }> {
|
): Promise<{ success: boolean; tag?: string; message?: string }> {
|
||||||
try {
|
try {
|
||||||
console.log('[QwenAgentManager] ===== CHECKPOINT SAVE START =====');
|
this.consoleLog('[QwenAgentManager] ===== CHECKPOINT SAVE START =====');
|
||||||
console.log('[QwenAgentManager] Conversation ID:', conversationId);
|
this.consoleLog('[QwenAgentManager] Conversation ID:', conversationId);
|
||||||
console.log('[QwenAgentManager] Message count:', messages.length);
|
this.consoleLog('[QwenAgentManager] Message count:', messages.length);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Current working dir:',
|
'[QwenAgentManager] Current working dir:',
|
||||||
this.currentWorkingDir,
|
this.currentWorkingDir,
|
||||||
);
|
);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Current session ID (from CLI):',
|
'[QwenAgentManager] Current session ID (from CLI):',
|
||||||
this.currentSessionId,
|
this.currentSessionId,
|
||||||
);
|
);
|
||||||
@@ -998,11 +1010,11 @@ export class QwenAgentManager {
|
|||||||
try {
|
try {
|
||||||
// Route upcoming session/update messages as discrete messages for replay
|
// Route upcoming session/update messages as discrete messages for replay
|
||||||
this.rehydratingSessionId = sessionId;
|
this.rehydratingSessionId = sessionId;
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Rehydration start for session:',
|
'[QwenAgentManager] Rehydration start for session:',
|
||||||
sessionId,
|
sessionId,
|
||||||
);
|
);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Attempting session/load via ACP for session:',
|
'[QwenAgentManager] Attempting session/load via ACP for session:',
|
||||||
sessionId,
|
sessionId,
|
||||||
);
|
);
|
||||||
@@ -1010,7 +1022,7 @@ export class QwenAgentManager {
|
|||||||
sessionId,
|
sessionId,
|
||||||
cwdOverride,
|
cwdOverride,
|
||||||
);
|
);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Session load succeeded. Response:',
|
'[QwenAgentManager] Session load succeeded. Response:',
|
||||||
JSON.stringify(response).substring(0, 200),
|
JSON.stringify(response).substring(0, 200),
|
||||||
);
|
);
|
||||||
@@ -1050,7 +1062,10 @@ export class QwenAgentManager {
|
|||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// End rehydration routing regardless of outcome
|
// End rehydration routing regardless of outcome
|
||||||
console.log('[QwenAgentManager] Rehydration end for session:', sessionId);
|
this.consoleLog(
|
||||||
|
'[QwenAgentManager] Rehydration end for session:',
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
this.rehydratingSessionId = null;
|
this.rehydratingSessionId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1063,7 +1078,7 @@ export class QwenAgentManager {
|
|||||||
* @returns Loaded session messages or null
|
* @returns Loaded session messages or null
|
||||||
*/
|
*/
|
||||||
async loadSession(sessionId: string): Promise<ChatMessage[] | null> {
|
async loadSession(sessionId: string): Promise<ChatMessage[] | null> {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Loading session with version-aware strategy:',
|
'[QwenAgentManager] Loading session with version-aware strategy:',
|
||||||
sessionId,
|
sessionId,
|
||||||
);
|
);
|
||||||
@@ -1072,7 +1087,7 @@ export class QwenAgentManager {
|
|||||||
const cliContextManager = CliContextManager.getInstance();
|
const cliContextManager = CliContextManager.getInstance();
|
||||||
const supportsSessionLoad = cliContextManager.supportsSessionLoad();
|
const supportsSessionLoad = cliContextManager.supportsSessionLoad();
|
||||||
|
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] CLI supports session/load:',
|
'[QwenAgentManager] CLI supports session/load:',
|
||||||
supportsSessionLoad,
|
supportsSessionLoad,
|
||||||
);
|
);
|
||||||
@@ -1080,11 +1095,13 @@ export class QwenAgentManager {
|
|||||||
// Try ACP method first if supported
|
// Try ACP method first if supported
|
||||||
if (supportsSessionLoad) {
|
if (supportsSessionLoad) {
|
||||||
try {
|
try {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Attempting to load session via ACP method',
|
'[QwenAgentManager] Attempting to load session via ACP method',
|
||||||
);
|
);
|
||||||
await this.loadSessionViaAcp(sessionId);
|
await this.loadSessionViaAcp(sessionId);
|
||||||
console.log('[QwenAgentManager] Session loaded successfully via ACP');
|
this.consoleLog(
|
||||||
|
'[QwenAgentManager] Session loaded successfully via ACP',
|
||||||
|
);
|
||||||
|
|
||||||
// After loading via ACP, we still need to get messages from file system
|
// After loading via ACP, we still need to get messages from file system
|
||||||
// In future, we might get them directly from the ACP response
|
// In future, we might get them directly from the ACP response
|
||||||
@@ -1098,11 +1115,11 @@ export class QwenAgentManager {
|
|||||||
|
|
||||||
// Always fall back to file system method
|
// Always fall back to file system method
|
||||||
try {
|
try {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Loading session messages from file system',
|
'[QwenAgentManager] Loading session messages from file system',
|
||||||
);
|
);
|
||||||
const messages = await this.loadSessionMessagesFromFile(sessionId);
|
const messages = await this.loadSessionMessagesFromFile(sessionId);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Session messages loaded successfully from file system',
|
'[QwenAgentManager] Session messages loaded successfully from file system',
|
||||||
);
|
);
|
||||||
return messages;
|
return messages;
|
||||||
@@ -1125,7 +1142,7 @@ export class QwenAgentManager {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
): Promise<ChatMessage[] | null> {
|
): Promise<ChatMessage[] | null> {
|
||||||
try {
|
try {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Loading session from file system:',
|
'[QwenAgentManager] Loading session from file system:',
|
||||||
sessionId,
|
sessionId,
|
||||||
);
|
);
|
||||||
@@ -1137,7 +1154,7 @@ export class QwenAgentManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[QwenAgentManager] Session not found in file system:',
|
'[QwenAgentManager] Session not found in file system:',
|
||||||
sessionId,
|
sessionId,
|
||||||
);
|
);
|
||||||
@@ -1183,93 +1200,67 @@ export class QwenAgentManager {
|
|||||||
workingDir: string,
|
workingDir: string,
|
||||||
authStateManager?: AuthStateManager,
|
authStateManager?: AuthStateManager,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
console.log('[QwenAgentManager] Creating new session...');
|
// Reuse existing session if present
|
||||||
|
if (this.connection.currentSessionId) {
|
||||||
|
return this.connection.currentSessionId;
|
||||||
|
}
|
||||||
|
// Deduplicate concurrent session/new attempts
|
||||||
|
if (this.sessionCreateInFlight) {
|
||||||
|
return this.sessionCreateInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we have valid cached authentication
|
this.consoleLog('[QwenAgentManager] Creating new session...');
|
||||||
let hasValidAuth = false;
|
|
||||||
// Prefer the provided authStateManager, otherwise fall back to the one
|
// Prefer the provided authStateManager, otherwise fall back to the one
|
||||||
// remembered during connect(). This prevents accidental re-auth in
|
// remembered during connect(). This prevents accidental re-auth in
|
||||||
// fallback paths (e.g. session switching) when the handler didn't pass it.
|
// fallback paths (e.g. session switching) when the handler didn't pass it.
|
||||||
const effectiveAuth = authStateManager || this.defaultAuthStateManager;
|
const effectiveAuth = authStateManager || this.defaultAuthStateManager;
|
||||||
if (effectiveAuth) {
|
|
||||||
hasValidAuth = await effectiveAuth.hasValidAuth(workingDir, authMethod);
|
|
||||||
console.log(
|
|
||||||
'[QwenAgentManager] Has valid cached auth for new session:',
|
|
||||||
hasValidAuth,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only authenticate if we don't have valid cached auth
|
this.sessionCreateInFlight = (async () => {
|
||||||
if (!hasValidAuth) {
|
|
||||||
console.log(
|
|
||||||
'[QwenAgentManager] Authenticating before creating session...',
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await this.connection.authenticate(authMethod);
|
// Try to create a new ACP session. If Qwen asks for auth despite our
|
||||||
console.log('[QwenAgentManager] Authentication successful');
|
// cached flag (e.g. fresh process or expired tokens), re-authenticate and retry.
|
||||||
|
|
||||||
// Save auth state
|
|
||||||
if (effectiveAuth) {
|
|
||||||
console.log(
|
|
||||||
'[QwenAgentManager] Saving auth state after successful authentication',
|
|
||||||
);
|
|
||||||
await effectiveAuth.saveAuthState(workingDir, authMethod);
|
|
||||||
}
|
|
||||||
} catch (authError) {
|
|
||||||
console.error('[QwenAgentManager] Authentication failed:', authError);
|
|
||||||
// Clear potentially invalid cache
|
|
||||||
if (effectiveAuth) {
|
|
||||||
console.log(
|
|
||||||
'[QwenAgentManager] Clearing auth cache due to authentication failure',
|
|
||||||
);
|
|
||||||
await effectiveAuth.clearAuthState();
|
|
||||||
}
|
|
||||||
throw authError;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
'[QwenAgentManager] Skipping authentication - using valid cached auth',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to create a new ACP session. If Qwen asks for auth despite our
|
|
||||||
// cached flag (e.g. fresh process or expired tokens), re-authenticate and retry.
|
|
||||||
try {
|
|
||||||
await this.connection.newSession(workingDir);
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
const requiresAuth =
|
|
||||||
msg.includes('Authentication required') ||
|
|
||||||
msg.includes('(code: -32000)');
|
|
||||||
|
|
||||||
if (requiresAuth) {
|
|
||||||
console.warn(
|
|
||||||
'[QwenAgentManager] session/new requires authentication. Retrying with authenticate...',
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await this.connection.authenticate(authMethod);
|
|
||||||
// Persist auth cache so subsequent calls can skip the web flow.
|
|
||||||
if (effectiveAuth) {
|
|
||||||
await effectiveAuth.saveAuthState(workingDir, authMethod);
|
|
||||||
}
|
|
||||||
await this.connection.newSession(workingDir);
|
await this.connection.newSession(workingDir);
|
||||||
} catch (reauthErr) {
|
} catch (err) {
|
||||||
// Clear potentially stale cache on failure and rethrow
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
if (effectiveAuth) {
|
const requiresAuth =
|
||||||
await effectiveAuth.clearAuthState();
|
msg.includes('Authentication required') ||
|
||||||
|
msg.includes('(code: -32000)');
|
||||||
|
|
||||||
|
if (requiresAuth) {
|
||||||
|
console.warn(
|
||||||
|
'[QwenAgentManager] session/new requires authentication. Retrying with authenticate...',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await this.connection.authenticate(authMethod);
|
||||||
|
// Persist auth cache so subsequent calls can skip the web flow.
|
||||||
|
if (effectiveAuth) {
|
||||||
|
await effectiveAuth.saveAuthState(workingDir, authMethod);
|
||||||
|
}
|
||||||
|
await this.connection.newSession(workingDir);
|
||||||
|
} catch (reauthErr) {
|
||||||
|
// Clear potentially stale cache on failure and rethrow
|
||||||
|
if (effectiveAuth) {
|
||||||
|
await effectiveAuth.clearAuthState();
|
||||||
|
}
|
||||||
|
throw reauthErr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
throw reauthErr;
|
|
||||||
}
|
}
|
||||||
} else {
|
const newSessionId = this.connection.currentSessionId;
|
||||||
throw err;
|
this.consoleLog(
|
||||||
|
'[QwenAgentManager] New session created with ID:',
|
||||||
|
newSessionId,
|
||||||
|
);
|
||||||
|
return newSessionId;
|
||||||
|
} finally {
|
||||||
|
this.sessionCreateInFlight = null;
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
const newSessionId = this.connection.currentSessionId;
|
|
||||||
console.log(
|
return this.sessionCreateInFlight;
|
||||||
'[QwenAgentManager] New session created with ID:',
|
|
||||||
newSessionId,
|
|
||||||
);
|
|
||||||
return newSessionId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1285,7 +1276,7 @@ export class QwenAgentManager {
|
|||||||
* Cancel current prompt
|
* Cancel current prompt
|
||||||
*/
|
*/
|
||||||
async cancelCurrentPrompt(): Promise<void> {
|
async cancelCurrentPrompt(): Promise<void> {
|
||||||
console.log('[QwenAgentManager] Cancelling current prompt');
|
this.consoleLog('[QwenAgentManager] Cancelling current prompt');
|
||||||
await this.connection.cancelSession();
|
await this.connection.cancelSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
type ConsoleLogger = (...args: unknown[]) => void;
|
||||||
|
|
||||||
|
// Shared console logger instance, initialized during extension activation.
|
||||||
|
let sharedConsoleLogger: ConsoleLogger = () => {};
|
||||||
|
|
||||||
export function createLogger(
|
export function createLogger(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
logger: vscode.OutputChannel,
|
logger: vscode.OutputChannel,
|
||||||
@@ -16,3 +21,40 @@ export function createLogger(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dev-only logger that writes to the VS Code console (Developer Tools).
|
||||||
|
*/
|
||||||
|
export function createConsoleLogger(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
scope?: string,
|
||||||
|
): ConsoleLogger {
|
||||||
|
return (...args: unknown[]) => {
|
||||||
|
if (context.extensionMode !== vscode.ExtensionMode.Development) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scope) {
|
||||||
|
console.log(`[${scope}]`, ...args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the shared console logger so other modules can import it without
|
||||||
|
* threading the extension context everywhere.
|
||||||
|
*/
|
||||||
|
export function initSharedConsoleLogger(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
scope?: string,
|
||||||
|
) {
|
||||||
|
sharedConsoleLogger = createConsoleLogger(context, scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the shared console logger (no-op until initialized).
|
||||||
|
*/
|
||||||
|
export function getConsoleLogger(): ConsoleLogger {
|
||||||
|
return sharedConsoleLogger;
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ import { FileIcon, UserIcon } from './components/icons/index.js';
|
|||||||
import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js';
|
import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js';
|
||||||
import type { ApprovalModeValue } from '../types/acpTypes.js';
|
import type { ApprovalModeValue } from '../types/acpTypes.js';
|
||||||
import type { PlanEntry } from '../types/chatTypes.js';
|
import type { PlanEntry } from '../types/chatTypes.js';
|
||||||
|
import { createWebviewConsoleLogger } from './utils/logger.js';
|
||||||
|
|
||||||
export const App: React.FC = () => {
|
export const App: React.FC = () => {
|
||||||
const vscode = useVSCode();
|
const vscode = useVSCode();
|
||||||
|
const consoleLog = useMemo(() => createWebviewConsoleLogger('App'), []);
|
||||||
|
|
||||||
// Core hooks
|
// Core hooks
|
||||||
const sessionManagement = useSessionManagement(vscode);
|
const sessionManagement = useSessionManagement(vscode);
|
||||||
@@ -167,7 +169,7 @@ export const App: React.FC = () => {
|
|||||||
}, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]);
|
}, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]);
|
||||||
|
|
||||||
// Message submission
|
// Message submission
|
||||||
const handleSubmit = useMessageSubmit({
|
const { handleSubmit: submitMessage } = useMessageSubmit({
|
||||||
inputText,
|
inputText,
|
||||||
setInputText,
|
setInputText,
|
||||||
messageHandling,
|
messageHandling,
|
||||||
@@ -487,6 +489,22 @@ export const App: React.FC = () => {
|
|||||||
setThinkingEnabled((prev) => !prev);
|
setThinkingEnabled((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When user sends a message after scrolling up, re-pin and jump to the bottom
|
||||||
|
const handleSubmitWithScroll = useCallback(
|
||||||
|
(e: React.FormEvent) => {
|
||||||
|
setPinnedToBottom(true);
|
||||||
|
|
||||||
|
const container = messagesContainerRef.current;
|
||||||
|
if (container) {
|
||||||
|
const top = container.scrollHeight - container.clientHeight;
|
||||||
|
container.scrollTo({ top });
|
||||||
|
}
|
||||||
|
|
||||||
|
submitMessage(e);
|
||||||
|
},
|
||||||
|
[submitMessage],
|
||||||
|
);
|
||||||
|
|
||||||
// Create unified message array containing all types of messages and tool calls
|
// Create unified message array containing all types of messages and tool calls
|
||||||
const allMessages = useMemo<
|
const allMessages = useMemo<
|
||||||
Array<{
|
Array<{
|
||||||
@@ -524,7 +542,7 @@ export const App: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}, [messageHandling.messages, inProgressToolCalls, completedToolCalls]);
|
}, [messageHandling.messages, inProgressToolCalls, completedToolCalls]);
|
||||||
|
|
||||||
console.log('[App] Rendering messages:', allMessages);
|
consoleLog('[App] Rendering messages:', allMessages);
|
||||||
|
|
||||||
// Render all messages and tool calls
|
// Render all messages and tool calls
|
||||||
const renderMessages = useCallback<() => React.ReactNode>(
|
const renderMessages = useCallback<() => React.ReactNode>(
|
||||||
@@ -686,7 +704,7 @@ export const App: React.FC = () => {
|
|||||||
onCompositionStart={() => setIsComposing(true)}
|
onCompositionStart={() => setIsComposing(true)}
|
||||||
onCompositionEnd={() => setIsComposing(false)}
|
onCompositionEnd={() => setIsComposing(false)}
|
||||||
onKeyDown={() => {}}
|
onKeyDown={() => {}}
|
||||||
onSubmit={handleSubmit.handleSubmit}
|
onSubmit={handleSubmitWithScroll}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onToggleEditMode={handleToggleEditMode}
|
onToggleEditMode={handleToggleEditMode}
|
||||||
onToggleThinking={handleToggleThinking}
|
onToggleThinking={handleToggleThinking}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { WebViewContent } from '../webview/WebViewContent.js';
|
|||||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||||
import { getFileName } from './utils/webviewUtils.js';
|
import { getFileName } from './utils/webviewUtils.js';
|
||||||
import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js';
|
import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js';
|
||||||
|
import { createConsoleLogger } from '../utils/logger.js';
|
||||||
|
|
||||||
export class WebViewProvider {
|
export class WebViewProvider {
|
||||||
private panelManager: PanelManager;
|
private panelManager: PanelManager;
|
||||||
@@ -32,12 +33,15 @@ export class WebViewProvider {
|
|||||||
private pendingPermissionResolve: ((optionId: string) => void) | null = null;
|
private pendingPermissionResolve: ((optionId: string) => void) | null = null;
|
||||||
// Track current ACP mode id to influence permission/diff behavior
|
// Track current ACP mode id to influence permission/diff behavior
|
||||||
private currentModeId: ApprovalModeValue | null = null;
|
private currentModeId: ApprovalModeValue | null = null;
|
||||||
|
private consoleLog: (...args: unknown[]) => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
private extensionUri: vscode.Uri,
|
private extensionUri: vscode.Uri,
|
||||||
) {
|
) {
|
||||||
this.agentManager = new QwenAgentManager();
|
const agentConsoleLogger = createConsoleLogger(context, 'QwenAgentManager');
|
||||||
|
this.consoleLog = createConsoleLogger(context, 'WebViewProvider');
|
||||||
|
this.agentManager = new QwenAgentManager(agentConsoleLogger);
|
||||||
this.conversationStore = new ConversationStore(context);
|
this.conversationStore = new ConversationStore(context);
|
||||||
this.authStateManager = AuthStateManager.getInstance(context);
|
this.authStateManager = AuthStateManager.getInstance(context);
|
||||||
this.panelManager = new PanelManager(extensionUri, () => {
|
this.panelManager = new PanelManager(extensionUri, () => {
|
||||||
@@ -380,7 +384,7 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
// Set up state serialization
|
// Set up state serialization
|
||||||
newPanel.onDidChangeViewState(() => {
|
newPanel.onDidChangeViewState(() => {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Panel view state changed, triggering serialization check',
|
'[WebViewProvider] Panel view state changed, triggering serialization check',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -510,7 +514,7 @@ export class WebViewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to restore authentication state and initialize connection
|
// Attempt to restore authentication state and initialize connection
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Attempting to restore auth state and connection...',
|
'[WebViewProvider] Attempting to restore auth state and connection...',
|
||||||
);
|
);
|
||||||
await this.attemptAuthStateRestoration();
|
await this.attemptAuthStateRestoration();
|
||||||
@@ -532,23 +536,26 @@ export class WebViewProvider {
|
|||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
authMethod,
|
||||||
);
|
);
|
||||||
console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth);
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] Has valid cached auth:',
|
||||||
|
hasValidAuth,
|
||||||
|
);
|
||||||
|
|
||||||
if (hasValidAuth) {
|
if (hasValidAuth) {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Valid auth found, attempting connection...',
|
'[WebViewProvider] Valid auth found, attempting connection...',
|
||||||
);
|
);
|
||||||
// Try to connect with cached auth
|
// Try to connect with cached auth
|
||||||
await this.initializeAgentConnection();
|
await this.initializeAgentConnection();
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] No valid auth found, rendering empty conversation',
|
'[WebViewProvider] No valid auth found, rendering empty conversation',
|
||||||
);
|
);
|
||||||
// Render the chat UI immediately without connecting
|
// Render the chat UI immediately without connecting
|
||||||
await this.initializeEmptyConversation();
|
await this.initializeEmptyConversation();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] No auth state manager, rendering empty conversation',
|
'[WebViewProvider] No auth state manager, rendering empty conversation',
|
||||||
);
|
);
|
||||||
await this.initializeEmptyConversation();
|
await this.initializeEmptyConversation();
|
||||||
@@ -565,84 +572,101 @@ export class WebViewProvider {
|
|||||||
* Can be called from show() or via /login command
|
* Can be called from show() or via /login command
|
||||||
*/
|
*/
|
||||||
async initializeAgentConnection(): Promise<void> {
|
async initializeAgentConnection(): Promise<void> {
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
return AuthStateManager.runExclusiveAuth(() =>
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
this.doInitializeAgentConnection(),
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Starting initialization, workingDir:',
|
|
||||||
workingDir,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] AuthStateManager available:',
|
|
||||||
!!this.authStateManager,
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if CLI is installed before attempting to connect
|
/**
|
||||||
const cliDetection = await CliDetector.detectQwenCli();
|
* Internal: perform actual connection/initialization (no auth locking).
|
||||||
|
*/
|
||||||
|
private async doInitializeAgentConnection(): Promise<void> {
|
||||||
|
const run = async () => {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
|
||||||
if (!cliDetection.isInstalled) {
|
this.consoleLog(
|
||||||
console.log(
|
'[WebViewProvider] Starting initialization, workingDir:',
|
||||||
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
workingDir,
|
||||||
);
|
);
|
||||||
console.log('[WebViewProvider] CLI detection error:', cliDetection.error);
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] AuthStateManager available:',
|
||||||
// Show VSCode notification with installation option
|
!!this.authStateManager,
|
||||||
await CliInstaller.promptInstallation();
|
|
||||||
|
|
||||||
// Initialize empty conversation (can still browse history)
|
|
||||||
await this.initializeEmptyConversation();
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Qwen CLI detected, attempting connection...',
|
|
||||||
);
|
);
|
||||||
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
|
|
||||||
console.log('[WebViewProvider] CLI version:', cliDetection.version);
|
|
||||||
|
|
||||||
try {
|
// Check if CLI is installed before attempting to connect
|
||||||
console.log('[WebViewProvider] Connecting to agent...');
|
const cliDetection = await CliDetector.detectQwenCli();
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Using authStateManager:',
|
if (!cliDetection.isInstalled) {
|
||||||
!!this.authStateManager,
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
||||||
);
|
);
|
||||||
const authInfo = await this.authStateManager.getAuthInfo();
|
this.consoleLog(
|
||||||
console.log('[WebViewProvider] Auth cache status:', authInfo);
|
'[WebViewProvider] CLI detection error:',
|
||||||
|
cliDetection.error,
|
||||||
// Pass the detected CLI path to ensure we use the correct installation
|
|
||||||
await this.agentManager.connect(
|
|
||||||
workingDir,
|
|
||||||
this.authStateManager,
|
|
||||||
cliDetection.cliPath,
|
|
||||||
);
|
);
|
||||||
console.log('[WebViewProvider] Agent connected successfully');
|
|
||||||
this.agentInitialized = true;
|
|
||||||
|
|
||||||
// Load messages from the current Qwen session
|
// Show VSCode notification with installation option
|
||||||
await this.loadCurrentSessionMessages();
|
await CliInstaller.promptInstallation();
|
||||||
|
|
||||||
// Notify webview that agent is connected
|
// Initialize empty conversation (can still browse history)
|
||||||
this.sendMessageToWebView({
|
|
||||||
type: 'agentConnected',
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
} catch (_error) {
|
|
||||||
console.error('[WebViewProvider] Agent connection error:', _error);
|
|
||||||
// Clear auth cache on error (might be auth issue)
|
|
||||||
await this.authStateManager.clearAuthState();
|
|
||||||
vscode.window.showWarningMessage(
|
|
||||||
`Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
|
||||||
);
|
|
||||||
// Fallback to empty conversation
|
|
||||||
await this.initializeEmptyConversation();
|
await this.initializeEmptyConversation();
|
||||||
|
} else {
|
||||||
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] Qwen CLI detected, attempting connection...',
|
||||||
|
);
|
||||||
|
this.consoleLog('[WebViewProvider] CLI path:', cliDetection.cliPath);
|
||||||
|
this.consoleLog('[WebViewProvider] CLI version:', cliDetection.version);
|
||||||
|
|
||||||
// Notify webview that agent connection failed
|
try {
|
||||||
this.sendMessageToWebView({
|
this.consoleLog('[WebViewProvider] Connecting to agent...');
|
||||||
type: 'agentConnectionError',
|
this.consoleLog(
|
||||||
data: {
|
'[WebViewProvider] Using authStateManager:',
|
||||||
message: _error instanceof Error ? _error.message : String(_error),
|
!!this.authStateManager,
|
||||||
},
|
);
|
||||||
});
|
const authInfo = await this.authStateManager.getAuthInfo();
|
||||||
|
this.consoleLog('[WebViewProvider] Auth cache status:', authInfo);
|
||||||
|
|
||||||
|
// Pass the detected CLI path to ensure we use the correct installation
|
||||||
|
await this.agentManager.connect(
|
||||||
|
workingDir,
|
||||||
|
this.authStateManager,
|
||||||
|
cliDetection.cliPath,
|
||||||
|
);
|
||||||
|
this.consoleLog('[WebViewProvider] Agent connected successfully');
|
||||||
|
this.agentInitialized = true;
|
||||||
|
|
||||||
|
// Load messages from the current Qwen session
|
||||||
|
await this.loadCurrentSessionMessages();
|
||||||
|
|
||||||
|
// Notify webview that agent is connected
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'agentConnected',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
console.error('[WebViewProvider] Agent connection error:', _error);
|
||||||
|
// Clear auth cache on error (might be auth issue)
|
||||||
|
await this.authStateManager.clearAuthState();
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
||||||
|
);
|
||||||
|
// Fallback to empty conversation
|
||||||
|
await this.initializeEmptyConversation();
|
||||||
|
|
||||||
|
// Notify webview that agent connection failed
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'agentConnectionError',
|
||||||
|
data: {
|
||||||
|
message:
|
||||||
|
_error instanceof Error ? _error.message : String(_error),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
return run();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -650,86 +674,97 @@ export class WebViewProvider {
|
|||||||
* Called when user explicitly uses /login command
|
* Called when user explicitly uses /login command
|
||||||
*/
|
*/
|
||||||
async forceReLogin(): Promise<void> {
|
async forceReLogin(): Promise<void> {
|
||||||
console.log('[WebViewProvider] Force re-login requested');
|
this.consoleLog('[WebViewProvider] Force re-login requested');
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Current authStateManager:',
|
'[WebViewProvider] Current authStateManager:',
|
||||||
!!this.authStateManager,
|
!!this.authStateManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
await vscode.window.withProgress(
|
// If a login/connection flow is already running, reuse it to avoid double prompts
|
||||||
{
|
const p = Promise.resolve(
|
||||||
location: vscode.ProgressLocation.Notification,
|
vscode.window.withProgress(
|
||||||
title: 'Logging in to Qwen Code... ',
|
{
|
||||||
cancellable: false,
|
location: vscode.ProgressLocation.Notification,
|
||||||
},
|
cancellable: false,
|
||||||
async (progress) => {
|
},
|
||||||
try {
|
async (progress) => {
|
||||||
progress.report({ message: 'Preparing sign-in...' });
|
try {
|
||||||
|
progress.report({ message: 'Preparing sign-in...' });
|
||||||
|
|
||||||
// Clear existing auth cache
|
// Clear existing auth cache
|
||||||
if (this.authStateManager) {
|
if (this.authStateManager) {
|
||||||
await this.authStateManager.clearAuthState();
|
await this.authStateManager.clearAuthState();
|
||||||
console.log('[WebViewProvider] Auth cache cleared');
|
this.consoleLog('[WebViewProvider] Auth cache cleared');
|
||||||
} else {
|
} else {
|
||||||
console.log('[WebViewProvider] No authStateManager to clear');
|
this.consoleLog('[WebViewProvider] No authStateManager to clear');
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect existing connection if any
|
|
||||||
if (this.agentInitialized) {
|
|
||||||
try {
|
|
||||||
this.agentManager.disconnect();
|
|
||||||
console.log('[WebViewProvider] Existing connection disconnected');
|
|
||||||
} catch (_error) {
|
|
||||||
console.log('[WebViewProvider] Error disconnecting:', _error);
|
|
||||||
}
|
}
|
||||||
this.agentInitialized = false;
|
|
||||||
|
// Disconnect existing connection if any
|
||||||
|
if (this.agentInitialized) {
|
||||||
|
try {
|
||||||
|
this.agentManager.disconnect();
|
||||||
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] Existing connection disconnected',
|
||||||
|
);
|
||||||
|
} catch (_error) {
|
||||||
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] Error disconnecting:',
|
||||||
|
_error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.agentInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a moment for cleanup to complete
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
progress.report({
|
||||||
|
message: 'Connecting to CLI and starting sign-in...',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reinitialize connection (will trigger fresh authentication)
|
||||||
|
await this.doInitializeAgentConnection();
|
||||||
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] Force re-login completed successfully',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure auth state is saved after successful re-login
|
||||||
|
if (this.authStateManager) {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
||||||
|
this.consoleLog(
|
||||||
|
'[WebViewProvider] Auth state saved after re-login',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send success notification to WebView
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'loginSuccess',
|
||||||
|
data: { message: 'Successfully logged in!' },
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
console.error('[WebViewProvider] Force re-login failed:', _error);
|
||||||
|
console.error(
|
||||||
|
'[WebViewProvider] Error stack:',
|
||||||
|
_error instanceof Error ? _error.stack : 'N/A',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send error notification to WebView
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'loginError',
|
||||||
|
data: {
|
||||||
|
message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
throw _error;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Wait a moment for cleanup to complete
|
),
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
progress.report({
|
|
||||||
message: 'Connecting to CLI and starting sign-in...',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reinitialize connection (will trigger fresh authentication)
|
|
||||||
await this.initializeAgentConnection();
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Force re-login completed successfully',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure auth state is saved after successful re-login
|
|
||||||
if (this.authStateManager) {
|
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
|
||||||
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
|
||||||
console.log('[WebViewProvider] Auth state saved after re-login');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send success notification to WebView
|
|
||||||
this.sendMessageToWebView({
|
|
||||||
type: 'loginSuccess',
|
|
||||||
data: { message: 'Successfully logged in!' },
|
|
||||||
});
|
|
||||||
} catch (_error) {
|
|
||||||
console.error('[WebViewProvider] Force re-login failed:', _error);
|
|
||||||
console.error(
|
|
||||||
'[WebViewProvider] Error stack:',
|
|
||||||
_error instanceof Error ? _error.stack : 'N/A',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send error notification to WebView
|
|
||||||
this.sendMessageToWebView({
|
|
||||||
type: 'loginError',
|
|
||||||
data: {
|
|
||||||
message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
throw _error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return AuthStateManager.runExclusiveAuth(() => p);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -737,15 +772,15 @@ export class WebViewProvider {
|
|||||||
* Called when restoring WebView after VSCode restart
|
* Called when restoring WebView after VSCode restart
|
||||||
*/
|
*/
|
||||||
async refreshConnection(): Promise<void> {
|
async refreshConnection(): Promise<void> {
|
||||||
console.log('[WebViewProvider] Refresh connection requested');
|
this.consoleLog('[WebViewProvider] Refresh connection requested');
|
||||||
|
|
||||||
// Disconnect existing connection if any
|
// Disconnect existing connection if any
|
||||||
if (this.agentInitialized) {
|
if (this.agentInitialized) {
|
||||||
try {
|
try {
|
||||||
this.agentManager.disconnect();
|
this.agentManager.disconnect();
|
||||||
console.log('[WebViewProvider] Existing connection disconnected');
|
this.consoleLog('[WebViewProvider] Existing connection disconnected');
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.log('[WebViewProvider] Error disconnecting:', _error);
|
this.consoleLog('[WebViewProvider] Error disconnecting:', _error);
|
||||||
}
|
}
|
||||||
this.agentInitialized = false;
|
this.agentInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -756,7 +791,7 @@ export class WebViewProvider {
|
|||||||
// Reinitialize connection (will use cached auth if available)
|
// Reinitialize connection (will use cached auth if available)
|
||||||
try {
|
try {
|
||||||
await this.initializeAgentConnection();
|
await this.initializeAgentConnection();
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Connection refresh completed successfully',
|
'[WebViewProvider] Connection refresh completed successfully',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -786,35 +821,41 @@ export class WebViewProvider {
|
|||||||
*/
|
*/
|
||||||
private async loadCurrentSessionMessages(): Promise<void> {
|
private async loadCurrentSessionMessages(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Initializing with new session (skipping restoration)',
|
'[WebViewProvider] Initializing with new session (skipping restoration)',
|
||||||
);
|
);
|
||||||
|
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
|
||||||
// Skip session restoration entirely and create a new session directly
|
// avoid creating another session if connect() already created one.
|
||||||
try {
|
if (!this.agentManager.currentSessionId) {
|
||||||
await this.agentManager.createNewSession(
|
try {
|
||||||
workingDir,
|
await this.agentManager.createNewSession(
|
||||||
this.authStateManager,
|
workingDir,
|
||||||
);
|
this.authStateManager,
|
||||||
console.log('[WebViewProvider] ACP session created successfully');
|
);
|
||||||
|
this.consoleLog('[WebViewProvider] ACP session created successfully');
|
||||||
|
|
||||||
// Ensure auth state is saved after successful session creation
|
// Ensure auth state is saved after successful session creation
|
||||||
if (this.authStateManager) {
|
if (this.authStateManager) {
|
||||||
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Auth state saved after session creation',
|
'[WebViewProvider] Auth state saved after session creation',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (sessionError) {
|
||||||
|
console.error(
|
||||||
|
'[WebViewProvider] Failed to create ACP session:',
|
||||||
|
sessionError,
|
||||||
|
);
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (sessionError) {
|
} else {
|
||||||
console.error(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Failed to create ACP session:',
|
'[WebViewProvider] Existing ACP session detected, skipping new session creation',
|
||||||
sessionError,
|
|
||||||
);
|
|
||||||
vscode.window.showWarningMessage(
|
|
||||||
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,14 +878,14 @@ export class WebViewProvider {
|
|||||||
*/
|
*/
|
||||||
private async initializeEmptyConversation(): Promise<void> {
|
private async initializeEmptyConversation(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[WebViewProvider] Initializing empty conversation');
|
this.consoleLog('[WebViewProvider] Initializing empty conversation');
|
||||||
const newConv = await this.conversationStore.createConversation();
|
const newConv = await this.conversationStore.createConversation();
|
||||||
this.messageHandler.setCurrentConversationId(newConv.id);
|
this.messageHandler.setCurrentConversationId(newConv.id);
|
||||||
this.sendMessageToWebView({
|
this.sendMessageToWebView({
|
||||||
type: 'conversationLoaded',
|
type: 'conversationLoaded',
|
||||||
data: newConv,
|
data: newConv,
|
||||||
});
|
});
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Empty conversation initialized:',
|
'[WebViewProvider] Empty conversation initialized:',
|
||||||
this.messageHandler.getCurrentConversationId(),
|
this.messageHandler.getCurrentConversationId(),
|
||||||
);
|
);
|
||||||
@@ -968,7 +1009,7 @@ export class WebViewProvider {
|
|||||||
* Call this when auth cache is cleared to force re-authentication
|
* Call this when auth cache is cleared to force re-authentication
|
||||||
*/
|
*/
|
||||||
resetAgentState(): void {
|
resetAgentState(): void {
|
||||||
console.log('[WebViewProvider] Resetting agent state');
|
this.consoleLog('[WebViewProvider] Resetting agent state');
|
||||||
this.agentInitialized = false;
|
this.agentInitialized = false;
|
||||||
// Disconnect existing connection
|
// Disconnect existing connection
|
||||||
this.agentManager.disconnect();
|
this.agentManager.disconnect();
|
||||||
@@ -978,7 +1019,7 @@ export class WebViewProvider {
|
|||||||
* Clear authentication cache for this WebViewProvider instance
|
* Clear authentication cache for this WebViewProvider instance
|
||||||
*/
|
*/
|
||||||
async clearAuthCache(): Promise<void> {
|
async clearAuthCache(): Promise<void> {
|
||||||
console.log('[WebViewProvider] Clearing auth cache for this instance');
|
this.consoleLog('[WebViewProvider] Clearing auth cache for this instance');
|
||||||
if (this.authStateManager) {
|
if (this.authStateManager) {
|
||||||
await this.authStateManager.clearAuthState();
|
await this.authStateManager.clearAuthState();
|
||||||
this.resetAgentState();
|
this.resetAgentState();
|
||||||
@@ -990,8 +1031,8 @@ export class WebViewProvider {
|
|||||||
* This sets up the panel with all event listeners
|
* This sets up the panel with all event listeners
|
||||||
*/
|
*/
|
||||||
async restorePanel(panel: vscode.WebviewPanel): Promise<void> {
|
async restorePanel(panel: vscode.WebviewPanel): Promise<void> {
|
||||||
console.log('[WebViewProvider] Restoring WebView panel');
|
this.consoleLog('[WebViewProvider] Restoring WebView panel');
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Current authStateManager in restore:',
|
'[WebViewProvider] Current authStateManager in restore:',
|
||||||
!!this.authStateManager,
|
!!this.authStateManager,
|
||||||
);
|
);
|
||||||
@@ -1122,10 +1163,10 @@ export class WebViewProvider {
|
|||||||
// Capture the tab reference on restore
|
// Capture the tab reference on restore
|
||||||
this.panelManager.captureTab();
|
this.panelManager.captureTab();
|
||||||
|
|
||||||
console.log('[WebViewProvider] Panel restored successfully');
|
this.consoleLog('[WebViewProvider] Panel restored successfully');
|
||||||
|
|
||||||
// Attempt to restore authentication state and initialize connection
|
// Attempt to restore authentication state and initialize connection
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Attempting to restore auth state and connection after restore...',
|
'[WebViewProvider] Attempting to restore auth state and connection after restore...',
|
||||||
);
|
);
|
||||||
await this.attemptAuthStateRestoration();
|
await this.attemptAuthStateRestoration();
|
||||||
@@ -1139,12 +1180,12 @@ export class WebViewProvider {
|
|||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
agentInitialized: boolean;
|
agentInitialized: boolean;
|
||||||
} {
|
} {
|
||||||
console.log('[WebViewProvider] Getting state for serialization');
|
this.consoleLog('[WebViewProvider] Getting state for serialization');
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Current conversationId:',
|
'[WebViewProvider] Current conversationId:',
|
||||||
this.messageHandler.getCurrentConversationId(),
|
this.messageHandler.getCurrentConversationId(),
|
||||||
);
|
);
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] Current agentInitialized:',
|
'[WebViewProvider] Current agentInitialized:',
|
||||||
this.agentInitialized,
|
this.agentInitialized,
|
||||||
);
|
);
|
||||||
@@ -1152,7 +1193,7 @@ export class WebViewProvider {
|
|||||||
conversationId: this.messageHandler.getCurrentConversationId(),
|
conversationId: this.messageHandler.getCurrentConversationId(),
|
||||||
agentInitialized: this.agentInitialized,
|
agentInitialized: this.agentInitialized,
|
||||||
};
|
};
|
||||||
console.log('[WebViewProvider] Returning state:', state);
|
this.consoleLog('[WebViewProvider] Returning state:', state);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,10 +1211,10 @@ export class WebViewProvider {
|
|||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
agentInitialized: boolean;
|
agentInitialized: boolean;
|
||||||
}): void {
|
}): void {
|
||||||
console.log('[WebViewProvider] Restoring state:', state);
|
this.consoleLog('[WebViewProvider] Restoring state:', state);
|
||||||
this.messageHandler.setCurrentConversationId(state.conversationId);
|
this.messageHandler.setCurrentConversationId(state.conversationId);
|
||||||
this.agentInitialized = state.agentInitialized;
|
this.agentInitialized = state.agentInitialized;
|
||||||
console.log(
|
this.consoleLog(
|
||||||
'[WebViewProvider] State restored. agentInitialized:',
|
'[WebViewProvider] State restored. agentInitialized:',
|
||||||
this.agentInitialized,
|
this.agentInitialized,
|
||||||
);
|
);
|
||||||
@@ -1206,8 +1247,6 @@ export class WebViewProvider {
|
|||||||
type: 'conversationCleared',
|
type: 'conversationCleared',
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[WebViewProvider] New session created successfully');
|
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.error('[WebViewProvider] Failed to create new session:', _error);
|
console.error('[WebViewProvider] Failed to create new session:', _error);
|
||||||
vscode.window.showErrorMessage(`Failed to create new session: ${_error}`);
|
vscode.window.showErrorMessage(`Failed to create new session: ${_error}`);
|
||||||
|
|||||||
@@ -291,6 +291,41 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure an ACP session exists before sending prompt
|
||||||
|
if (!this.agentManager.currentSessionId) {
|
||||||
|
try {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
await this.agentManager.createNewSession(workingDir);
|
||||||
|
} catch (createErr) {
|
||||||
|
console.error(
|
||||||
|
'[SessionMessageHandler] Failed to create session before sending message:',
|
||||||
|
createErr,
|
||||||
|
);
|
||||||
|
const errorMsg =
|
||||||
|
createErr instanceof Error ? createErr.message : String(createErr);
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)')
|
||||||
|
) {
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwen-code.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send to agent
|
// Send to agent
|
||||||
try {
|
try {
|
||||||
this.resetStreamContent();
|
this.resetStreamContent();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
import type { ToolCallUpdate } from '../../types/chatTypes.js';
|
import type { ToolCallUpdate } from '../../types/chatTypes.js';
|
||||||
import type { ApprovalModeValue } from '../../types/acpTypes.js';
|
import type { ApprovalModeValue } from '../../types/acpTypes.js';
|
||||||
import type { PlanEntry } from '../../types/chatTypes.js';
|
import type { PlanEntry } from '../../types/chatTypes.js';
|
||||||
|
import { createWebviewConsoleLogger } from '../utils/logger.js';
|
||||||
|
|
||||||
interface UseWebViewMessagesProps {
|
interface UseWebViewMessagesProps {
|
||||||
// Session management
|
// Session management
|
||||||
@@ -129,6 +130,7 @@ export const useWebViewMessages = ({
|
|||||||
}: UseWebViewMessagesProps) => {
|
}: UseWebViewMessagesProps) => {
|
||||||
// VS Code API for posting messages back to the extension host
|
// VS Code API for posting messages back to the extension host
|
||||||
const vscode = useVSCode();
|
const vscode = useVSCode();
|
||||||
|
const consoleLog = useRef(createWebviewConsoleLogger('WebViewMessages'));
|
||||||
// Track active long-running tool calls (execute/bash/command) so we can
|
// Track active long-running tool calls (execute/bash/command) so we can
|
||||||
// keep the bottom "waiting" message visible until all of them complete.
|
// keep the bottom "waiting" message visible until all of them complete.
|
||||||
const activeExecToolCallsRef = useRef<Set<string>>(new Set());
|
const activeExecToolCallsRef = useRef<Set<string>>(new Set());
|
||||||
@@ -227,40 +229,26 @@ export const useWebViewMessages = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 'cliNotInstalled': {
|
case 'agentConnected': {
|
||||||
// // Show CLI not installed message
|
// Agent connected successfully; clear any pending spinner
|
||||||
// const errorMsg =
|
handlers.messageHandling.clearWaitingForResponse();
|
||||||
// (message?.data?.error as string) ||
|
break;
|
||||||
// 'Qwen Code CLI is not installed. Please install it to enable full functionality.';
|
}
|
||||||
|
|
||||||
// handlers.messageHandling.addMessage({
|
case 'agentConnectionError': {
|
||||||
// role: 'assistant',
|
// Agent connection failed; surface the error and unblock the UI
|
||||||
// content: `Qwen CLI is not installed. Please install it to enable full functionality.\n\nError: ${errorMsg}\n\nInstallation instructions:\n1. Install via npm:\n npm install -g @qwen-code/qwen-code@latest\n\n2. After installation, reload VS Code or restart the extension.`,
|
handlers.messageHandling.clearWaitingForResponse();
|
||||||
// timestamp: Date.now(),
|
const errorMsg =
|
||||||
// });
|
(message?.data?.message as string) ||
|
||||||
// break;
|
'Failed to connect to Qwen agent.';
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'agentConnected': {
|
handlers.messageHandling.addMessage({
|
||||||
// // Agent connected successfully
|
role: 'assistant',
|
||||||
// handlers.messageHandling.clearWaitingForResponse();
|
content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
||||||
// break;
|
timestamp: Date.now(),
|
||||||
// }
|
});
|
||||||
|
break;
|
||||||
// case 'agentConnectionError': {
|
}
|
||||||
// // Agent connection failed
|
|
||||||
// handlers.messageHandling.clearWaitingForResponse();
|
|
||||||
// const errorMsg =
|
|
||||||
// (message?.data?.message as string) ||
|
|
||||||
// 'Failed to connect to Qwen agent.';
|
|
||||||
|
|
||||||
// handlers.messageHandling.addMessage({
|
|
||||||
// role: 'assistant',
|
|
||||||
// content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
|
||||||
// timestamp: Date.now(),
|
|
||||||
// });
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
case 'loginError': {
|
case 'loginError': {
|
||||||
// Clear loading state and show error notice
|
// Clear loading state and show error notice
|
||||||
@@ -765,7 +753,10 @@ export const useWebViewMessages = ({
|
|||||||
path: string;
|
path: string;
|
||||||
}>;
|
}>;
|
||||||
if (files) {
|
if (files) {
|
||||||
console.log('[WebView] Received workspaceFiles:', files.length);
|
consoleLog.current(
|
||||||
|
'[WebView] Received workspaceFiles:',
|
||||||
|
files.length,
|
||||||
|
);
|
||||||
handlers.fileContext.setWorkspaceFiles(files);
|
handlers.fileContext.setWorkspaceFiles(files);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* Import component styles */
|
/* Import component styles */
|
||||||
@import '../components/messages/Assistant/AssistantMessage.css';
|
|
||||||
@import './timeline.css';
|
@import './timeline.css';
|
||||||
@import '../components/messages/MarkdownRenderer/MarkdownRenderer.css';
|
@import '../components/messages/MarkdownRenderer/MarkdownRenderer.css';
|
||||||
|
|
||||||
|
|||||||
25
packages/vscode-ide-companion/src/webview/utils/logger.ts
Normal file
25
packages/vscode-ide-companion/src/webview/utils/logger.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dev-only console logger for the WebView bundle.
|
||||||
|
* In production builds it becomes a no-op to avoid noisy logs.
|
||||||
|
*/
|
||||||
|
export function createWebviewConsoleLogger(scope?: string) {
|
||||||
|
return (...args: unknown[]) => {
|
||||||
|
const env = (globalThis as { process?: { env?: Record<string, string> } })
|
||||||
|
.process?.env;
|
||||||
|
const isProduction = env?.NODE_ENV === 'production';
|
||||||
|
if (isProduction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scope) {
|
||||||
|
console.log(`[${scope}]`, ...args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen Team
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*
|
|
||||||
* Minimal line-diff utility for webview previews.
|
|
||||||
*
|
|
||||||
* This is a lightweight LCS-based algorithm to compute add/remove operations
|
|
||||||
* between two texts. It intentionally avoids heavy dependencies and is
|
|
||||||
* sufficient for rendering a compact preview inside the chat.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type DiffOp =
|
|
||||||
| { type: 'add'; line: string; newIndex: number }
|
|
||||||
| { type: 'remove'; line: string; oldIndex: number };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute a minimal line-diff (added/removed only).
|
|
||||||
* - Equal lines are omitted from output by design (we only preview changes).
|
|
||||||
* - Order of operations follows the new text progression so the preview feels natural.
|
|
||||||
*/
|
|
||||||
export function computeLineDiff(
|
|
||||||
oldText: string | null | undefined,
|
|
||||||
newText: string | undefined,
|
|
||||||
): DiffOp[] {
|
|
||||||
const a = (oldText || '').split('\n');
|
|
||||||
const b = (newText || '').split('\n');
|
|
||||||
|
|
||||||
const n = a.length;
|
|
||||||
const m = b.length;
|
|
||||||
|
|
||||||
// Build LCS DP table
|
|
||||||
const dp: number[][] = Array.from({ length: n + 1 }, () =>
|
|
||||||
new Array(m + 1).fill(0),
|
|
||||||
);
|
|
||||||
for (let i = n - 1; i >= 0; i--) {
|
|
||||||
for (let j = m - 1; j >= 0; j--) {
|
|
||||||
if (a[i] === b[j]) {
|
|
||||||
dp[i][j] = dp[i + 1][j + 1] + 1;
|
|
||||||
} else {
|
|
||||||
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk to produce operations
|
|
||||||
const ops: DiffOp[] = [];
|
|
||||||
let i = 0;
|
|
||||||
let j = 0;
|
|
||||||
while (i < n && j < m) {
|
|
||||||
if (a[i] === b[j]) {
|
|
||||||
i++;
|
|
||||||
j++;
|
|
||||||
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
||||||
// remove a[i]
|
|
||||||
ops.push({ type: 'remove', line: a[i], oldIndex: i });
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
// add b[j]
|
|
||||||
ops.push({ type: 'add', line: b[j], newIndex: j });
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remaining tails
|
|
||||||
while (i < n) {
|
|
||||||
ops.push({ type: 'remove', line: a[i], oldIndex: i });
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
while (j < m) {
|
|
||||||
ops.push({ type: 'add', line: b[j], newIndex: j });
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ops;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Truncate a long list of operations for preview purposes.
|
|
||||||
* Keeps first `head` and last `tail` operations, inserting a gap marker.
|
|
||||||
*/
|
|
||||||
export function truncateOps<T>(
|
|
||||||
ops: T[],
|
|
||||||
head = 120,
|
|
||||||
tail = 80,
|
|
||||||
): { items: T[]; truncated: boolean; omitted: number } {
|
|
||||||
if (ops.length <= head + tail) {
|
|
||||||
return { items: ops, truncated: false, omitted: 0 };
|
|
||||||
}
|
|
||||||
const items = [...ops.slice(0, head), ...ops.slice(-tail)];
|
|
||||||
return { items, truncated: true, omitted: ops.length - head - tail };
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user