feat: Add VS Code extension with Svelte web UI - #103
Conversation
Implements a complete VS Code extension for LogViewer with: Extension Features: - Sidebar panel with context selector, quick filters, and search - Results panel with tabbed log viewer and infinite scroll - Hash-based tab management (reuses tabs for identical searches) - Server lifecycle management (spawns Go binary on demand) Web UI Features: - Svelte 5 with modern runes (, , ) - Edge-to-edge table design with VS Code theme integration - Color-coded log levels (ERROR/WARN/INFO/DEBUG) - Row expansion for JSON details - Infinite scroll pagination with IntersectionObserver - Load more button for manual pagination trigger - Toolbar actions: refresh, export JSON, copy to clipboard - Recent searches saved in localStorage Backend Changes: - Added pagination support to handlers.go - Pass pageToken from request to LogSearch - Return nextPageToken via GetPaginationInfo() - Support NativeQuery in QueryRequest Build System: - Makefile targets for building web frontend - Makefile targets for copying assets to extension - Makefile targets for packaging .vsix Bug Fixes: - VS Code API can only be acquired once (fixed in Sidebar.svelte) - IntersectionObserver setup with Svelte 5 - Backend pagination token properly retrieved and returned - Reversed log order (newest first, load older on scroll) Documentation: - extension/README.md - User guide and development docs - extension/IMPLEMENTATION_NOTES.md - Technical details and bugs fixed Co-Authored-By: Claude <noreply@anthropic.com>
Summary of ChangesHello @wquintal, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a comprehensive VS Code extension for LogViewer, significantly enhancing the user experience by providing an integrated, interactive log analysis tool. The extension leverages a Svelte 5 web UI for its sidebar and results panels, offering features like context selection, quick filters, advanced search, and infinite scroll. Underpinning this is an updated Go backend with improved pagination and native query support, ensuring efficient data handling. The changes also include a refined build system to manage the multi-component architecture, making development and deployment more robust. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive VS Code extension for LogViewer, featuring a Svelte 5 web UI and robust backend integration. The implementation of hash-based tab management and infinite scroll pagination significantly enhances the user experience. However, there are critical issues regarding file export and clipboard operations within the webview sandbox that need to be addressed by delegating these tasks to the extension host via message passing. Additionally, the port discovery mechanism and asset path handling in the webview should be made more robust to ensure reliability across different environments.
|
|
||
| function handleExport() { | ||
| const json = JSON.stringify(logs, null, 2); | ||
| const blob = new Blob([json], { type: 'application/json' }); | ||
| const url = URL.createObjectURL(blob); | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| a.download = `logs-${contextId}-${Date.now()}.json`; | ||
| a.click(); | ||
| URL.revokeObjectURL(url); | ||
| } |
There was a problem hiding this comment.
Triggering a file download using document.createElement('a').click() will likely fail within a VS Code webview due to security sandbox restrictions. The recommended approach is to send a message to the extension host and use the vscode.workspace.fs or vscode.window.showSaveDialog APIs to handle file saving.
| /(?:listening|started|port)[^\d]*(\d+)/i | ||
| ); | ||
| if (portMatch) { |
There was a problem hiding this comment.
The current regex for port discovery is too broad and could accidentally match other numbers in the server's output (like process IDs or timestamps). Since the Go backend explicitly prints a specific string for this purpose, the regex should be anchored to that string to ensure reliability.
const portMatch = output.match(/Server listening on port (\d+)/i);| function handleCopy() { | ||
| const text = logs.map(l => `${l.timestamp} ${l.level} ${l.message}`).join('\n'); | ||
| navigator.clipboard.writeText(text); | ||
| } |
There was a problem hiding this comment.
| html = html.replace(/href="\.\/assets\//g, `href="${assetsUri}/`); | ||
| html = html.replace(/src="\.\/assets\//g, `src="${assetsUri}/`); |
There was a problem hiding this comment.
The asset path replacement logic is brittle as it strictly expects ./assets/. Depending on Vite's configuration or version, paths might be generated as assets/ or /assets/. A more flexible regex would improve the robustness of the webview loading process.
| html = html.replace(/href="\.\/assets\//g, `href="${assetsUri}/`); | |
| html = html.replace(/src="\.\/assets\//g, `src="${assetsUri}/`); | |
| html = html.replace(/(href|src)="\.?\/?assets\//g, `$1="${assetsUri}/`); |
| <script lang="ts"> | ||
| import { onMount, onDestroy } from 'svelte'; | ||
| import { api } from '$lib/api'; | ||
| import { logs, meta, isLoading, error } from '$lib/stores/logs'; | ||
| import { selectedContextId } from '$lib/stores/context'; | ||
| import { searchState } from '$lib/stores/search'; | ||
| import LogRow from './LogRow.svelte'; | ||
| import { get } from 'svelte/store'; | ||
|
|
||
| let sentinel: HTMLDivElement; | ||
| let observer: IntersectionObserver; | ||
|
|
||
| export async function executeSearch(append = false) { | ||
| const contextId = get(selectedContextId); | ||
| if (!contextId) return; | ||
|
|
||
| isLoading.set(true); | ||
| error.set(null); | ||
|
|
||
| try { | ||
| const currentSearch = get(searchState); | ||
| const currentMeta = get(meta); | ||
|
|
||
| const response = await api.queryLogs({ | ||
| contextId, | ||
| nativeQuery: currentSearch.query || undefined, | ||
| range: { last: currentSearch.timeRange }, | ||
| fields: currentSearch.level ? { level: currentSearch.level } : undefined, | ||
| size: 100, | ||
| pageToken: append ? currentMeta?.nextPageToken : undefined, | ||
| }); | ||
|
|
||
| if (append) { | ||
| logs.update((current) => [...current, ...response.logs]); | ||
| } else { | ||
| logs.set(response.logs); | ||
| } | ||
| meta.set(response.meta); | ||
| } catch (e) { | ||
| error.set(e instanceof Error ? e.message : 'Query failed'); | ||
| } finally { | ||
| isLoading.set(false); | ||
| } | ||
| } | ||
|
|
||
| function loadMore() { | ||
| const loading = get(isLoading); | ||
| const currentMeta = get(meta); | ||
| if (!loading && currentMeta?.nextPageToken) { | ||
| executeSearch(true); | ||
| } | ||
| } | ||
|
|
||
| onMount(() => { | ||
| observer = new IntersectionObserver( | ||
| (entries) => { | ||
| if (entries[0].isIntersecting) { | ||
| loadMore(); | ||
| } | ||
| }, | ||
| { threshold: 0.1 } | ||
| ); | ||
|
|
||
| if (sentinel) { | ||
| observer.observe(sentinel); | ||
| } | ||
| }); | ||
|
|
||
| onDestroy(() => { | ||
| observer?.disconnect(); | ||
| }); | ||
| </script> | ||
|
|
||
| <div class="log-table"> | ||
| {#if $error} | ||
| <div class="error">{$error}</div> | ||
| {/if} | ||
|
|
||
| <table> | ||
| <thead> | ||
| <tr> | ||
| <th class="timestamp">Timestamp</th> | ||
| <th class="level">Level</th> | ||
| <th class="message">Message</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {#each $logs as log, i (log.timestamp + i)} | ||
| <LogRow {log} /> | ||
| {/each} | ||
| </tbody> | ||
| </table> | ||
|
|
||
| {#if $isLoading} | ||
| <div class="loading">Loading...</div> | ||
| {/if} | ||
|
|
||
| <!-- Sentinel for infinite scroll --> | ||
| <div bind:this={sentinel} class="sentinel"></div> | ||
|
|
||
| {#if $meta} | ||
| <div class="meta"> | ||
| {$meta.resultCount} results in {$meta.queryTime} | ||
| </div> | ||
| {/if} | ||
| </div> | ||
|
|
||
| <style> | ||
| .log-table { | ||
| flex: 1; | ||
| overflow: auto; | ||
| } | ||
| table { | ||
| width: 100%; | ||
| border-collapse: collapse; | ||
| font-family: monospace; | ||
| font-size: 12px; | ||
| } | ||
| th { | ||
| text-align: left; | ||
| padding: 0.5rem; | ||
| background: var(--vscode-editor-background, #1e1e1e); | ||
| position: sticky; | ||
| top: 0; | ||
| border-bottom: 1px solid var(--vscode-panel-border, #333333); | ||
| } | ||
| .timestamp { | ||
| width: 180px; | ||
| } | ||
| .level { | ||
| width: 60px; | ||
| } | ||
| .error { | ||
| padding: 1rem; | ||
| color: var(--vscode-errorForeground, #f44336); | ||
| } | ||
| .loading, | ||
| .meta { | ||
| padding: 0.5rem; | ||
| text-align: center; | ||
| color: var(--vscode-descriptionForeground, #888888); | ||
| } | ||
| .sentinel { | ||
| height: 1px; | ||
| } | ||
| </style> |
There was a problem hiding this comment.
This component appears to be redundant as Results.svelte implements the same functionality using modern Svelte 5 runes. Maintaining two separate implementations of the log table increases technical debt and the risk of inconsistent behavior. Consider removing this file and unifying the logic in Results.svelte.
Implements comprehensive configuration management features: New Settings: - logviewer.configPath: Specify custom config file path - Supports default ~/.logviewer/config.yaml - Supports drop-in configs in ~/.logviewer/configs/*.yaml - Respects LOGVIEWER_CONFIG environment variable - logviewer.watchConfigFiles (default: true) - Watches config files for changes - Detects modifications, creations, and deletions - Automatically recreates watcher when settings change - logviewer.autoRestartOnConfigChange (default: false) - Auto-restart: Server restarts silently on config change - Manual mode: Shows notification with 'Restart Now' button - Prevents concurrent restarts with isRestarting flag Features: - File watching for config files (FileSystemWatcher) - Multi-file config support (main + drop-in directory) - Custom config path support with dynamic watcher recreation - Notification on config change with restart options - Warning notification on config file deletion - Comprehensive logging to LogViewer Server output channel - Graceful error handling for restart failures Implementation: - Added getConfigPaths() to resolve all watched config files - Added setupConfigWatcher() with change/create/delete handlers - Added handleConfigChangeRestart() with auto/manual modes - Added disposeConfigWatcher() for cleanup on setting changes - Settings change listener to recreate watcher dynamically - Proper disposal in deactivate() to clean up resources Documentation: - extension/SETTINGS.md: Complete settings reference - All settings with descriptions and examples - Recommended configurations for different scenarios - Troubleshooting guide - Environment variable documentation - extension/TESTING_CONFIG_WATCH.md: Comprehensive test suite - 10 manual test cases covering all scenarios - Edge case testing (permissions, symlinks, network drives) - Performance testing guidance - Debugging instructions Benefits: - Development: Auto-restart on config save speeds up iteration - Production: Manual restart prevents unexpected disruptions - Flexibility: Works with default paths, custom paths, and env vars - Reliability: Proper error handling and user feedback Note: The Go server loads config at startup (no hot-reload). Extension handles config changes via server restart. Co-Authored-By: Claude <noreply@anthropic.com>
Adds a convenient footer link in the sidebar panel to quickly open the LogViewer configuration file in VS Code. Features: - Footer button at bottom of sidebar with gear icon (⚙️) - Styled as VS Code link with hover/active states - Uses margin-top: auto to stick to bottom of sidebar - Sends 'openConfig' message to extension host Extension handling: - New message handler for 'openConfig' type - getConfigPath() resolves config file path: - Uses logviewer.configPath setting if set - Falls back to LOGVIEWER_CONFIG environment variable - Defaults to ~/.logviewer/config.yaml - handleOpenConfig() opens file in editor: - Prompts to create file if it doesn't exist - Creates directory structure if needed - Includes basic YAML template (clients/contexts) - Opens in main editor column (not preview) Benefits: - Quick access to config without leaving VS Code - Helps new users discover config file location - Streamlines config editing workflow - Follows VS Code patterns (like settings.json link) UI Design: - Consistent with VS Code sidebar styling - Uses semantic colors (textLink-foreground) - Proper focus states for accessibility - Tooltip shows 'Open configuration file' Co-Authored-By: Claude <noreply@anthropic.com>
Implements real-time configuration updates via SSE, allowing the sidebar to automatically refresh when config files change. Backend Changes (pkg/server/events.go): - EventBroker: Manages SSE client subscriptions and broadcasts - Subscribe/Unsubscribe methods for client management - Broadcast sends events to all connected clients - Non-blocking with timeouts to prevent slow clients blocking - Event types: - config-reloaded: Sent when config file changes - server-error: Sent when config reload fails - connected: Initial connection confirmation - ConfigWatcher: Watches config file with fsnotify - Monitors file Write and Create events - Debounces rapid changes (1 second) - Prevents concurrent reloads with mutex - Graceful error handling with error events - SSE Handler (/events endpoint): - Proper SSE headers (text/event-stream, no-cache) - Heartbeat every 30 seconds - Automatic reconnection on disconnect - Request context cancellation support Backend Changes (pkg/server/server.go): - Added fields: - eventBroker: Manages SSE connections - configWatcher: Watches config file - configPath: Path to config file for reloading - configMutex: Thread-safe config access - NewServer: Now accepts configPath parameter - Initializes EventBroker - Creates event route /events - Start: Initializes config watcher if path provided - Starts watcher in background - Stops watcher on shutdown - ReloadConfig: Hot-reloads configuration - Loads new config file - Recreates client and search factories - Atomically swaps config with mutex - Thread-safe for concurrent requests - GetConfig: Thread-safe config access Frontend Changes (web/src/lib/components/Sidebar.svelte): - setupEventSource: Connects to SSE endpoint - Reads port from window.LOGVIEWER_PORT - Listens for config-reloaded events - Auto-refreshes contexts on config change - Handles errors and reconnection - loadContexts: Extracted as reusable function - Called on mount and on config-reloaded event - Clears errors on successful load - onMount: Now calls setupEventSource - SSE connection established immediately - Logs connection status to console Dependencies: - Added github.com/fsnotify/fsnotify for file watching Benefits: - No manual restart needed after config changes - Real-time context list updates in sidebar - Seamless UX - just save config file - Works alongside extension's file watcher (two-tier) - Multiple clients supported (all browsers/panels) Architecture: Two-tier watching: 1. Extension watches → prompts to restart server 2. Server watches → hot-reloads config + broadcasts SSE User can choose: - Disable extension watching (manual restart only) - Enable auto-restart (server restarts, loses state) - Keep both (notification + SSE hot-reload without restart) Thread safety: - configMutex protects config reads/writes - EventBroker uses RWMutex for client map - Concurrent requests use current config safely Co-Authored-By: Claude <noreply@anthropic.com>
Implements a complete VS Code extension for LogViewer with:
Extension Features:
Web UI Features:
Backend Changes:
Build System:
Bug Fixes:
Documentation: