mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
wip(vscode-ide-companion): OnboardingPage
This commit is contained in:
@@ -24,6 +24,125 @@ export class CliDetector {
|
||||
private static lastCheckTime: number = 0;
|
||||
private static readonly CACHE_DURATION_MS = 30000; // 30 seconds
|
||||
|
||||
/**
|
||||
* Lightweight check if the Qwen Code CLI is installed
|
||||
* This version only checks for CLI existence without getting version info for faster performance
|
||||
* @param forceRefresh - Force a new check, ignoring cache
|
||||
* @returns Detection result with installation status and path
|
||||
*/
|
||||
static async detectQwenCliLightweight(
|
||||
forceRefresh = false,
|
||||
): Promise<CliDetectionResult> {
|
||||
const now = Date.now();
|
||||
|
||||
// Return cached result if available and not expired
|
||||
if (
|
||||
!forceRefresh &&
|
||||
this.cachedResult &&
|
||||
now - this.lastCheckTime < this.CACHE_DURATION_MS
|
||||
) {
|
||||
console.log('[CliDetector] Returning cached result');
|
||||
return this.cachedResult;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[CliDetector] Starting lightweight CLI detection, current PATH:',
|
||||
process.env.PATH,
|
||||
);
|
||||
|
||||
try {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const whichCommand = isWindows ? 'where' : 'which';
|
||||
|
||||
// Check if qwen command exists
|
||||
try {
|
||||
// Use simpler detection without NVM for speed
|
||||
const detectionCommand = isWindows
|
||||
? `${whichCommand} qwen`
|
||||
: `${whichCommand} qwen`;
|
||||
|
||||
console.log(
|
||||
'[CliDetector] Detecting CLI with lightweight command:',
|
||||
detectionCommand,
|
||||
);
|
||||
|
||||
const { stdout } = await execAsync(detectionCommand, {
|
||||
timeout: 3000, // Reduced timeout for faster detection
|
||||
shell: isWindows ? undefined : '/bin/bash',
|
||||
});
|
||||
|
||||
// The output may contain multiple lines
|
||||
// We want the first line which should be the actual path
|
||||
const lines = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim());
|
||||
const cliPath = lines[0]; // Just take the first path
|
||||
|
||||
console.log('[CliDetector] Found CLI at:', cliPath);
|
||||
|
||||
this.cachedResult = {
|
||||
isInstalled: true,
|
||||
cliPath,
|
||||
// Version is not retrieved in lightweight detection
|
||||
};
|
||||
this.lastCheckTime = now;
|
||||
return this.cachedResult;
|
||||
} catch (detectionError) {
|
||||
console.log('[CliDetector] CLI not found, error:', detectionError);
|
||||
// CLI not found
|
||||
let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`;
|
||||
|
||||
// Provide specific guidance for permission errors
|
||||
if (detectionError instanceof Error) {
|
||||
const errorMessage = detectionError.message;
|
||||
if (
|
||||
errorMessage.includes('EACCES') ||
|
||||
errorMessage.includes('Permission denied')
|
||||
) {
|
||||
error += `\n\nThis may be due to permission issues. Possible solutions:
|
||||
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
||||
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
||||
\n3. Use nvm for Node.js version management to avoid permission issues
|
||||
\n4. Check your PATH environment variable includes npm's global bin directory`;
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedResult = {
|
||||
isInstalled: false,
|
||||
error,
|
||||
};
|
||||
this.lastCheckTime = now;
|
||||
return this.cachedResult;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[CliDetector] General detection error:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`;
|
||||
|
||||
// Provide specific guidance for permission errors
|
||||
if (
|
||||
errorMessage.includes('EACCES') ||
|
||||
errorMessage.includes('Permission denied')
|
||||
) {
|
||||
userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions:
|
||||
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
||||
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
||||
\n3. Use nvm for Node.js version management to avoid permission issues
|
||||
\n4. Check your PATH environment variable includes npm's global bin directory`;
|
||||
}
|
||||
|
||||
this.cachedResult = {
|
||||
isInstalled: false,
|
||||
error: userFriendlyError,
|
||||
};
|
||||
this.lastCheckTime = now;
|
||||
return this.cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Qwen Code CLI is installed
|
||||
* @param forceRefresh - Force a new check, ignoring cache
|
||||
|
||||
@@ -42,7 +42,9 @@ export class AcpConnection {
|
||||
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
|
||||
optionId: string;
|
||||
}> = () => Promise.resolve({ optionId: 'allow' });
|
||||
onEndTurn: () => void = () => {};
|
||||
onEndTurn: (reason?: string) => void = (reason?: string | undefined) => {
|
||||
console.log('[ACP] onEndTurn__________ reason:', reason || 'unknown');
|
||||
};
|
||||
// Called after successful initialize() with the initialize result
|
||||
onInitialized: (init: unknown) => void = () => {};
|
||||
|
||||
|
||||
@@ -110,14 +110,31 @@ export class AcpMessageHandler {
|
||||
// JSON.stringify(message.result).substring(0, 200),
|
||||
message.result,
|
||||
);
|
||||
if (
|
||||
|
||||
console.log(
|
||||
'[ACP] Response for message.result:',
|
||||
message.result,
|
||||
message.result &&
|
||||
typeof message.result === 'object' &&
|
||||
'stopReason' in message.result &&
|
||||
message.result.stopReason === 'end_turn'
|
||||
'stopReason' in message.result,
|
||||
|
||||
!!callbacks.onEndTurn,
|
||||
);
|
||||
|
||||
if (message.result && typeof message.result === 'object') {
|
||||
const stopReasonValue =
|
||||
(message.result as { stopReason?: unknown }).stopReason ??
|
||||
(message.result as { stop_reason?: unknown }).stop_reason;
|
||||
if (typeof stopReasonValue === 'string') {
|
||||
callbacks.onEndTurn(stopReasonValue);
|
||||
} else if (
|
||||
'stopReason' in message.result ||
|
||||
'stop_reason' in message.result
|
||||
) {
|
||||
// stop_reason present but not a string (e.g., null) -> still emit
|
||||
callbacks.onEndTurn();
|
||||
}
|
||||
}
|
||||
resolve(message.result);
|
||||
} else if ('error' in message) {
|
||||
const errorCode = message.error?.code || 'unknown';
|
||||
|
||||
@@ -17,11 +17,18 @@ import type {
|
||||
ToolCallUpdateData,
|
||||
QwenAgentCallbacks,
|
||||
} from '../types/chatTypes.js';
|
||||
import { QwenConnectionHandler } from '../services/qwenConnectionHandler.js';
|
||||
import {
|
||||
QwenConnectionHandler,
|
||||
type QwenConnectionResult,
|
||||
} from '../services/qwenConnectionHandler.js';
|
||||
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
|
||||
import { CliContextManager } from '../cli/cliContextManager.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,
|
||||
type CliVersionInfo,
|
||||
} from '../cli/cliVersionManager.js';
|
||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||
|
||||
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
|
||||
|
||||
@@ -30,6 +37,13 @@ export type { ChatMessage, PlanEntry, ToolCallUpdateData };
|
||||
*
|
||||
* Coordinates various modules and provides unified interface
|
||||
*/
|
||||
interface AgentConnectOptions {
|
||||
autoAuthenticate?: boolean;
|
||||
}
|
||||
interface AgentSessionOptions {
|
||||
autoAuthenticate?: boolean;
|
||||
}
|
||||
|
||||
export class QwenAgentManager {
|
||||
private connection: AcpConnection;
|
||||
private sessionReader: QwenSessionReader;
|
||||
@@ -119,10 +133,10 @@ export class QwenAgentManager {
|
||||
return { optionId: 'allow_once' };
|
||||
};
|
||||
|
||||
this.connection.onEndTurn = () => {
|
||||
this.connection.onEndTurn = (reason?: string) => {
|
||||
try {
|
||||
if (this.callbacks.onEndTurn) {
|
||||
this.callbacks.onEndTurn();
|
||||
this.callbacks.onEndTurn(reason);
|
||||
} else if (this.callbacks.onStreamChunk) {
|
||||
// Fallback: send a zero-length chunk then rely on streamEnd elsewhere
|
||||
this.callbacks.onStreamChunk('');
|
||||
@@ -136,6 +150,36 @@ export class QwenAgentManager {
|
||||
this.connection.onInitialized = (init: unknown) => {
|
||||
try {
|
||||
const obj = (init || {}) as Record<string, unknown>;
|
||||
|
||||
// Extract version information from initialize response
|
||||
const serverVersion =
|
||||
obj['version'] || obj['serverVersion'] || obj['cliVersion'];
|
||||
if (serverVersion) {
|
||||
console.log(
|
||||
'[QwenAgentManager] Server version from initialize response:',
|
||||
serverVersion,
|
||||
);
|
||||
|
||||
// Update CLI context with version info from server
|
||||
const cliContextManager = CliContextManager.getInstance();
|
||||
|
||||
// Create version info directly without async call
|
||||
const versionInfo: CliVersionInfo = {
|
||||
version: String(serverVersion),
|
||||
isSupported: true, // Assume supported for now
|
||||
features: {
|
||||
supportsSessionList: true,
|
||||
supportsSessionLoad: true,
|
||||
},
|
||||
detectionResult: {
|
||||
isInstalled: true,
|
||||
version: String(serverVersion),
|
||||
},
|
||||
};
|
||||
|
||||
cliContextManager.setCurrentVersionInfo(versionInfo);
|
||||
}
|
||||
|
||||
const modes = obj['modes'] as
|
||||
| {
|
||||
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
|
||||
@@ -164,13 +208,18 @@ export class QwenAgentManager {
|
||||
* @param workingDir - Working directory
|
||||
* @param cliPath - CLI path (optional, if provided will override the path in configuration)
|
||||
*/
|
||||
async connect(workingDir: string, _cliPath?: string): Promise<void> {
|
||||
async connect(
|
||||
workingDir: string,
|
||||
_cliPath?: string,
|
||||
options?: AgentConnectOptions,
|
||||
): Promise<QwenConnectionResult> {
|
||||
this.currentWorkingDir = workingDir;
|
||||
await this.connectionHandler.connect(
|
||||
return this.connectionHandler.connect(
|
||||
this.connection,
|
||||
this.sessionReader,
|
||||
workingDir,
|
||||
_cliPath,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1170,7 +1219,11 @@ export class QwenAgentManager {
|
||||
* @param workingDir - Working directory
|
||||
* @returns Newly created session ID
|
||||
*/
|
||||
async createNewSession(workingDir: string): Promise<string | null> {
|
||||
async createNewSession(
|
||||
workingDir: string,
|
||||
options?: AgentSessionOptions,
|
||||
): Promise<string | null> {
|
||||
const autoAuthenticate = options?.autoAuthenticate ?? true;
|
||||
// Reuse existing session if present
|
||||
if (this.connection.currentSessionId) {
|
||||
return this.connection.currentSessionId;
|
||||
@@ -1188,12 +1241,15 @@ export class QwenAgentManager {
|
||||
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)');
|
||||
const requiresAuth = isAuthenticationRequiredError(err);
|
||||
|
||||
if (requiresAuth) {
|
||||
if (!autoAuthenticate) {
|
||||
console.warn(
|
||||
'[QwenAgentManager] session/new requires authentication but auto-auth is disabled. Deferring until user logs in.',
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
console.warn(
|
||||
'[QwenAgentManager] session/new requires authentication. Retrying with authenticate...',
|
||||
);
|
||||
@@ -1310,9 +1366,10 @@ export class QwenAgentManager {
|
||||
/**
|
||||
* Register end-of-turn callback
|
||||
*
|
||||
* @param callback - Called when ACP stopReason === 'end_turn'
|
||||
* @param callback - Called when ACP stopReason is reported
|
||||
*/
|
||||
onEndTurn(callback: () => void): void {
|
||||
onEndTurn(callback: (reason?: string) => void): void {
|
||||
console.log('[QwenAgentManager] onEndTurn__________ callback:', callback);
|
||||
this.callbacks.onEndTurn = callback;
|
||||
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
|
||||
}
|
||||
|
||||
@@ -10,15 +10,17 @@
|
||||
* Handles Qwen Agent connection establishment, authentication, and session creation
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
// import * as vscode from 'vscode';
|
||||
import type { AcpConnection } from './acpConnection.js';
|
||||
import type { QwenSessionReader } from '../services/qwenSessionReader.js';
|
||||
import {
|
||||
CliVersionManager,
|
||||
MIN_CLI_VERSION_FOR_SESSION_METHODS,
|
||||
} from '../cli/cliVersionManager.js';
|
||||
import { CliContextManager } from '../cli/cliContextManager.js';
|
||||
import { CliDetector } from '../cli/cliDetector.js';
|
||||
import { authMethod } from '../types/acpTypes.js';
|
||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||
|
||||
export interface QwenConnectionResult {
|
||||
sessionCreated: boolean;
|
||||
requiresAuth: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Qwen Connection Handler class
|
||||
@@ -38,36 +40,39 @@ export class QwenConnectionHandler {
|
||||
sessionReader: QwenSessionReader,
|
||||
workingDir: string,
|
||||
cliPath?: string,
|
||||
): Promise<void> {
|
||||
options?: {
|
||||
autoAuthenticate?: boolean;
|
||||
},
|
||||
): Promise<QwenConnectionResult> {
|
||||
const connectId = Date.now();
|
||||
console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`);
|
||||
const autoAuthenticate = options?.autoAuthenticate ?? true;
|
||||
let sessionCreated = false;
|
||||
let requiresAuth = false;
|
||||
|
||||
// Check CLI version and features
|
||||
const cliVersionManager = CliVersionManager.getInstance();
|
||||
const versionInfo = await cliVersionManager.detectCliVersion();
|
||||
console.log('[QwenAgentManager] CLI version info:', versionInfo);
|
||||
|
||||
// Store CLI context
|
||||
const cliContextManager = CliContextManager.getInstance();
|
||||
cliContextManager.setCurrentVersionInfo(versionInfo);
|
||||
|
||||
// Show warning if CLI version is below minimum requirement
|
||||
if (!versionInfo.isSupported) {
|
||||
// Wait to determine release version number
|
||||
vscode.window.showWarningMessage(
|
||||
`Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`,
|
||||
// Lightweight check if CLI exists (without version info for faster performance)
|
||||
const detectionResult = await CliDetector.detectQwenCliLightweight(
|
||||
/* forceRefresh */ true,
|
||||
);
|
||||
if (!detectionResult.isInstalled) {
|
||||
throw new Error(detectionResult.error || 'Qwen CLI not found');
|
||||
}
|
||||
console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath);
|
||||
|
||||
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||
// Use the provided CLI path if available, otherwise use the configured path
|
||||
const effectiveCliPath =
|
||||
cliPath || config.get<string>('qwen.cliPath', 'qwen');
|
||||
// TODO: @yiliang114. closed temporarily
|
||||
// Show warning if CLI version is below minimum requirement
|
||||
// if (!versionInfo.isSupported) {
|
||||
// // Wait to determine release version number
|
||||
// vscode.window.showWarningMessage(
|
||||
// `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`,
|
||||
// );
|
||||
// }
|
||||
|
||||
// Build extra CLI arguments (only essential parameters)
|
||||
const extraArgs: string[] = [];
|
||||
|
||||
await connection.connect(effectiveCliPath, workingDir, extraArgs);
|
||||
// TODO:
|
||||
await connection.connect(cliPath!, workingDir, extraArgs);
|
||||
|
||||
// Try to restore existing session or create new session
|
||||
// Note: Auto-restore on connect is disabled to avoid surprising loads
|
||||
@@ -85,18 +90,40 @@ export class QwenConnectionHandler {
|
||||
console.log(
|
||||
'[QwenAgentManager] Creating new session (letting CLI handle authentication)...',
|
||||
);
|
||||
await this.newSessionWithRetry(connection, workingDir, 3, authMethod);
|
||||
await this.newSessionWithRetry(
|
||||
connection,
|
||||
workingDir,
|
||||
3,
|
||||
authMethod,
|
||||
autoAuthenticate,
|
||||
);
|
||||
console.log('[QwenAgentManager] New session created successfully');
|
||||
sessionCreated = true;
|
||||
} catch (sessionError) {
|
||||
console.log(`\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`);
|
||||
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(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`);
|
||||
console.log(`========================================\n`);
|
||||
return { sessionCreated, requiresAuth };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,6 +138,7 @@ export class QwenConnectionHandler {
|
||||
workingDir: string,
|
||||
maxRetries: number,
|
||||
authMethod: string,
|
||||
autoAuthenticate: boolean,
|
||||
): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
@@ -130,10 +158,14 @@ export class QwenConnectionHandler {
|
||||
|
||||
// If Qwen reports that authentication is required, try to
|
||||
// authenticate on-the-fly once and retry without waiting.
|
||||
const requiresAuth =
|
||||
errorMessage.includes('Authentication required') ||
|
||||
errorMessage.includes('(code: -32000)');
|
||||
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...',
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface QwenAgentCallbacks {
|
||||
onToolCall?: (update: ToolCallUpdateData) => void;
|
||||
onPlan?: (entries: PlanEntry[]) => void;
|
||||
onPermissionRequest?: (request: AcpPermissionRequest) => Promise<string>;
|
||||
onEndTurn?: () => void;
|
||||
onEndTurn?: (reason?: string) => void;
|
||||
onModeInfo?: (info: {
|
||||
currentModeId?: ApprovalModeValue;
|
||||
availableModes?: Array<{
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface AcpConnectionCallbacks {
|
||||
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
|
||||
optionId: string;
|
||||
}>;
|
||||
onEndTurn: () => void;
|
||||
onEndTurn: (reason?: string) => void;
|
||||
}
|
||||
|
||||
export interface AcpConnectionState {
|
||||
|
||||
26
packages/vscode-ide-companion/src/utils/authErrors.ts
Normal file
26
packages/vscode-ide-companion/src/utils/authErrors.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
const AUTH_ERROR_PATTERNS = [
|
||||
'Authentication required',
|
||||
'(code: -32000)',
|
||||
'Unauthorized',
|
||||
'Invalid token',
|
||||
'Session expired',
|
||||
];
|
||||
|
||||
export const isAuthenticationRequiredError = (error: unknown): boolean => {
|
||||
if (!error) {
|
||||
return false;
|
||||
}
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: String(error);
|
||||
return AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
|
||||
};
|
||||
@@ -29,6 +29,7 @@ import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer
|
||||
import { ToolCall } from './components/messages/toolcalls/ToolCall.js';
|
||||
import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js';
|
||||
import { EmptyState } from './components/layout/EmptyState.js';
|
||||
import { OnboardingPage } from './components/layout/OnboardingPage.js';
|
||||
import { type CompletionItem } from '../types/completionItemTypes.js';
|
||||
import { useCompletionTrigger } from './hooks/useCompletionTrigger.js';
|
||||
import { ChatHeader } from './components/layout/ChatHeader.js';
|
||||
@@ -67,6 +68,7 @@ export const App: React.FC = () => {
|
||||
toolCall: PermissionToolCall;
|
||||
} | null>(null);
|
||||
const [planEntries, setPlanEntries] = useState<PlanEntry[]>([]);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(
|
||||
null,
|
||||
) as React.RefObject<HTMLDivElement>;
|
||||
@@ -176,6 +178,7 @@ export const App: React.FC = () => {
|
||||
vscode,
|
||||
inputFieldRef,
|
||||
isStreaming: messageHandling.isStreaming,
|
||||
isWaitingForResponse: messageHandling.isWaitingForResponse,
|
||||
});
|
||||
|
||||
// Handle cancel/stop from the input bar
|
||||
@@ -218,6 +221,7 @@ export const App: React.FC = () => {
|
||||
inputFieldRef,
|
||||
setInputText,
|
||||
setEditMode,
|
||||
setIsAuthenticated,
|
||||
});
|
||||
|
||||
// Auto-scroll handling: keep the view pinned to bottom when new content arrives,
|
||||
@@ -662,26 +666,37 @@ export const App: React.FC = () => {
|
||||
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className="chat-messages messages-container flex-1 overflow-y-auto overflow-x-hidden pt-5 pr-5 pl-5 pb-[120px] flex flex-col relative min-w-0 focus:outline-none [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-thumb]:rounded-sm [&::-webkit-scrollbar-thumb]:hover:bg-white/30 [&>*]:flex [&>*]:gap-0 [&>*]:items-start [&>*]:text-left [&>*]:py-2 [&>*:not(:last-child)]:pb-[8px] [&>*]:flex-col [&>*]:relative [&>*]:animate-[fadeIn_0.2s_ease-in]"
|
||||
className="chat-messages messages-container flex-1 overflow-y-auto overflow-x-hidden pt-5 pr-5 pl-5 pb-[140px] flex flex-col relative min-w-0 focus:outline-none [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-thumb]:rounded-sm [&::-webkit-scrollbar-thumb]:hover:bg-white/30 [&>*]:flex [&>*]:gap-0 [&>*]:items-start [&>*]:text-left [&>*]:py-2 [&>*:not(:last-child)]:pb-[8px] [&>*]:flex-col [&>*]:relative [&>*]:animate-[fadeIn_0.2s_ease-in]"
|
||||
>
|
||||
{!hasContent ? (
|
||||
<EmptyState />
|
||||
isAuthenticated === false ? (
|
||||
<OnboardingPage
|
||||
onLogin={() => {
|
||||
vscode.postMessage({ type: 'login', data: {} });
|
||||
messageHandling.setWaitingForResponse(
|
||||
'Logging in to Qwen Code...',
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : isAuthenticated === null ? (
|
||||
<EmptyState loadingMessage="Checking login status…" />
|
||||
) : (
|
||||
<EmptyState isAuthenticated />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{/* Render all messages and tool calls */}
|
||||
{renderMessages()}
|
||||
{/* Flow-in persistent slot: keeps a small constant height so toggling */}
|
||||
{/* the waiting message doesn't change list height to zero. When */}
|
||||
{/* active, render the waiting message inline (not fixed). */}
|
||||
<div className="waiting-message-slot min-h-[28px]">
|
||||
|
||||
{/* Waiting message positioned fixed above the input form to avoid layout shifts */}
|
||||
{messageHandling.isWaitingForResponse &&
|
||||
messageHandling.loadingMessage && (
|
||||
<div className="waiting-message-slot min-h-[28px]">
|
||||
<WaitingMessage
|
||||
loadingMessage={messageHandling.loadingMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { WebViewContent } from '../webview/WebViewContent.js';
|
||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||
import { getFileName } from './utils/webviewUtils.js';
|
||||
import { type ApprovalModeValue } from '../types/acpTypes.js';
|
||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||
|
||||
export class WebViewProvider {
|
||||
private panelManager: PanelManager;
|
||||
@@ -119,12 +120,16 @@ export class WebViewProvider {
|
||||
});
|
||||
});
|
||||
|
||||
// Setup end-turn handler from ACP stopReason=end_turn
|
||||
this.agentManager.onEndTurn(() => {
|
||||
// Setup end-turn handler from ACP stopReason notifications
|
||||
this.agentManager.onEndTurn((reason) => {
|
||||
console.log(' ============== ', reason);
|
||||
// Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere
|
||||
this.sendMessageToWebView({
|
||||
type: 'streamEnd',
|
||||
data: { timestamp: Date.now(), reason: 'end_turn' },
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
reason: reason || 'end_turn',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -520,10 +525,10 @@ export class WebViewProvider {
|
||||
private async attemptAuthStateRestoration(): Promise<void> {
|
||||
try {
|
||||
console.log(
|
||||
'[WebViewProvider] Attempting connection (CLI handle authentication)...',
|
||||
'[WebViewProvider] Attempting connection (without auto-auth)...',
|
||||
);
|
||||
//always attempt connection and let CLI handle authentication
|
||||
await this.initializeAgentConnection();
|
||||
// Attempt a lightweight connection to detect prior auth without forcing login
|
||||
await this.initializeAgentConnection({ autoAuthenticate: false });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[WebViewProvider] Error in attemptAuthStateRestoration:',
|
||||
@@ -537,14 +542,19 @@ export class WebViewProvider {
|
||||
* Initialize agent connection and session
|
||||
* Can be called from show() or via /login command
|
||||
*/
|
||||
async initializeAgentConnection(): Promise<void> {
|
||||
return this.doInitializeAgentConnection();
|
||||
async initializeAgentConnection(options?: {
|
||||
autoAuthenticate?: boolean;
|
||||
}): Promise<void> {
|
||||
return this.doInitializeAgentConnection(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: perform actual connection/initialization (no auth locking).
|
||||
*/
|
||||
private async doInitializeAgentConnection(): Promise<void> {
|
||||
private async doInitializeAgentConnection(options?: {
|
||||
autoAuthenticate?: boolean;
|
||||
}): Promise<void> {
|
||||
const autoAuthenticate = options?.autoAuthenticate ?? true;
|
||||
const run = async () => {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
@@ -553,7 +563,9 @@ export class WebViewProvider {
|
||||
'[WebViewProvider] Starting initialization, workingDir:',
|
||||
workingDir,
|
||||
);
|
||||
console.log('[WebViewProvider] Using CLI-managed authentication');
|
||||
console.log(
|
||||
`[WebViewProvider] Using CLI-managed authentication (autoAuth=${autoAuthenticate})`,
|
||||
);
|
||||
|
||||
// Check if CLI is installed before attempting to connect
|
||||
const cliDetection = await CliDetector.detectQwenCli();
|
||||
@@ -583,18 +595,34 @@ export class WebViewProvider {
|
||||
console.log('[WebViewProvider] Connecting to agent...');
|
||||
|
||||
// Pass the detected CLI path to ensure we use the correct installation
|
||||
await this.agentManager.connect(workingDir, cliDetection.cliPath);
|
||||
const connectResult = await this.agentManager.connect(
|
||||
workingDir,
|
||||
cliDetection.cliPath,
|
||||
options,
|
||||
);
|
||||
console.log('[WebViewProvider] Agent connected successfully');
|
||||
this.agentInitialized = true;
|
||||
if (connectResult.requiresAuth) {
|
||||
this.sendMessageToWebView({
|
||||
type: 'authState',
|
||||
data: { authenticated: false },
|
||||
});
|
||||
}
|
||||
|
||||
// Load messages from the current Qwen session
|
||||
await this.loadCurrentSessionMessages();
|
||||
const sessionReady = await this.loadCurrentSessionMessages(options);
|
||||
|
||||
if (sessionReady) {
|
||||
// Notify webview that agent is connected
|
||||
this.sendMessageToWebView({
|
||||
type: 'agentConnected',
|
||||
data: {},
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
'[WebViewProvider] Session creation deferred until user logs in.',
|
||||
);
|
||||
}
|
||||
} catch (_error) {
|
||||
console.error('[WebViewProvider] Agent connection error:', _error);
|
||||
vscode.window.showWarningMessage(
|
||||
@@ -654,7 +682,7 @@ export class WebViewProvider {
|
||||
});
|
||||
|
||||
// Reinitialize connection (will trigger fresh authentication)
|
||||
await this.doInitializeAgentConnection();
|
||||
await this.doInitializeAgentConnection({ autoAuthenticate: true });
|
||||
console.log(
|
||||
'[WebViewProvider] Force re-login completed successfully',
|
||||
);
|
||||
@@ -737,7 +765,11 @@ export class WebViewProvider {
|
||||
* Load messages from current Qwen session
|
||||
* Skips session restoration and creates a new session directly
|
||||
*/
|
||||
private async loadCurrentSessionMessages(): Promise<void> {
|
||||
private async loadCurrentSessionMessages(options?: {
|
||||
autoAuthenticate?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const autoAuthenticate = options?.autoAuthenticate ?? true;
|
||||
let sessionReady = false;
|
||||
try {
|
||||
console.log(
|
||||
'[WebViewProvider] Initializing with new session (skipping restoration)',
|
||||
@@ -748,10 +780,32 @@ export class WebViewProvider {
|
||||
|
||||
// avoid creating another session if connect() already created one.
|
||||
if (!this.agentManager.currentSessionId) {
|
||||
if (!autoAuthenticate) {
|
||||
console.log(
|
||||
'[WebViewProvider] Skipping ACP session creation until user logs in.',
|
||||
);
|
||||
this.sendMessageToWebView({
|
||||
type: 'authState',
|
||||
data: { authenticated: false },
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await this.agentManager.createNewSession(workingDir);
|
||||
await this.agentManager.createNewSession(workingDir, {
|
||||
autoAuthenticate,
|
||||
});
|
||||
console.log('[WebViewProvider] ACP session created successfully');
|
||||
sessionReady = true;
|
||||
} catch (sessionError) {
|
||||
const requiresAuth = isAuthenticationRequiredError(sessionError);
|
||||
if (requiresAuth && !autoAuthenticate) {
|
||||
console.log(
|
||||
'[WebViewProvider] ACP session requires authentication; waiting for explicit login.',
|
||||
);
|
||||
this.sendMessageToWebView({
|
||||
type: 'authState',
|
||||
data: { authenticated: false },
|
||||
});
|
||||
} else {
|
||||
console.error(
|
||||
'[WebViewProvider] Failed to create ACP session:',
|
||||
sessionError,
|
||||
@@ -760,10 +814,13 @@ export class WebViewProvider {
|
||||
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
'[WebViewProvider] Existing ACP session detected, skipping new session creation',
|
||||
);
|
||||
sessionReady = true;
|
||||
}
|
||||
|
||||
await this.initializeEmptyConversation();
|
||||
@@ -776,7 +833,10 @@ export class WebViewProvider {
|
||||
`Failed to load session messages: ${_error}`,
|
||||
);
|
||||
await this.initializeEmptyConversation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return sessionReady;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,10 +7,24 @@
|
||||
import type React from 'react';
|
||||
import { generateIconUrl } from '../../utils/resourceUrl.js';
|
||||
|
||||
export const EmptyState: React.FC = () => {
|
||||
interface EmptyStateProps {
|
||||
isAuthenticated?: boolean;
|
||||
loadingMessage?: string;
|
||||
}
|
||||
|
||||
export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
isAuthenticated = false,
|
||||
loadingMessage,
|
||||
}) => {
|
||||
// Generate icon URL using the utility function
|
||||
const iconUri = generateIconUrl('icon.png');
|
||||
|
||||
const description = loadingMessage
|
||||
? 'Preparing Qwen Code…'
|
||||
: isAuthenticated
|
||||
? 'What would you like to do? Ask about this codebase or we can start writing code.'
|
||||
: 'Welcome! Please log in to start using Qwen Code.';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-5 md:p-10">
|
||||
<div className="flex flex-col items-center gap-8 w-full">
|
||||
@@ -23,9 +37,14 @@ export const EmptyState: React.FC = () => {
|
||||
/>
|
||||
<div className="text-center">
|
||||
<div className="text-[15px] text-app-primary-foreground leading-normal font-normal max-w-[400px]">
|
||||
What to do first? Ask about this codebase or we can start writing
|
||||
code.
|
||||
{description}
|
||||
</div>
|
||||
{loadingMessage && (
|
||||
<div className="flex items-center justify-center gap-2 mt-4 text-sm text-app-secondary-foreground">
|
||||
<span className="inline-block h-3 w-3 rounded-full border-2 border-app-secondary-foreground/40 border-t-app-primary-foreground animate-spin" />
|
||||
<span>{loadingMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,6 +113,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
onCompletionClose,
|
||||
}) => {
|
||||
const editModeInfo = getEditModeInfo(editMode);
|
||||
const composerDisabled = isStreaming || isWaitingForResponse;
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
// ESC should cancel the current interaction (stop generation)
|
||||
@@ -144,7 +145,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="p-1 px-4 pb-4"
|
||||
className="p-1 px-4 pb-4 absolute bottom-0 left-0 right-0"
|
||||
style={{ backgroundColor: 'var(--app-primary-background)' }}
|
||||
>
|
||||
<div className="block">
|
||||
@@ -171,7 +172,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
|
||||
<div
|
||||
ref={inputFieldRef}
|
||||
contentEditable="plaintext-only"
|
||||
contentEditable={'plaintext-only'}
|
||||
className="composer-input"
|
||||
role="textbox"
|
||||
aria-label="Message input"
|
||||
@@ -179,10 +180,19 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
data-placeholder="Ask Qwen Code …"
|
||||
// Use a data flag so CSS can show placeholder even if the browser
|
||||
// inserts an invisible <br> into contentEditable (so :empty no longer matches)
|
||||
data-empty={inputText.trim().length === 0 ? 'true' : 'false'}
|
||||
data-empty={
|
||||
inputText.replace(/\u200B/g, '').trim().length === 0
|
||||
? 'true'
|
||||
: 'false'
|
||||
}
|
||||
onInput={(e) => {
|
||||
if (composerDisabled) {
|
||||
return;
|
||||
}
|
||||
const target = e.target as HTMLDivElement;
|
||||
onInputChange(target.textContent || '');
|
||||
// Filter out zero-width space that we use to maintain height
|
||||
const text = target.textContent?.replace(/\u200B/g, '') || '';
|
||||
onInputChange(text);
|
||||
}}
|
||||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
@@ -280,7 +290,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-send-compact [&>svg]:w-5 [&>svg]:h-5"
|
||||
disabled={!inputText.trim()}
|
||||
disabled={composerDisabled || !inputText.trim()}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type React from 'react';
|
||||
import { generateIconUrl } from '../../utils/resourceUrl.js';
|
||||
|
||||
interface OnboardingPageProps {
|
||||
onLogin: () => void;
|
||||
}
|
||||
|
||||
export const OnboardingPage: React.FC<OnboardingPageProps> = ({ onLogin }) => {
|
||||
const iconUri = generateIconUrl('icon.png');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-5 md:p-10">
|
||||
<div className="flex flex-col items-center gap-8 w-full max-w-md">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={iconUri}
|
||||
alt="Qwen Code Logo"
|
||||
className="w-[80px] h-[80px] object-contain"
|
||||
/>
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-[#4f46e5] rounded-full flex items-center justify-center">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="white">
|
||||
<path
|
||||
d="M2.5 1.5L9.5 8.5M9.5 1.5L2.5 8.5"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-app-primary-foreground mb-2">
|
||||
Welcome to Qwen Code
|
||||
</h1>
|
||||
<p className="text-app-secondary-foreground max-w-sm">
|
||||
Qwen Code helps you understand, navigate, and transform your
|
||||
codebase with AI assistance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-5 w-full">
|
||||
<div className="bg-app-secondary-background rounded-xl p-5 border border-app-primary-border-color shadow-sm">
|
||||
<h2 className="font-semibold text-app-primary-foreground mb-3">Get Started</h2>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-[#4f46e5] flex-shrink-0"></div>
|
||||
<span className="text-sm text-app-secondary-foreground">Understand complex codebases faster</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-[#4f46e5] flex-shrink-0"></div>
|
||||
<span className="text-sm text-app-secondary-foreground">Navigate with AI-powered suggestions</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-[#4f46e5] flex-shrink-0"></div>
|
||||
<span className="text-sm text-app-secondary-foreground">Transform code with confidence</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div> */}
|
||||
|
||||
<button
|
||||
onClick={onLogin}
|
||||
className="w-full px-4 py-3 bg-[#4f46e5] text-white font-medium rounded-lg shadow-sm"
|
||||
>
|
||||
Log in to Qwen Code
|
||||
</button>
|
||||
|
||||
{/* <div className="text-center">
|
||||
<p className="text-xs text-app-secondary-foreground">
|
||||
By logging in, you agree to the Terms of Service and Privacy
|
||||
Policy.
|
||||
</p>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,7 @@ interface UseMessageSubmitProps {
|
||||
setInputText: (text: string) => void;
|
||||
inputFieldRef: React.RefObject<HTMLDivElement>;
|
||||
isStreaming: boolean;
|
||||
isWaitingForResponse: boolean;
|
||||
// When true, do NOT auto-attach the active editor file/selection to context
|
||||
skipAutoActiveContext?: boolean;
|
||||
|
||||
@@ -40,6 +41,7 @@ export const useMessageSubmit = ({
|
||||
setInputText,
|
||||
inputFieldRef,
|
||||
isStreaming,
|
||||
isWaitingForResponse,
|
||||
skipAutoActiveContext = false,
|
||||
fileContext,
|
||||
messageHandling,
|
||||
@@ -48,7 +50,7 @@ export const useMessageSubmit = ({
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!inputText.trim() || isStreaming) {
|
||||
if (!inputText.trim() || isStreaming || isWaitingForResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +58,10 @@ export const useMessageSubmit = ({
|
||||
if (inputText.trim() === '/login') {
|
||||
setInputText('');
|
||||
if (inputFieldRef.current) {
|
||||
inputFieldRef.current.textContent = '';
|
||||
// Use a zero-width space to maintain the height of the contentEditable element
|
||||
inputFieldRef.current.textContent = '\u200B';
|
||||
// Set the data-empty attribute to show the placeholder
|
||||
inputFieldRef.current.setAttribute('data-empty', 'true');
|
||||
}
|
||||
vscode.postMessage({
|
||||
type: 'login',
|
||||
@@ -142,7 +147,10 @@ export const useMessageSubmit = ({
|
||||
|
||||
setInputText('');
|
||||
if (inputFieldRef.current) {
|
||||
inputFieldRef.current.textContent = '';
|
||||
// Use a zero-width space to maintain the height of the contentEditable element
|
||||
inputFieldRef.current.textContent = '\u200B';
|
||||
// Set the data-empty attribute to show the placeholder
|
||||
inputFieldRef.current.setAttribute('data-empty', 'true');
|
||||
}
|
||||
fileContext.clearFileReferences();
|
||||
},
|
||||
@@ -154,6 +162,7 @@ export const useMessageSubmit = ({
|
||||
vscode,
|
||||
fileContext,
|
||||
skipAutoActiveContext,
|
||||
isWaitingForResponse,
|
||||
messageHandling,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -109,6 +109,8 @@ interface UseWebViewMessagesProps {
|
||||
setInputText: (text: string) => void;
|
||||
// Edit mode setter (maps ACP modes to UI modes)
|
||||
setEditMode?: (mode: ApprovalModeValue) => void;
|
||||
// Authentication state setter
|
||||
setIsAuthenticated?: (authenticated: boolean | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,6 +128,7 @@ export const useWebViewMessages = ({
|
||||
inputFieldRef,
|
||||
setInputText,
|
||||
setEditMode,
|
||||
setIsAuthenticated,
|
||||
}: UseWebViewMessagesProps) => {
|
||||
// VS Code API for posting messages back to the extension host
|
||||
const vscode = useVSCode();
|
||||
@@ -141,6 +144,7 @@ export const useWebViewMessages = ({
|
||||
clearToolCalls,
|
||||
setPlanEntries,
|
||||
handlePermissionRequest,
|
||||
setIsAuthenticated,
|
||||
});
|
||||
|
||||
// Track last "Updated Plan" snapshot toolcall to support merge/dedupe
|
||||
@@ -185,6 +189,7 @@ export const useWebViewMessages = ({
|
||||
clearToolCalls,
|
||||
setPlanEntries,
|
||||
handlePermissionRequest,
|
||||
setIsAuthenticated,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -216,6 +221,7 @@ export const useWebViewMessages = ({
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loginSuccess': {
|
||||
// Clear loading state and show a short assistant notice
|
||||
handlers.messageHandling.clearWaitingForResponse();
|
||||
@@ -224,12 +230,16 @@ export const useWebViewMessages = ({
|
||||
content: 'Successfully logged in. You can continue chatting.',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// Set authentication state to true
|
||||
handlers.setIsAuthenticated?.(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'agentConnected': {
|
||||
// Agent connected successfully; clear any pending spinner
|
||||
handlers.messageHandling.clearWaitingForResponse();
|
||||
// Set authentication state to true
|
||||
handlers.setIsAuthenticated?.(true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -245,6 +255,8 @@ export const useWebViewMessages = ({
|
||||
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(),
|
||||
});
|
||||
// Set authentication state to false
|
||||
handlers.setIsAuthenticated?.(false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -259,6 +271,20 @@ export const useWebViewMessages = ({
|
||||
content: errorMsg,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// Set authentication state to false
|
||||
handlers.setIsAuthenticated?.(false);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'authState': {
|
||||
const state = (
|
||||
message?.data as { authenticated?: boolean | null } | undefined
|
||||
)?.authenticated;
|
||||
if (typeof state === 'boolean') {
|
||||
handlers.setIsAuthenticated?.(state);
|
||||
} else {
|
||||
handlers.setIsAuthenticated?.(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -303,6 +329,7 @@ export const useWebViewMessages = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('[useWebViewMessages1111]__________ other message:', msg);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -336,7 +363,7 @@ export const useWebViewMessages = ({
|
||||
const reason = (
|
||||
(message.data as { reason?: string } | undefined)?.reason || ''
|
||||
).toLowerCase();
|
||||
if (reason === 'user_cancelled') {
|
||||
if (reason === 'user_cancelled' || reason === 'cancelled') {
|
||||
activeExecToolCallsRef.current.clear();
|
||||
handlers.messageHandling.clearWaitingForResponse();
|
||||
break;
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
.composer-form:focus-within {
|
||||
/* match existing highlight behavior */
|
||||
border-color: var(--app-input-highlight);
|
||||
box-shadow: 0 1px 2px
|
||||
color-mix(in srgb, var(--app-input-highlight), transparent 80%);
|
||||
box-shadow: 0 1px 2px color-mix(in srgb, var(--app-input-highlight), transparent 80%);
|
||||
}
|
||||
|
||||
/* Composer: input editable area */
|
||||
@@ -67,7 +66,7 @@
|
||||
The data attribute is needed because some browsers insert a <br> in
|
||||
contentEditable, which breaks :empty matching. */
|
||||
.composer-input:empty:before,
|
||||
.composer-input[data-empty='true']::before {
|
||||
.composer-input[data-empty="true"]::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--app-input-placeholder-foreground);
|
||||
pointer-events: none;
|
||||
@@ -81,7 +80,7 @@
|
||||
outline: none;
|
||||
}
|
||||
.composer-input:disabled,
|
||||
.composer-input[contenteditable='false'] {
|
||||
.composer-input[contenteditable="false"] {
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,22 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Single-item AI sequence (both a start and an end): hide the connector entirely */
|
||||
.qwen-message.message-item:not(.user-message-container):is(
|
||||
:first-child,
|
||||
.user-message-container
|
||||
+ .qwen-message.message-item:not(.user-message-container),
|
||||
.chat-messages
|
||||
> :not(.qwen-message.message-item)
|
||||
+ .qwen-message.message-item:not(.user-message-container)
|
||||
):is(
|
||||
:has(+ .user-message-container),
|
||||
:has(+ :not(.qwen-message.message-item)),
|
||||
:last-child
|
||||
)::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Handle the start of each AI message sequence - includes the first AI message in the entire message list and new AI messages interrupted by user messages */
|
||||
.qwen-message.message-item:not(.user-message-container):first-child::after,
|
||||
.user-message-container + .qwen-message.message-item:not(.user-message-container)::after,
|
||||
|
||||
Reference in New Issue
Block a user