mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
refactor(vscode-ide-companion): 重构 WebViewProvider 初始化逻辑
- 抽离初始化代理连接逻辑到单独的方法中 - 优化面板恢复时的代理连接流程 - 移除 EmptyState 组件中的信息横幅 - 在 App 组件中添加可关闭的信息横幅 - 调整输入表单样式,移除冗余样式
This commit is contained in:
@@ -196,69 +196,7 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
// Initialize agent connection only once
|
// Initialize agent connection only once
|
||||||
if (!this.agentInitialized) {
|
if (!this.agentInitialized) {
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
await this.initializeAgentConnection();
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Starting initialization, workingDir:',
|
|
||||||
workingDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
const config = vscode.workspace.getConfiguration('qwenCode');
|
|
||||||
const qwenEnabled = config.get<boolean>('qwen.enabled', true);
|
|
||||||
|
|
||||||
if (qwenEnabled) {
|
|
||||||
// Check if CLI is installed before attempting to connect
|
|
||||||
const cliDetection = await CliDetector.detectQwenCli();
|
|
||||||
|
|
||||||
if (!cliDetection.isInstalled) {
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] CLI detection error:',
|
|
||||||
cliDetection.error,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show VSCode notification with installation option
|
|
||||||
await this.promptCliInstallation();
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
console.log('[WebViewProvider] Connecting to agent...');
|
|
||||||
const authInfo = await this.authStateManager.getAuthInfo();
|
|
||||||
console.log('[WebViewProvider] Auth cache status:', authInfo);
|
|
||||||
|
|
||||||
await this.agentManager.connect(workingDir, this.authStateManager);
|
|
||||||
console.log('[WebViewProvider] Agent connected successfully');
|
|
||||||
this.agentInitialized = true;
|
|
||||||
|
|
||||||
// Load messages from the current Qwen session
|
|
||||||
await this.loadCurrentSessionMessages();
|
|
||||||
} 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[WebViewProvider] Qwen agent is disabled in settings');
|
|
||||||
// Fallback to ConversationStore
|
|
||||||
await this.initializeEmptyConversation();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Agent already initialized, reusing existing connection',
|
'[WebViewProvider] Agent already initialized, reusing existing connection',
|
||||||
@@ -268,6 +206,76 @@ export class WebViewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize agent connection and session
|
||||||
|
* Can be called from show() or restorePanel()
|
||||||
|
*/
|
||||||
|
private async initializeAgentConnection(): Promise<void> {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Starting initialization, workingDir:',
|
||||||
|
workingDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||||
|
const qwenEnabled = config.get<boolean>('qwen.enabled', true);
|
||||||
|
|
||||||
|
if (qwenEnabled) {
|
||||||
|
// Check if CLI is installed before attempting to connect
|
||||||
|
const cliDetection = await CliDetector.detectQwenCli();
|
||||||
|
|
||||||
|
if (!cliDetection.isInstalled) {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] CLI detection error:',
|
||||||
|
cliDetection.error,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show VSCode notification with installation option
|
||||||
|
await this.promptCliInstallation();
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
console.log('[WebViewProvider] Connecting to agent...');
|
||||||
|
const authInfo = await this.authStateManager.getAuthInfo();
|
||||||
|
console.log('[WebViewProvider] Auth cache status:', authInfo);
|
||||||
|
|
||||||
|
await this.agentManager.connect(workingDir, this.authStateManager);
|
||||||
|
console.log('[WebViewProvider] Agent connected successfully');
|
||||||
|
this.agentInitialized = true;
|
||||||
|
|
||||||
|
// Load messages from the current Qwen session
|
||||||
|
await this.loadCurrentSessionMessages();
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[WebViewProvider] Qwen agent is disabled in settings');
|
||||||
|
// Fallback to ConversationStore
|
||||||
|
await this.initializeEmptyConversation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async checkCliInstallation(): Promise<void> {
|
private async checkCliInstallation(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await CliDetector.detectQwenCli();
|
const result = await CliDetector.detectQwenCli();
|
||||||
@@ -829,9 +837,8 @@ export class WebViewProvider {
|
|||||||
vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview.js'),
|
vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview.js'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const iconUri = this.panel!.webview.asWebviewUri(
|
// Convert extension URI for webview access - this allows frontend to construct resource paths
|
||||||
vscode.Uri.joinPath(this.extensionUri, 'assets', 'icon.png'),
|
const extensionUri = this.panel!.webview.asWebviewUri(this.extensionUri);
|
||||||
);
|
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -841,9 +848,8 @@ export class WebViewProvider {
|
|||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${this.panel!.webview.cspSource}; script-src ${this.panel!.webview.cspSource}; style-src ${this.panel!.webview.cspSource} 'unsafe-inline';">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${this.panel!.webview.cspSource}; script-src ${this.panel!.webview.cspSource}; style-src ${this.panel!.webview.cspSource} 'unsafe-inline';">
|
||||||
<title>Qwen Code Chat</title>
|
<title>Qwen Code Chat</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-extension-uri="${extensionUri}">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script>window.ICON_URI = "${iconUri}";</script>
|
|
||||||
<script src="${scriptUri}"></script>
|
<script src="${scriptUri}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
@@ -951,6 +957,30 @@ export class WebViewProvider {
|
|||||||
this.capturePanelTab();
|
this.capturePanelTab();
|
||||||
|
|
||||||
console.log('[WebViewProvider] Panel restored successfully');
|
console.log('[WebViewProvider] Panel restored successfully');
|
||||||
|
|
||||||
|
// Initialize agent connection if not already done
|
||||||
|
if (!this.agentInitialized) {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Initializing agent connection after restore...',
|
||||||
|
);
|
||||||
|
this.initializeAgentConnection().catch((error) => {
|
||||||
|
console.error(
|
||||||
|
'[WebViewProvider] Failed to initialize agent after restore:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Agent already initialized, loading current session...',
|
||||||
|
);
|
||||||
|
// Reload current session messages
|
||||||
|
this.loadCurrentSessionMessages().catch((error) => {
|
||||||
|
console.error(
|
||||||
|
'[WebViewProvider] Failed to load session messages after restore:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -594,13 +594,83 @@ button {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
Info Banner (at bottom)
|
||||||
|
=========================== */
|
||||||
|
.info-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: var(--app-input-secondary-background);
|
||||||
|
border-top: 1px solid var(--app-primary-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: var(--app-primary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--app-primary-foreground);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-link {
|
||||||
|
color: var(--app-claude-orange);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--corner-radius-small);
|
||||||
|
color: var(--app-primary-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-close:hover {
|
||||||
|
background-color: var(--app-ghost-button-hover-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-close svg {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===========================
|
/* ===========================
|
||||||
Claude Code Style Input Form (.Me > .u)
|
Claude Code Style Input Form (.Me > .u)
|
||||||
=========================== */
|
=========================== */
|
||||||
/* Outer container (.Me) */
|
/* Outer container (.Me) */
|
||||||
.input-form-container {
|
.input-form-container {
|
||||||
border-top: 1px solid var(--app-primary-border-color);
|
|
||||||
background-color: var(--app-primary-background);
|
background-color: var(--app-primary-background);
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inner wrapper */
|
/* Inner wrapper */
|
||||||
@@ -608,13 +678,18 @@ button {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form (.u) */
|
/* Form (.u) - The actual input form with border and shadow */
|
||||||
.input-form {
|
.input-form {
|
||||||
|
background: var(--app-input-background);
|
||||||
|
border: 1px solid var(--app-input-border);
|
||||||
|
border-radius: var(--corner-radius-large);
|
||||||
|
color: var(--app-input-foreground);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
max-width: 680px;
|
||||||
padding: 0;
|
margin: 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Banner/Warning area (.Wr) */
|
/* Banner/Warning area (.Wr) */
|
||||||
@@ -624,7 +699,7 @@ button {
|
|||||||
|
|
||||||
/* Input wrapper (.fo) */
|
/* Input wrapper (.fo) */
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
padding: 16px;
|
/* padding: 16px; */
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,10 +709,10 @@ button {
|
|||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background-color: var(--app-input-background);
|
background-color: transparent;
|
||||||
color: var(--app-input-foreground);
|
color: var(--app-input-foreground);
|
||||||
border: 1px solid var(--app-input-border);
|
border: none;
|
||||||
border-radius: var(--corner-radius-medium);
|
border-radius: 0;
|
||||||
font-size: var(--vscode-chat-font-size, 13px);
|
font-size: var(--vscode-chat-font-size, 13px);
|
||||||
font-family: var(--vscode-chat-font-family);
|
font-family: var(--vscode-chat-font-family);
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -646,11 +721,10 @@ button {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-editable:focus {
|
.input-field-editable:focus {
|
||||||
border-color: var(--app-input-active-border);
|
/* No border change needed since we don't have a border */
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-editable:empty:before {
|
.input-field-editable:empty:before {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export const App: React.FC = () => {
|
|||||||
);
|
);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputFieldRef = useRef<HTMLDivElement>(null);
|
const inputFieldRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showBanner, setShowBanner] = useState(true);
|
||||||
|
|
||||||
const handlePermissionRequest = React.useCallback(
|
const handlePermissionRequest = React.useCallback(
|
||||||
(request: {
|
(request: {
|
||||||
@@ -464,6 +465,55 @@ export const App: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Info Banner */}
|
||||||
|
{showBanner && (
|
||||||
|
<div className="info-banner">
|
||||||
|
<div className="banner-content">
|
||||||
|
<svg
|
||||||
|
className="banner-icon"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M5.14648 7.14648C5.34175 6.95122 5.65825 6.95122 5.85352 7.14648L8.35352 9.64648C8.44728 9.74025 8.5 9.86739 8.5 10C8.5 10.0994 8.47037 10.1958 8.41602 10.2773L8.35352 10.3535L5.85352 12.8535C5.65825 13.0488 5.34175 13.0488 5.14648 12.8535C4.95122 12.6583 4.95122 12.3417 5.14648 12.1465L7.29297 10L5.14648 7.85352C4.95122 7.65825 4.95122 7.34175 5.14648 7.14648Z"></path>
|
||||||
|
<path d="M14.5 12C14.7761 12 15 12.2239 15 12.5C15 12.7761 14.7761 13 14.5 13H9.5C9.22386 13 9 12.7761 9 12.5C9 12.2239 9.22386 12 9.5 12H14.5Z"></path>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M16.5 4C17.3284 4 18 4.67157 18 5.5V14.5C18 15.3284 17.3284 16 16.5 16H3.5C2.67157 16 2 15.3284 2 14.5V5.5C2 4.67157 2.67157 4 3.5 4H16.5ZM3.5 5C3.22386 5 3 5.22386 3 5.5V14.5C3 14.7761 3.22386 15 3.5 15H16.5C16.7761 15 17 14.7761 17 14.5V5.5C17 5.22386 16.7761 5 16.5 5H3.5Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<label>
|
||||||
|
Prefer the Terminal experience?{' '}
|
||||||
|
<a href="#" className="banner-link">
|
||||||
|
Switch back in Settings.
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="banner-close"
|
||||||
|
aria-label="Close banner"
|
||||||
|
onClick={() => setShowBanner(false)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1 1L13 13M1 13L13 1"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="input-form-container">
|
<div className="input-form-container">
|
||||||
<div className="input-form-wrapper">
|
<div className="input-form-wrapper">
|
||||||
<form className="input-form" onSubmit={handleSubmit}>
|
<form className="input-form" onSubmit={handleSubmit}>
|
||||||
|
|||||||
@@ -46,74 +46,3 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Banner Styles */
|
|
||||||
.empty-state-banner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: var(--app-input-secondary-background);
|
|
||||||
border: 1px solid var(--app-primary-border-color);
|
|
||||||
border-radius: var(--corner-radius-medium);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
fill: var(--app-primary-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-content label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--app-primary-foreground);
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-link {
|
|
||||||
color: var(--app-claude-orange);
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-close {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--corner-radius-small);
|
|
||||||
color: var(--app-primary-foreground);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-close:hover {
|
|
||||||
background-color: var(--app-ghost-button-hover-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-close svg {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,17 +6,11 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import './EmptyState.css';
|
import './EmptyState.css';
|
||||||
|
import { generateIconUrl } from '../utils/resourceUrl.js';
|
||||||
// Extend Window interface to include ICON_URI
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
ICON_URI?: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmptyState: React.FC = () => {
|
export const EmptyState: React.FC = () => {
|
||||||
// Get icon URI from window, fallback to empty string if not available
|
// Generate icon URL using the utility function
|
||||||
const iconUri = window.ICON_URI || '';
|
const iconUri = generateIconUrl('icon.png');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
@@ -37,49 +31,6 @@ export const EmptyState: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info Banner */}
|
|
||||||
<div className="empty-state-banner">
|
|
||||||
<div className="banner-content">
|
|
||||||
<svg
|
|
||||||
className="banner-icon"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M5.14648 7.14648C5.34175 6.95122 5.65825 6.95122 5.85352 7.14648L8.35352 9.64648C8.44728 9.74025 8.5 9.86739 8.5 10C8.5 10.0994 8.47037 10.1958 8.41602 10.2773L8.35352 10.3535L5.85352 12.8535C5.65825 13.0488 5.34175 13.0488 5.14648 12.8535C4.95122 12.6583 4.95122 12.3417 5.14648 12.1465L7.29297 10L5.14648 7.85352C4.95122 7.65825 4.95122 7.34175 5.14648 7.14648Z"></path>
|
|
||||||
<path d="M14.5 12C14.7761 12 15 12.2239 15 12.5C15 12.7761 14.7761 13 14.5 13H9.5C9.22386 13 9 12.7761 9 12.5C9 12.2239 9.22386 12 9.5 12H14.5Z"></path>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M16.5 4C17.3284 4 18 4.67157 18 5.5V14.5C18 15.3284 17.3284 16 16.5 16H3.5C2.67157 16 2 15.3284 2 14.5V5.5C2 4.67157 2.67157 4 3.5 4H16.5ZM3.5 5C3.22386 5 3 5.22386 3 5.5V14.5C3 14.7761 3.22386 15 3.5 15H16.5C16.7761 15 17 14.7761 17 14.5V5.5C17 5.22386 16.7761 5 16.5 5H3.5Z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<label>
|
|
||||||
Prefer the Terminal experience?{' '}
|
|
||||||
<a href="#" className="banner-link">
|
|
||||||
Switch back in Settings.
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button className="banner-close" aria-label="Close banner">
|
|
||||||
<svg
|
|
||||||
width="10"
|
|
||||||
height="10"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1 1L13 13M1 13L13 1"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Extend Window interface to include __EXTENSION_URI__
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__EXTENSION_URI__?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the extension URI from the body data attribute or window global
|
||||||
|
* @returns Extension URI or undefined if not found
|
||||||
|
*/
|
||||||
|
function getExtensionUri(): string | undefined {
|
||||||
|
// First try to get from window (for backwards compatibility)
|
||||||
|
if (window.__EXTENSION_URI__) {
|
||||||
|
return window.__EXTENSION_URI__;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then try to get from body data attribute (CSP-compliant method)
|
||||||
|
const bodyUri = document.body?.getAttribute('data-extension-uri');
|
||||||
|
if (bodyUri) {
|
||||||
|
// Cache it in window for future use
|
||||||
|
window.__EXTENSION_URI__ = bodyUri;
|
||||||
|
return bodyUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a resource URL for webview access
|
||||||
|
* Similar to the pattern used in other VSCode extensions
|
||||||
|
*
|
||||||
|
* @param relativePath - Relative path from extension root (e.g., 'assets/icon.png')
|
||||||
|
* @returns Full webview-accessible URL
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <img src={generateResourceUrl('assets/icon.png')} />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function generateResourceUrl(relativePath: string): string {
|
||||||
|
const extensionUri = getExtensionUri();
|
||||||
|
|
||||||
|
if (!extensionUri) {
|
||||||
|
console.warn('[resourceUrl] Extension URI not found in window or body');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading slash if present
|
||||||
|
const cleanPath = relativePath.startsWith('/')
|
||||||
|
? relativePath.slice(1)
|
||||||
|
: relativePath;
|
||||||
|
|
||||||
|
// Ensure extension URI has trailing slash
|
||||||
|
const baseUri = extensionUri.endsWith('/')
|
||||||
|
? extensionUri
|
||||||
|
: `${extensionUri}/`;
|
||||||
|
|
||||||
|
return `${baseUri}${cleanPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for generating icon URLs
|
||||||
|
* @param iconPath - Path relative to assets directory
|
||||||
|
*/
|
||||||
|
export function generateIconUrl(iconPath: string): string {
|
||||||
|
return generateResourceUrl(`assets/${iconPath}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user