mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
wip(vscode-ide-companion): 实现 quick win 功能
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import {
|
||||
type ToolCall as PermissionToolCall,
|
||||
} from './components/PermissionRequest.js';
|
||||
import { ToolCall, type ToolCallData } from './components/ToolCall.js';
|
||||
import { EmptyState } from './components/EmptyState.js';
|
||||
|
||||
interface ToolCallUpdate {
|
||||
type: 'tool_call' | 'tool_call_update';
|
||||
@@ -54,6 +55,7 @@ export const App: React.FC = () => {
|
||||
const [qwenSessions, setQwenSessions] = useState<
|
||||
Array<Record<string, unknown>>
|
||||
>([]);
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||
const [showSessionSelector, setShowSessionSelector] = useState(false);
|
||||
const [permissionRequest, setPermissionRequest] = useState<{
|
||||
options: PermissionOption[];
|
||||
@@ -63,6 +65,7 @@ export const App: React.FC = () => {
|
||||
new Map(),
|
||||
);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputFieldRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handlePermissionRequest = React.useCallback(
|
||||
(request: {
|
||||
@@ -201,12 +204,26 @@ export const App: React.FC = () => {
|
||||
handleToolCallUpdate(message.data);
|
||||
break;
|
||||
|
||||
case 'qwenSessionList':
|
||||
setQwenSessions(message.data.sessions || []);
|
||||
case 'qwenSessionList': {
|
||||
const sessions = message.data.sessions || [];
|
||||
setQwenSessions(sessions);
|
||||
// If no current session is selected and there are sessions, select the first one
|
||||
if (!currentSessionId && sessions.length > 0) {
|
||||
const firstSessionId =
|
||||
(sessions[0].id as string) || (sessions[0].sessionId as string);
|
||||
if (firstSessionId) {
|
||||
setCurrentSessionId(firstSessionId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'qwenSessionSwitched':
|
||||
setShowSessionSelector(false);
|
||||
// Update current session ID
|
||||
if (message.data.sessionId) {
|
||||
setCurrentSessionId(message.data.sessionId as string);
|
||||
}
|
||||
// Load messages from the session
|
||||
if (message.data.messages) {
|
||||
setMessages(message.data.messages);
|
||||
@@ -230,13 +247,23 @@ export const App: React.FC = () => {
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [currentStreamContent, handlePermissionRequest, handleToolCallUpdate]);
|
||||
}, [
|
||||
currentStreamContent,
|
||||
currentSessionId,
|
||||
handlePermissionRequest,
|
||||
handleToolCallUpdate,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-scroll to bottom when messages change
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages, currentStreamContent]);
|
||||
|
||||
// Load sessions on component mount
|
||||
useEffect(() => {
|
||||
vscode.postMessage({ type: 'getQwenSessions', data: {} });
|
||||
}, [vscode]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -251,7 +278,11 @@ export const App: React.FC = () => {
|
||||
data: { text: inputText },
|
||||
});
|
||||
|
||||
// Clear input field
|
||||
setInputText('');
|
||||
if (inputFieldRef.current) {
|
||||
inputFieldRef.current.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadQwenSessions = () => {
|
||||
@@ -262,25 +293,39 @@ export const App: React.FC = () => {
|
||||
const handleNewQwenSession = () => {
|
||||
vscode.postMessage({ type: 'newQwenSession', data: {} });
|
||||
setShowSessionSelector(false);
|
||||
setCurrentSessionId(null);
|
||||
// Clear messages in UI
|
||||
setMessages([]);
|
||||
setCurrentStreamContent('');
|
||||
};
|
||||
|
||||
const handleSwitchSession = (sessionId: string) => {
|
||||
if (sessionId === currentSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: 'switchQwenSession',
|
||||
data: { sessionId },
|
||||
});
|
||||
setCurrentSessionId(sessionId);
|
||||
setShowSessionSelector(false);
|
||||
};
|
||||
|
||||
// Check if there are any messages or active content
|
||||
const hasContent =
|
||||
messages.length > 0 ||
|
||||
isStreaming ||
|
||||
toolCalls.size > 0 ||
|
||||
permissionRequest !== null;
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
{showSessionSelector && (
|
||||
<div className="session-selector-overlay">
|
||||
<div className="session-selector">
|
||||
<div className="session-selector-header">
|
||||
<h3>Qwen Sessions</h3>
|
||||
<h3>Past Conversations</h3>
|
||||
<button onClick={() => setShowSessionSelector(false)}>✕</button>
|
||||
</div>
|
||||
<div className="session-selector-actions">
|
||||
@@ -338,62 +383,196 @@ export const App: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="chat-header">
|
||||
<button className="session-button" onClick={handleLoadQwenSessions}>
|
||||
📋 Sessions
|
||||
<button
|
||||
className="header-conversations-button"
|
||||
onClick={handleLoadQwenSessions}
|
||||
title="Past conversations"
|
||||
>
|
||||
<span className="button-content">
|
||||
<span className="button-text">Past Conversations</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
className="dropdown-icon"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<div className="header-spacer"></div>
|
||||
<button
|
||||
className="new-session-header-button"
|
||||
onClick={handleNewQwenSession}
|
||||
title="New Session"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
className="icon-svg"
|
||||
>
|
||||
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="messages-container">
|
||||
{messages.map((msg, index) => (
|
||||
<div key={index} className={`message ${msg.role}`}>
|
||||
<div className="message-content">{msg.content}</div>
|
||||
<div className="message-timestamp">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!hasContent ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<>
|
||||
{messages.map((msg, index) => (
|
||||
<div key={index} className={`message ${msg.role}`}>
|
||||
<div className="message-content">{msg.content}</div>
|
||||
<div className="message-timestamp">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Tool Calls */}
|
||||
{Array.from(toolCalls.values()).map((toolCall) => (
|
||||
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
|
||||
))}
|
||||
{/* Tool Calls */}
|
||||
{Array.from(toolCalls.values()).map((toolCall) => (
|
||||
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
|
||||
))}
|
||||
|
||||
{/* Permission Request */}
|
||||
{permissionRequest && (
|
||||
<PermissionRequest
|
||||
options={permissionRequest.options}
|
||||
toolCall={permissionRequest.toolCall}
|
||||
onResponse={handlePermissionResponse}
|
||||
/>
|
||||
{/* Permission Request */}
|
||||
{permissionRequest && (
|
||||
<PermissionRequest
|
||||
options={permissionRequest.options}
|
||||
toolCall={permissionRequest.toolCall}
|
||||
onResponse={handlePermissionResponse}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isStreaming && currentStreamContent && (
|
||||
<div className="message assistant streaming">
|
||||
<div className="message-content">{currentStreamContent}</div>
|
||||
<div className="streaming-indicator">●</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isStreaming && currentStreamContent && (
|
||||
<div className="message assistant streaming">
|
||||
<div className="message-content">{currentStreamContent}</div>
|
||||
<div className="streaming-indicator">●</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<form className="input-form" onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder="Type your message..."
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText((e.target as HTMLInputElement).value)}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="send-button"
|
||||
disabled={isStreaming || !inputText.trim()}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
<div className="input-form-container">
|
||||
<div className="input-form-wrapper">
|
||||
<form className="input-form" onSubmit={handleSubmit}>
|
||||
<div className="input-banner"></div>
|
||||
<div className="input-wrapper">
|
||||
<div
|
||||
ref={inputFieldRef}
|
||||
contentEditable="plaintext-only"
|
||||
className="input-field-editable"
|
||||
role="textbox"
|
||||
aria-label="Message input"
|
||||
aria-multiline="true"
|
||||
data-placeholder="Ask Claude to edit…"
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
setInputText(target.textContent || '');
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}
|
||||
}}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
</div>
|
||||
<div className="input-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button edit-mode-button"
|
||||
title="Claude will ask before each edit. Click to switch modes."
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Ask before edits</span>
|
||||
</button>
|
||||
<div className="action-divider"></div>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon-button thinking-button"
|
||||
title="Thinking off"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915 1.30834 14.8848 4.31624 14.8848 8C14.8848 11.8025 11.8025 14.8848 8 14.8848C4.19752 14.8848 1.11523 11.8025 1.11523 8C1.11523 7.67691 1.37711 7.41504 1.7002 7.41504C2.02319 7.41514 2.28516 7.67698 2.28516 8C2.28516 11.1563 4.84369 13.7148 8 13.7148C11.1563 13.7148 13.7148 11.1563 13.7148 8C13.7148 4.94263 11.3141 2.4464 8.29492 2.29297V2.29199L7.99609 2.28516H7.9873V2.28418L7.89648 2.27539L7.88281 2.27441V2.27344C7.61596 2.21897 7.41513 1.98293 7.41504 1.7002C7.41504 1.37711 7.67691 1.11523 8 1.11523H8.00293ZM8 3.81543C8.32309 3.81543 8.58496 4.0773 8.58496 4.40039V7.6377L10.9619 8.82715C11.2505 8.97169 11.3678 9.32256 11.2236 9.61133C11.0972 9.86425 10.8117 9.98544 10.5488 9.91504L10.5352 9.91211V9.91016L10.4502 9.87891L10.4385 9.87402V9.87305L7.73828 8.52344C7.54007 8.42433 7.41504 8.22155 7.41504 8V4.40039C7.41504 4.0773 7.67691 3.81543 8 3.81543ZM2.44336 5.12695C2.77573 5.19517 3.02597 5.48929 3.02637 5.8418C3.02637 6.19456 2.7761 6.49022 2.44336 6.55859L2.2959 6.57324C1.89241 6.57324 1.56543 6.24529 1.56543 5.8418C1.56588 5.43853 1.89284 5.1123 2.2959 5.1123L2.44336 5.12695ZM3.46094 2.72949C3.86418 2.72984 4.19017 3.05712 4.19043 3.45996V3.46094C4.19009 3.86393 3.86392 4.19008 3.46094 4.19043H3.45996C3.05712 4.19017 2.72983 3.86419 2.72949 3.46094V3.45996C2.72976 3.05686 3.05686 2.72976 3.45996 2.72949H3.46094ZM5.98926 1.58008C6.32235 1.64818 6.57324 1.94276 6.57324 2.2959L6.55859 2.44336C6.49022 2.7761 6.19456 3.02637 5.8418 3.02637C5.43884 3.02591 5.11251 2.69895 5.1123 2.2959L5.12695 2.14844C5.19504 1.81591 5.48906 1.56583 5.8418 1.56543L5.98926 1.58008Z"
|
||||
strokeWidth="0.27"
|
||||
style={{
|
||||
stroke: 'var(--app-secondary-foreground)',
|
||||
fill: 'var(--app-secondary-foreground)',
|
||||
}}
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon-button command-button"
|
||||
title="Show command menu (/)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12.528 3.047a.75.75 0 0 1 .449.961L8.433 16.504a.75.75 0 1 1-1.41-.512l4.544-12.496a.75.75 0 0 1 .961-.449Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="send-button-icon"
|
||||
disabled={isStreaming || !inputText.trim()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
229
packages/vscode-ide-companion/src/webview/ClaudeCodeStyles.css
Normal file
229
packages/vscode-ide-companion/src/webview/ClaudeCodeStyles.css
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Styles extracted from Claude Code extension (v2.0.43)
|
||||
* Path: /Users/jinjing/Downloads/Anthropic.claude-code-2.0.43/extension/webview/index.css
|
||||
*/
|
||||
|
||||
/* ===========================
|
||||
Header Styles (from Claude Code .he)
|
||||
=========================== */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--app-primary-border-color);
|
||||
padding: 6px 10px;
|
||||
gap: 4px;
|
||||
background-color: var(--app-header-background);
|
||||
justify-content: flex-start;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Session Selector Button (from Claude Code .E)
|
||||
=========================== */
|
||||
.session-selector-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
}
|
||||
|
||||
.session-selector-button:focus,
|
||||
.session-selector-button:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
}
|
||||
|
||||
/* Session Selector Button Internal Elements */
|
||||
.session-selector-button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.session-selector-button-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.session-selector-button-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-selector-button-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Icon Button (from Claude Code .j)
|
||||
=========================== */
|
||||
.icon-button {
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.icon-button:focus,
|
||||
.icon-button:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Session Selector Modal (from Claude Code .Wt)
|
||||
=========================== */
|
||||
.session-selector-modal {
|
||||
position: fixed;
|
||||
background: var(--app-menu-background);
|
||||
border: 1px solid var(--app-menu-border);
|
||||
border-radius: var(--corner-radius-small);
|
||||
width: min(400px, calc(100vw - 32px));
|
||||
max-height: min(500px, 50vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
outline: none;
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
}
|
||||
|
||||
/* Modal Content Area (from Claude Code .It) */
|
||||
.session-selector-modal-content {
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Group Header (from Claude Code .te) */
|
||||
.session-group-header {
|
||||
padding: 4px 8px;
|
||||
color: var(--app-primary-foreground);
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.session-group-header:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Session List Container (from Claude Code .St) */
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--app-list-padding);
|
||||
gap: var(--app-list-gap);
|
||||
}
|
||||
|
||||
/* Session List Item (from Claude Code .s and .s.U) */
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--app-list-item-padding);
|
||||
justify-content: space-between;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.session-item:hover,
|
||||
.session-item.hovering {
|
||||
background: var(--app-list-hover-background);
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background: var(--app-list-active-background);
|
||||
color: var(--app-list-active-foreground);
|
||||
}
|
||||
|
||||
/* Session Item Check Icon (from Claude Code .ne) */
|
||||
.session-item-check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.session-item.active .session-item-check {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Session Item Label (from Claude Code .ae) */
|
||||
.session-item-label {
|
||||
flex: 1;
|
||||
color: var(--app-primary-foreground);
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-item.active .session-item-label {
|
||||
font-weight: 600;
|
||||
color: var(--app-list-active-foreground);
|
||||
}
|
||||
|
||||
/* Session Item Meta Info (from Claude Code .Et) */
|
||||
.session-item-meta {
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
CSS Variables (from Claude Code root styles)
|
||||
=========================== */
|
||||
:root {
|
||||
/* Header */
|
||||
--app-header-background: var(--vscode-sideBar-background);
|
||||
|
||||
/* List Styles */
|
||||
--app-list-padding: 0px;
|
||||
--app-list-item-padding: 4px 8px;
|
||||
--app-list-border-color: transparent;
|
||||
--app-list-border-radius: 4px;
|
||||
--app-list-hover-background: var(--vscode-list-hoverBackground);
|
||||
--app-list-active-background: var(--vscode-list-activeSelectionBackground);
|
||||
--app-list-active-foreground: var(--vscode-list-activeSelectionForeground);
|
||||
--app-list-gap: 2px;
|
||||
|
||||
/* Menu Styles */
|
||||
--app-menu-background: var(--vscode-menu-background);
|
||||
--app-menu-border: var(--vscode-menu-border);
|
||||
--app-menu-foreground: var(--vscode-menu-foreground);
|
||||
--app-menu-selection-background: var(--vscode-menu-selectionBackground);
|
||||
--app-menu-selection-foreground: var(--vscode-menu-selectionForeground);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-state-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-state-logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.empty-state-logo-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.empty-state-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 15px;
|
||||
color: var(--app-primary-foreground);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import './EmptyState.css';
|
||||
|
||||
// Extend Window interface to include ICON_URI
|
||||
declare global {
|
||||
interface Window {
|
||||
ICON_URI?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const EmptyState: React.FC = () => {
|
||||
// Get icon URI from window, fallback to empty string if not available
|
||||
const iconUri = window.ICON_URI || '';
|
||||
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-content">
|
||||
{/* Qwen Logo */}
|
||||
<div className="empty-state-logo">
|
||||
{iconUri && (
|
||||
<img
|
||||
src={iconUri}
|
||||
alt="Qwen Logo"
|
||||
className="empty-state-logo-image"
|
||||
/>
|
||||
)}
|
||||
<div className="empty-state-text">
|
||||
<div className="empty-state-title">
|
||||
What to do first? Ask about this codebase or we can start writing
|
||||
code.
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* PlanDisplay.css - Styles for the task plan component
|
||||
*/
|
||||
|
||||
.plan-display {
|
||||
background-color: rgba(100, 150, 255, 0.05);
|
||||
border: 1px solid rgba(100, 150, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 8px 0;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.plan-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.plan-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(150, 180, 255, 1);
|
||||
}
|
||||
|
||||
.plan-entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.plan-entry {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background-color: var(--vscode-input-background);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.plan-entry[data-priority="high"] {
|
||||
border-left-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.plan-entry[data-priority="medium"] {
|
||||
border-left-color: #ffd93d;
|
||||
}
|
||||
|
||||
.plan-entry[data-priority="low"] {
|
||||
border-left-color: #6bcf7f;
|
||||
}
|
||||
|
||||
.plan-entry.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.plan-entry.completed .plan-entry-content {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.plan-entry.in_progress {
|
||||
background-color: rgba(100, 150, 255, 0.1);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.plan-entry-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.plan-entry-status,
|
||||
.plan-entry-priority {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.plan-entry-index {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.plan-entry-content {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import './PlanDisplay.css';
|
||||
|
||||
export interface PlanEntry {
|
||||
content: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
}
|
||||
|
||||
interface PlanDisplayProps {
|
||||
entries: PlanEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* PlanDisplay component - displays AI's task plan/todo list
|
||||
*/
|
||||
export const PlanDisplay: React.FC<PlanDisplayProps> = ({ entries }) => {
|
||||
const getPriorityIcon = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return '🔴';
|
||||
case 'medium':
|
||||
return '🟡';
|
||||
case 'low':
|
||||
return '🟢';
|
||||
default:
|
||||
return '⚪';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '⏱️';
|
||||
case 'in_progress':
|
||||
return '⚙️';
|
||||
case 'completed':
|
||||
return '✅';
|
||||
default:
|
||||
return '❓';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="plan-display">
|
||||
<div className="plan-header">
|
||||
<span className="plan-icon">📋</span>
|
||||
<span className="plan-title">Task Plan</span>
|
||||
</div>
|
||||
<div className="plan-entries">
|
||||
{entries.map((entry, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`plan-entry ${entry.status}`}
|
||||
data-priority={entry.priority}
|
||||
>
|
||||
<div className="plan-entry-header">
|
||||
<span className="plan-entry-status">
|
||||
{getStatusIcon(entry.status)}
|
||||
</span>
|
||||
<span className="plan-entry-priority">
|
||||
{getPriorityIcon(entry.priority)}
|
||||
</span>
|
||||
<span className="plan-entry-index">{index + 1}.</span>
|
||||
</div>
|
||||
<div className="plan-entry-content">{entry.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user