diff --git a/eslint.config.js b/eslint.config.js index 15446467..a896c241 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -114,7 +114,10 @@ export default tseslint.config( 'memfs/lib/volume.js', 'yargs/**', 'msw/node', - '**/generated/**' + '**/generated/**', + './styles/tailwind.css', + './styles/App.css', + './styles/ClaudeCodeStyles.css' ], }, ], diff --git a/packages/vscode-ide-companion/eslint.config.mjs b/packages/vscode-ide-companion/eslint.config.mjs index 5050f5ba..4b444a9b 100644 --- a/packages/vscode-ide-companion/eslint.config.mjs +++ b/packages/vscode-ide-companion/eslint.config.mjs @@ -7,6 +7,7 @@ import typescriptEslint from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; import reactHooks from 'eslint-plugin-react-hooks'; +import importPlugin from 'eslint-plugin-import'; export default [ { @@ -55,10 +56,13 @@ export default [ ], 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'error', + // Restrict deep imports but allow known-safe exceptions used by the webview + // - react-dom/client: required for React 18's createRoot API + // - ./styles/**: local CSS modules loaded by the webview 'import/no-internal-modules': [ 'error', { - allow: ['./styles/**'], + allow: ['react-dom/client', './styles/**'], }, ], diff --git a/packages/vscode-ide-companion/src/webview/components/SessionManager.css b/packages/vscode-ide-companion/src/webview/components/SessionManager.css deleted file mode 100644 index 275e6c5a..00000000 --- a/packages/vscode-ide-companion/src/webview/components/SessionManager.css +++ /dev/null @@ -1,199 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -/* Session Manager Styles */ -.session-manager { - display: flex; - flex-direction: column; - height: 100%; - font-size: var(--vscode-chat-font-size, 13px); - font-family: var(--vscode-chat-font-family); -} - -.session-manager-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px; - border-bottom: 1px solid var(--app-primary-border-color); -} - -.session-manager-header h3 { - margin: 0; - font-weight: 600; - color: var(--app-primary-foreground); -} - -.session-manager-actions { - padding: 16px; - border-bottom: 1px solid var(--app-primary-border-color); -} - -.session-manager-actions .secondary-button { - padding: 6px 12px; - border-radius: 4px; - border: none; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - font-size: var(--vscode-chat-font-size, 13px); - font-family: var(--vscode-chat-font-family); - cursor: pointer; - display: flex; - align-items: center; - gap: 6px; -} - -.session-manager-actions .secondary-button:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -.session-search { - padding: 12px 16px; - display: flex; - align-items: center; - gap: 8px; - border-bottom: 1px solid var(--app-primary-border-color); -} - -.session-search svg { - width: 16px; - height: 16px; - opacity: 0.5; - flex-shrink: 0; - color: var(--app-primary-foreground); -} - -.session-search input { - flex: 1; - background: transparent; - border: none; - outline: none; - color: var(--app-primary-foreground); - font-size: var(--vscode-chat-font-size, 13px); - font-family: var(--vscode-chat-font-family); - padding: 0; -} - -.session-search input::placeholder { - color: var(--app-input-placeholder-foreground); - opacity: 0.6; -} - -.session-list { - flex: 1; - overflow-y: auto; - padding: 8px; -} - -.session-list-loading { - display: flex; - align-items: center; - justify-content: center; - padding: 32px; - gap: 8px; - color: var(--app-secondary-foreground); -} - -.session-list-empty { - padding: 32px; - text-align: center; - color: var(--app-secondary-foreground); - opacity: 0.6; -} - -.session-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px; - background: transparent; - border: none; - border-radius: 6px; - cursor: pointer; - text-align: left; - width: 100%; - color: var(--app-primary-foreground); - transition: background 0.1s ease; - margin-bottom: 4px; -} - -.session-item:hover { - background: var(--app-list-hover-background); -} - -.session-item.active { - background: var(--app-list-active-background); - color: var(--app-list-active-foreground); -} - -.session-item-info { - flex: 1; - min-width: 0; -} - -.session-item-name { - font-weight: 500; - margin-bottom: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.session-item-meta { - display: flex; - gap: 12px; - font-size: 0.9em; - color: var(--app-secondary-foreground); -} - -.session-item-date, -.session-item-count { - white-space: nowrap; -} - -.session-item-actions { - display: flex; - gap: 8px; - margin-left: 12px; -} - -.session-item-actions .icon-button { - width: 24px; - height: 24px; - padding: 0; - background: transparent; - border: 1px solid transparent; - border-radius: 4px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - outline: none; -} - -.session-item-actions .icon-button:hover { - background: var(--app-ghost-button-hover-background); -} - -.session-item-actions .icon-button svg { - width: 16px; - height: 16px; - color: var(--app-primary-foreground); -} - -.loading-spinner { - width: 16px; - height: 16px; - border: 2px solid transparent; - border-top: 2px solid currentColor; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} \ No newline at end of file diff --git a/packages/vscode-ide-companion/src/webview/components/SessionManager.tsx b/packages/vscode-ide-companion/src/webview/components/SessionManager.tsx deleted file mode 100644 index 1b8b3461..00000000 --- a/packages/vscode-ide-companion/src/webview/components/SessionManager.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState, useEffect } from 'react'; -import { useVSCode } from '../hooks/useVSCode.js'; -import { - RefreshIcon, - SaveDocumentIcon, - SearchIcon, - PlayIcon, - SwitchIcon, -} from './icons/index.js'; - -interface Session { - id: string; - name: string; - lastUpdated: string; - messageCount: number; -} - -interface SessionManagerProps { - currentSessionId: string | null; - onSwitchSession: (sessionId: string) => void; - onSaveSession: () => void; - onResumeSession: (sessionId: string) => void; -} - -export const SessionManager: React.FC = ({ - currentSessionId, - onSwitchSession, - onSaveSession, - onResumeSession, -}) => { - const vscode = useVSCode(); - const [sessions, setSessions] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - - const loadSessions = React.useCallback(() => { - setIsLoading(true); - vscode.postMessage({ - type: 'listSavedSessions', - data: {}, - }); - }, [vscode]); - - // Load sessions when component mounts - useEffect(() => { - loadSessions(); - }, [loadSessions]); - - // Listen for session list updates - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - const message = event.data; - - if (message.type === 'savedSessionsList') { - setIsLoading(false); - setSessions(message.data.sessions || []); - } - }; - - window.addEventListener('message', handleMessage); - return () => window.removeEventListener('message', handleMessage); - }, []); - - const filteredSessions = sessions.filter((session) => - session.name.toLowerCase().includes(searchQuery.toLowerCase()), - ); - - const handleSaveCurrent = () => { - onSaveSession(); - }; - - const handleResumeSession = (sessionId: string) => { - onResumeSession(sessionId); - }; - - const handleSwitchSession = (sessionId: string) => { - onSwitchSession(sessionId); - }; - - return ( -
-
-

Saved Conversations

- -
- -
- -
- -
- - setSearchQuery(e.target.value)} - /> -
- -
- {isLoading ? ( -
-
- Loading conversations... -
- ) : filteredSessions.length === 0 ? ( -
- {searchQuery - ? 'No matching conversations' - : 'No saved conversations yet'} -
- ) : ( - filteredSessions.map((session) => ( -
-
-
{session.name}
-
- - {new Date(session.lastUpdated).toLocaleDateString()} - - - {session.messageCount}{' '} - {session.messageCount === 1 ? 'message' : 'messages'} - -
-
-
- - -
-
- )) - )} -
-
- ); -}; diff --git a/packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx b/packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx index 4ff72d21..33aa4300 100644 --- a/packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx @@ -21,39 +21,26 @@ export const ChatHeader: React.FC = ({ onNewSession, }) => (
- {/* Past Conversations Button */} - {/* Spacer */} -
+
- {/* New Session Button */} diff --git a/packages/vscode-ide-companion/src/webview/styles/App.css b/packages/vscode-ide-companion/src/webview/styles/App.css index 23741339..673dbb72 100644 --- a/packages/vscode-ide-companion/src/webview/styles/App.css +++ b/packages/vscode-ide-companion/src/webview/styles/App.css @@ -88,9 +88,6 @@ /* Widget */ --app-widget-border: var(--vscode-editorWidget-border); --app-widget-shadow: var(--vscode-widget-shadow); - - /* Ghost Button (from Claude Code) */ - --app-ghost-button-hover-background: var(--vscode-toolbar-hoverBackground); } /* Light Theme Overrides */ @@ -592,4 +589,4 @@ button { .permission-success-text { color: #4caf50; font-size: 13px; -} \ No newline at end of file +} diff --git a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css index bab769b1..370d35dd 100644 --- a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css +++ b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css @@ -9,7 +9,6 @@ /* Import component styles */ @import '../components/SaveSessionDialog.css'; -@import '../components/SessionManager.css'; @import '../components/EmptyState.css'; @import '../components/CompletionMenu.css'; @import '../components/ContextPills.css'; @@ -19,220 +18,6 @@ @import '../components/toolcalls/shared/DiffDisplay.css'; @import '../components/messages/AssistantMessage.css'; -/* =========================== - 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 Dropdown (from Claude Code .St/.Wt) - =========================== */ -.session-dropdown { - 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); -} - -/* =========================== - Search Box Container (from Claude Code .Lt) - =========================== */ -.session-search { - padding: 8px; - display: flex; - align-items: center; - gap: 8px; - border-bottom: 1px solid var(--app-menu-border); -} - -.session-search svg, -.session-search-icon { - width: 16px; - height: 16px; - opacity: 0.5; - flex-shrink: 0; - color: var(--app-primary-foreground); -} - -/* Search Input (from Claude Code .U) */ -.session-search-input { - flex: 1; - background: transparent; - border: none; - outline: none; - color: var(--app-menu-foreground); - font-size: var(--vscode-chat-font-size, 13px); - font-family: var(--vscode-chat-font-family); - padding: 0; -} - -.session-search-input::placeholder { - color: var(--app-input-placeholder-foreground); - opacity: 0.6; -} - -/* Session List Content Area (from Claude Code .jt/.It) */ -.session-list-content { - overflow-y: auto; - flex: 1; - user-select: none; - padding: 8px; -} - -/* Group Label (from Claude Code .ae) */ -.session-group-label { - padding: 4px 8px; - color: var(--app-primary-foreground); - opacity: 0.5; - font-size: 0.9em; - font-weight: 500; -} - -.session-group-label:not(:first-child) { - margin-top: 8px; -} - -/* Session Group Container (from Claude Code .At) */ -.session-group { - display: flex; - flex-direction: column; - gap: 2px; -} - -/* Session Item Button (from Claude Code .s) */ -.session-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 8px; - background: transparent; - border: none; - border-radius: 6px; - cursor: pointer; - text-align: left; - width: 100%; - font-size: var(--vscode-chat-font-size, 13px); - font-family: var(--vscode-chat-font-family); - color: var(--app-primary-foreground); - transition: background 0.1s ease; -} - -.session-item:hover { - background: var(--app-list-hover-background); -} - -/* Active Session (from Claude Code .N) */ -.session-item.active { - background: var(--app-list-active-background); - color: var(--app-list-active-foreground); - font-weight: 600; -} - -/* Session Title (from Claude Code .ce) */ -.session-item-title { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; -} - -/* Session Time (from Claude Code .Dt) */ -.session-item-time { - opacity: 0.6; - font-size: 0.9em; - flex-shrink: 0; - margin-left: 12px; -} - -/* Backdrop for dropdown */ -.session-selector-backdrop { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 999; - background: transparent; -} /* =========================== CSS Variables (from Claude Code root styles) diff --git a/packages/vscode-ide-companion/src/webview/styles/tailwind.css b/packages/vscode-ide-companion/src/webview/styles/tailwind.css index d89b8a7c..8e55cfcb 100644 --- a/packages/vscode-ide-companion/src/webview/styles/tailwind.css +++ b/packages/vscode-ide-companion/src/webview/styles/tailwind.css @@ -6,4 +6,33 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +/* =========================== + Reusable Component Classes + =========================== */ +@layer components { + .btn-ghost { + @apply bg-transparent border border-transparent rounded cursor-pointer outline-none transition-colors duration-200; + color: var(--app-primary-foreground); + font-size: var(--vscode-chat-font-size, 13px); + border-radius: 4px; + } + + .btn-ghost:hover, + .btn-ghost:focus { + background: var(--app-ghost-button-hover-background); + } + + .btn-sm { + @apply p-small; + } + + .btn-md { + @apply py-small px-medium; + } + + .icon-sm { + @apply w-4 h-4; + } +} \ No newline at end of file diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js index b617950e..67f9eeea 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -19,6 +19,7 @@ export default { './src/webview/components/InputForm.tsx', './src/webview/components/PermissionDrawer.tsx', './src/webview/components/PlanDisplay.tsx', + './src/webview/components/session/SessionSelector.tsx', ], theme: { extend: {