diff --git a/apps/desktop/MULTIPLE_INSTANCES.md b/apps/desktop/MULTIPLE_INSTANCES.md deleted file mode 100644 index 8795a6bc28d..00000000000 --- a/apps/desktop/MULTIPLE_INSTANCES.md +++ /dev/null @@ -1,115 +0,0 @@ -# Running Multiple Dev Instances - -This guide explains how to run multiple Electron instances simultaneously for parallel development. - -## Quick Start - -### Method 1: Automatic Port Management (Recommended) - -The app automatically manages ports for you! Each instance will: -- Try to use the last used port (stored in `~/.superset/dev-port.json`) -- If that port is unavailable, automatically find the next available port in the range (4927-4999) -- Save the chosen port for next time - -Simply run multiple instances: - -```bash -# Terminal 1 - Instance 1 -cd apps/desktop && bun dev - -# Terminal 2 - Instance 2 (will automatically get next available port) -cd apps/desktop && bun dev - -# Terminal 3 - Instance 3 (will automatically get next available port) -cd apps/desktop && bun dev -``` - -Each instance will automatically select an available port without any configuration needed. - -### Method 2: Using Worktrees - -When you create a new worktree via the Superset app, each worktree will automatically get its own port: - -```bash -# Worktree 1 - automatically finds available port -# Worktree 2 - automatically finds available port -# Worktree 3 - automatically finds available port -``` - -Ports are managed automatically - no manual configuration needed! - -### Windows - -Ports are automatically managed on Windows too: - -```powershell -# Terminal 1 - Instance 1 (automatically gets available port) -cd apps\desktop; bun dev - -# Terminal 2 - Instance 2 (automatically gets next available port) -cd apps\desktop; bun dev -``` - -Each instance will automatically select an available port without any configuration needed. - -## How It Works - -Each instance runs with: -- **Separate dev server port** - Automatically selected from available ports (4927-4999), persisted in `~/.superset/dev-port.json` -- **Separate user data directory** - Each instance stores settings, cache, and local storage in `~/.superset-dev-{instance-name}` (optional) - -This allows you to: -- Test different branches simultaneously -- Compare features side-by-side -- Debug without affecting your main development instance -- Test migrations and upgrades - -## Manual Setup - -If you want to use a custom user data directory: - -```bash -# Run with custom user data directory -bun dev -- --user-data-dir="$HOME/.superset-dev-custom" -``` - -The port will still be automatically selected - no need to configure it manually! - -## User Data Directories - -Each instance stores its data in: -- **macOS/Linux**: `~/.superset-dev-{instance-name}/` -- **Windows**: `%USERPROFILE%\.superset-dev-{instance-name}\` - -This includes: -- Application settings -- Local storage -- IndexedDB data -- Cache -- Workspace configurations - -## Cleaning Up - -To reset an instance, delete its user data directory: - -```bash -# macOS/Linux -rm -rf ~/.superset-dev-instance1 - -# Windows -Remove-Item -Recurse -Force "$env:USERPROFILE\.superset-dev-instance1" -``` - -## Troubleshooting - -### Port already in use -The app automatically handles port conflicts by finding the next available port. If you see port-related issues: -1. The app will automatically switch to an available port -2. Check `~/.superset/dev-port.json` to see which port is being used -3. If needed, delete the config file to reset port selection - -### Instances share the same data -Make sure each instance uses a different user data directory. Check that the `--user-data-dir` flag is being passed correctly. - -### Changes not reflected -If code changes aren't showing up, make sure you're editing in the correct workspace and that hot reload is working in the terminal running that instance. diff --git a/apps/desktop/TERMINAL_PERSISTENCE.md b/apps/desktop/TERMINAL_PERSISTENCE.md deleted file mode 100644 index 29fab0f1069..00000000000 --- a/apps/desktop/TERMINAL_PERSISTENCE.md +++ /dev/null @@ -1,1010 +0,0 @@ -# Terminal Session Persistence Architecture - -## Overview - -This document outlines the architecture for persistent terminal sessions in the Superset Desktop app. Terminal sessions will survive app restarts by leveraging tmux, allowing long-running processes to continue in the background and seamlessly reconnect when the app reopens. - -## Table of Contents - -- [Goals](#goals) -- [Current vs Proposed Architecture](#current-vs-proposed-architecture) -- [Approach Analysis](#approach-analysis) -- [Recommended Solution: tmux Integration](#recommended-solution-tmux-integration) -- [Implementation Plan](#implementation-plan) -- [Technical Details](#technical-details) -- [Data Models](#data-models) -- [Error Handling](#error-handling) -- [Testing Strategy](#testing-strategy) -- [Security Considerations](#security-considerations) -- [Migration & Rollout](#migration--rollout) -- [Future Enhancements](#future-enhancements) - -## Goals - -1. **Background Execution**: Terminal sessions continue running when app is closed -2. **Seamless Reconnection**: Automatically reconnect to sessions when app reopens -3. **Session Persistence**: Maintain session state across app restarts -4. **Optional Recovery**: Support recovery from laptop restart (state-based) -5. **Consistency**: Reliable behavior across all scenarios -6. **Platform Support**: Work on macOS/Linux, graceful degradation on Windows - -## Current vs Proposed Architecture - -### Current Architecture (node-pty direct) - -**TerminalManager** (`apps/desktop/src/main/lib/terminal.ts:7`) - -```typescript -// Spawns shells directly -pty.spawn(shell, args, { cwd, cols, rows, env }) - -// Stores processes in Map -Map - -// On app close: killAll() terminates everything -// ❌ All terminal state lost on restart -``` - -**Problems**: -- No persistence across app restarts -- Long-running processes killed when app closes -- No session recovery mechanism -- Lost state on app crashes - -### Proposed Architecture (tmux-backed) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Electron Main Process │ -│ │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ TmuxSessionManager │ │ -│ │ - Create/list/attach/detach tmux sessions │ │ -│ │ - Map terminal-id -> tmux session │ │ -│ │ - Handle session lifecycle │ │ -│ │ - Persist session metadata │ │ -│ └────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ spawns/controls │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ TmuxControlClient │ │ -│ │ - Communicates via tmux control mode (-CC) │ │ -│ │ - Parses control mode protocol │ │ -│ │ - Sends commands to tmux │ │ -│ │ - Streams output to renderer │ │ -│ └────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - │ stdin/stdout - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ tmux (system process) │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ session-1 │ │ session-2 │ │ session-3 │ │ -│ │ (terminal) │ │ (terminal) │ │ (terminal) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ↑ Persist when app closes │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Approach Analysis - -### Option 1: tmux-Based Solution ✅ **RECOMMENDED** - -**Architecture**: Use tmux sessions managed by Electron app - -**Pros**: -- ✅ Sessions naturally persist when app closes -- ✅ Built-in session management and recovery -- ✅ Can survive app crashes and force quits -- ✅ Mature, battle-tested technology (decades of production use) -- ✅ Easy reconnection via tmux attach -- ✅ Optional persistence across reboots (tmux-resurrect) -- ✅ Users can manually inspect sessions: `tmux attach -t superset-` -- ✅ Handles multiple sessions efficiently -- ✅ Built-in scrollback buffer management - -**Cons**: -- ❌ Requires tmux installed on user's system -- ❌ Platform-specific (Unix-like systems only) -- ❌ Need to parse tmux control mode protocol -- ❌ May have different behavior than node-pty - -**Implementation Complexity**: Medium-High - -### Option 2: Background Service Process - -**Architecture**: Separate Node.js daemon managing node-pty terminals - -**Pros**: -- ✅ Cross-platform (Windows, macOS, Linux) -- ✅ Keeps existing node-pty integration -- ✅ Full control over implementation - -**Cons**: -- ❌ Complex IPC between Electron and daemon -- ❌ Process lifecycle management complexity -- ❌ Need to handle daemon crashes/restarts -- ❌ More code to maintain and debug -- ❌ Harder to recover from system restarts -- ❌ Security considerations (daemon running as user) - -**Implementation Complexity**: High - -### Option 3: Hybrid Approach - -**Architecture**: Background service wrapping tmux - -**Pros**: -- ✅ Best of both worlds - -**Cons**: -- ❌ Most complex to implement -- ❌ Two systems to maintain - -**Implementation Complexity**: Very High - -### Decision: tmux-Based Solution - -**Rationale**: -1. **Purpose-built**: tmux is designed exactly for this use case -2. **Reliability**: Decades of production use in critical environments -3. **Recovery**: Built-in session persistence and restoration -4. **Simplicity**: Less code to maintain than custom daemon -5. **Developer-friendly**: Many developers already familiar with tmux -6. **Proven**: Used by VS Code Remote, tmuxinator, and countless other tools - -## Recommended Solution: tmux Integration - -### Session Naming Convention - -Each terminal session maps to a unique tmux session: - -``` -superset- -``` - -Example: `superset-550e8400-e29b-41d4-a716-446655440000` - -**Benefits**: -- Easy identification of Superset sessions: `tmux ls | grep superset-` -- UUID ensures no collisions -- Maps directly to terminal ID in app -- Simple cleanup: `tmux kill-session -t superset-*` - -### Terminal Lifecycle - -#### 1. Terminal Creation - -**Without existing session**: -```typescript -const sessionName = `superset-${terminalId}`; -pty.spawn('tmux', [ - 'new-session', - '-s', sessionName, // Session name - '-c', cwd, // Working directory - '-x', cols, // Width - '-y', rows, // Height - shell // Shell to run (bash, zsh, etc.) -], { env }) -``` - -**With existing session (reconnect)**: -```typescript -const sessionExists = await checkTmuxSession(sessionName); -if (sessionExists) { - pty.spawn('tmux', ['attach-session', '-t', sessionName], { cols, rows }) -} -``` - -#### 2. Terminal Destruction - -**User closes terminal tab**: -```typescript -// Explicitly kill the tmux session -await exec(`tmux kill-session -t ${sessionName}`); -``` - -**App closes** (normal exit): -```typescript -// DO NOT kill sessions - just detach -// Sessions remain running in background -terminalManager.detachAll(); // New method -``` - -#### 3. App Startup - -**Restore flow**: -```typescript -// 1. List all superset tmux sessions -const sessions = await listSupersetSessions(); - -// 2. Load saved metadata from persistence -const savedSessions = await db.getTerminalSessions(); - -// 3. Match and restore -for (const session of sessions) { - const metadata = savedSessions.find(s => s.tmuxSessionName === session); - if (metadata) { - // Offer to restore in UI - showRestorePrompt(metadata); - } else { - // Orphaned session - offer cleanup - offerCleanup(session); - } -} -``` - -### tmux Control Mode - -tmux provides `-CC` control mode designed for IDE integration: - -```bash -# Start control mode -tmux -CC new-session -s session-name - -# Control mode protocol (line-based, structured output) -%begin - -%end - -%session-changed -%window-add -%layout-change -%output -``` - -**Benefits**: -- Structured, parseable output (vs raw terminal) -- Event notifications (session changes, output, etc.) -- Programmatic control -- Multiple clients can attach simultaneously -- Reliable state tracking - -## Implementation Plan - -### Phase 1: Core tmux Integration (2-3 weeks) - -**Files to Create**: -- `apps/desktop/src/main/lib/tmux-utils.ts` - Utility functions -- `apps/desktop/src/main/lib/tmux-control-client.ts` - Control mode handler -- `apps/desktop/src/main/lib/tmux-session-manager.ts` - Session lifecycle - -**Files to Modify**: -- `apps/desktop/src/main/lib/terminal-ipcs.ts` - Add tmux IPC handlers -- `apps/desktop/src/main/lib/terminal.ts` - Integrate TmuxSessionManager -- `apps/desktop/src/shared/ipc-channels.ts` - Add new IPC channels - -**Tasks**: - -**1. TmuxUtils Module** -```typescript -// apps/desktop/src/main/lib/tmux-utils.ts -export async function checkTmuxInstalled(): Promise; -export async function listTmuxSessions(): Promise; -export async function listSupersetSessions(): Promise; -export async function sessionExists(name: string): Promise; -export async function killTmuxSession(name: string): Promise; -export async function getTmuxVersion(): Promise; -``` - -**2. TmuxControlClient Class** -```typescript -// apps/desktop/src/main/lib/tmux-control-client.ts -class TmuxControlClient { - private process: pty.IPty; - private buffer: string; - - // Spawn tmux in control mode - constructor(sessionName: string, options: TmuxOptions); - - // Parse control mode protocol - private parseControlOutput(data: string): ControlEvent[]; - - // Handle different event types - on(event: 'output', handler: (data: string) => void): void; - on(event: 'session-changed', handler: (session: string) => void): void; - on(event: 'exit', handler: (code: number) => void): void; - - // Send commands to tmux - sendCommand(cmd: string): Promise; - resize(cols: number, rows: number): void; - write(data: string): void; - kill(): void; -} -``` - -**3. TmuxSessionManager Class** -```typescript -// apps/desktop/src/main/lib/tmux-session-manager.ts -class TmuxSessionManager { - private sessions: Map; - private metadata: SessionMetadataStore; - - async createSession(config: { - terminalId: string; - workspaceId?: string; - cwd: string; - cols: number; - rows: number; - shell?: string; - }): Promise; - - async attachSession(terminalId: string): Promise; - async detachSession(terminalId: string): Promise; - async killSession(terminalId: string): Promise; - - async listSessions(): Promise; - async listOrphanedSessions(): Promise; - async cleanupOrphanedSessions(): Promise; - - // Get or create (idempotent) - async getOrCreateSession(terminalId: string, config: SessionConfig): Promise; - - // Detach all on app close - detachAll(): void; -} -``` - -**4. Session State Persistence** -```typescript -// Store in SQLite (using existing Drizzle setup) -// apps/desktop/src/main/lib/session-metadata-store.ts -class SessionMetadataStore { - async saveSession(session: TerminalSession): Promise; - async getSession(terminalId: string): Promise; - async getAllSessions(): Promise; - async deleteSession(terminalId: string): Promise; - async cleanupStale(maxAgeDays: number): Promise; -} -``` - -**5. IPC Channels** -```typescript -// apps/desktop/src/shared/ipc-channels.ts -interface IpcChannels { - "terminal:create": { - request: { - terminalId: string; - workspaceId?: string; - cwd: string; - cols: number; - rows: number; - persist?: boolean; // Feature flag - }; - response: IpcResponse<{ sessionId: string }>; - }; - - "terminal:attach": { - request: { terminalId: string }; - response: IpcResponse<{ sessionId: string }>; - }; - - "terminal:send": { - request: { terminalId: string; data: string }; - response: IpcResponse; - }; - - "terminal:resize": { - request: { terminalId: string; cols: number; rows: number }; - response: IpcResponse; - }; - - "terminal:list-orphaned": { - request: void; - response: TerminalSession[]; - }; - - "terminal:restore": { - request: { terminalId: string }; - response: IpcResponse<{ sessionId: string }>; - }; - - "terminal:cleanup-orphaned": { - request: void; - response: IpcResponse<{ count: number }>; - }; -} -``` - -**6. Modify TerminalManager** -```typescript -// apps/desktop/src/main/lib/terminal.ts -class TerminalManager { - private tmuxManager: TmuxSessionManager; - private useTmux: boolean; // Feature flag - - constructor() { - this.useTmux = settings.get('terminal.persistSessions', false); - this.tmuxManager = new TmuxSessionManager(); - } - - async create(options: CreateOptions) { - if (this.useTmux && await checkTmuxInstalled()) { - return this.tmuxManager.createSession(options); - } - // Fall back to node-pty - return this.createNodePtyTerminal(options); - } - - // Add new method for app close - detachAll() { - if (this.useTmux) { - this.tmuxManager.detachAll(); - } else { - this.killAll(); - } - } -} -``` - -### Phase 2: Reconnection Logic (1-2 weeks) - -**On App Startup**: -1. Check tmux installed: `checkTmuxInstalled()` -2. List Superset sessions: `tmux ls | grep 'superset-'` -3. Load saved metadata from database -4. Match tmux sessions to metadata -5. Show restore UI if orphaned sessions exist -6. Clean up stale/dead sessions - -**On Workspace/Tab Open**: -1. Check if terminal ID has existing session -2. If yes: attach to session -3. If no: create new session -4. Stream output to renderer - -**On App Close**: -1. Call `terminalManager.detachAll()` -2. Save all session metadata to database -3. Sessions continue running in background - -**Restore UI**: -```typescript -// Show notification/modal on startup -if (orphanedSessions.length > 0) { - showNotification({ - title: 'Restore Terminal Sessions?', - message: `${orphanedSessions.length} sessions from previous session`, - actions: ['Restore All', 'Select', 'Ignore'] - }); -} -``` - -### Phase 3: UI/UX Enhancements (1-2 weeks) - -**Reconnection Indicator**: -- Show "Reconnecting..." state while attaching -- Display session uptime -- Show last command/output preview - -**Session Management Panel**: -- List all active sessions -- Show attached/detached status -- Resource usage (if available) -- Manual cleanup actions - -**Settings Panel**: -```typescript -interface TerminalSettings { - persistSessions: boolean; // Enable feature - autoRestore: boolean; // Auto-restore on startup - sessionTTLDays: number; // Cleanup after N days - maxOrphanedSessions: number; // Limit orphaned sessions -} -``` - -### Phase 4: Recovery from System Restart (Optional, 1-2 weeks) - -**Approach: State-Based Recovery** - -Since tmux sessions don't survive system reboots, we'll save enough state to recreate sessions: - -```typescript -interface SessionState { - terminalId: string; - cwd: string; - env: Record; - commandHistory: string[]; - lastCommand: string; - createdAt: number; - lastActive: number; -} - -// Before session ends, save state -async saveSessionState(session: TerminalSession) { - const state: SessionState = { - terminalId: session.id, - cwd: await getCurrentDir(session), - env: session.env, - commandHistory: await getHistory(session), - lastCommand: await getLastCommand(session), - createdAt: session.created, - lastActive: Date.now(), - }; - await db.saveSessionState(state); -} - -// On app restart after reboot -async recreateSession(state: SessionState) { - const session = await createSession({ - terminalId: state.terminalId, - cwd: state.cwd, - env: state.env, - }); - - // Notify user - showNotification({ - message: 'Session recreated (system was restarted)', - details: `Working directory: ${state.cwd}`, - }); -} -``` - -**Future: tmux-resurrect Integration** (more complex but true persistence) -- Integrate tmux-resurrect plugin -- Automatically save tmux state periodically -- Restore full session tree after reboot - -## Technical Details - -### Platform Compatibility - -| Platform | tmux Available | Strategy | -|----------|----------------|----------| -| macOS | ✅ Pre-installed (modern versions) | Use tmux by default | -| Linux | ✅ Available in all package managers | Use tmux if installed, offer install instructions | -| Windows | ⚠️ Requires WSL | Detect WSL, fall back to node-pty, show installation guide | - -**Windows Detection**: -```typescript -async function canUseTmux(): Promise { - if (process.platform === 'win32') { - // Check if running in WSL - return await isWSL(); - } - return await checkTmuxInstalled(); -} -``` - -### Performance Considerations - -**Output Buffering**: -- tmux maintains configurable scrollback buffer -- Default: 2000 lines -- On reconnect: replay buffer contents -- Configuration: `set-option -g history-limit 10000` - -**Multiple Sessions**: -- Each tmux session is lightweight (~1-2 MB RAM) -- Hundreds of sessions feasible on modern hardware -- Monitor system resources in production - -**Control Mode vs Raw**: -- Control mode adds minimal overhead -- Structured output easier to parse -- More reliable than regex on raw output - -### Migration Strategy - -**Feature Flag**: -```typescript -// Gradual rollout via settings -settings.set('terminal.persistSessions', true); -``` - -**Factory Pattern**: -```typescript -interface ITerminalProvider { - create(options: TerminalOptions): Promise; - attach(id: string): Promise; - list(): Promise; -} - -class NodePtyProvider implements ITerminalProvider { /* ... */ } -class TmuxProvider implements ITerminalProvider { /* ... */ } - -// Select provider based on capability and settings -function getTerminalProvider(): ITerminalProvider { - const persistEnabled = settings.get('terminal.persistSessions'); - const tmuxAvailable = checkTmuxInstalled(); - - if (persistEnabled && tmuxAvailable) { - return new TmuxProvider(); - } - return new NodePtyProvider(); -} -``` - -## Data Models - -### TerminalSession - -```typescript -interface TerminalSession { - id: string; // UUID (terminal ID) - tmuxSessionName: string; // superset- - workspaceId?: string; // Optional workspace association - created: number; // Timestamp (ms) - lastAttached: number; // Timestamp (ms) - lastDetached: number; // Timestamp (ms) - cwd: string; // Working directory - status: 'attached' | 'detached' | 'dead'; - dimensions: { - cols: number; - rows: number; - }; - shell: string; // Shell path (bash, zsh, etc.) - metadata?: { - title?: string; - customName?: string; - tags?: string[]; - }; -} -``` - -### Database Schema (Drizzle ORM) - -```typescript -// apps/desktop/packages/db/src/schema/terminal-sessions.ts -import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; - -export const terminalSessions = sqliteTable('terminal_sessions', { - id: text('id').primaryKey(), - tmuxSessionName: text('tmux_session_name').notNull().unique(), - workspaceId: text('workspace_id'), - createdAt: integer('created_at').notNull(), - lastAttachedAt: integer('last_attached_at'), - lastDetachedAt: integer('last_detached_at'), - cwd: text('cwd').notNull(), - status: text('status').notNull(), // 'attached' | 'detached' | 'dead' - cols: integer('cols').notNull(), - rows: integer('rows').notNull(), - shell: text('shell').notNull(), - metadata: text('metadata'), // JSON string -}); - -// Indexes -export const terminalSessionsWorkspaceIdIdx = index('terminal_sessions_workspace_id_idx') - .on(terminalSessions.workspaceId); -export const terminalSessionsStatusIdx = index('terminal_sessions_status_idx') - .on(terminalSessions.status); -``` - -## Error Handling - -### Error Scenarios & Mitigations - -| Scenario | Detection | Recovery | User Experience | -|----------|-----------|----------|-----------------| -| **tmux not installed** | On startup: `which tmux` | Fall back to node-pty | Show one-time notification with install instructions | -| **tmux session crashed** | Periodic health checks | Clean up metadata, offer recreate | Show error notification, remove from session list | -| **Session name collision** | Check existence before create | Generate new name with suffix | Transparent to user | -| **Orphaned sessions** | On startup: list vs database | Show cleanup UI | Prompt to restore or remove | -| **Permission issues** | tmux command errors | Log error, fall back to node-pty | Show error message with suggestions | -| **tmux version too old** | Parse version number | Warn and disable feature | Show upgrade notification | -| **Socket permission error** | EACCES on tmux commands | Check/fix socket permissions | Show permission fix instructions | - -### Error Messages - -```typescript -const ERROR_MESSAGES = { - TMUX_NOT_FOUND: { - title: 'tmux not found', - message: 'Persistent terminal sessions require tmux to be installed.', - actions: [ - { label: 'Install Instructions', action: 'open-docs' }, - { label: 'Disable Feature', action: 'disable-persistence' } - ] - }, - SESSION_CRASHED: { - title: 'Terminal session lost', - message: 'The terminal session has crashed or was killed externally.', - actions: [ - { label: 'Recreate Session', action: 'recreate' }, - { label: 'Close', action: 'close' } - ] - }, - ATTACH_FAILED: { - title: 'Failed to reconnect', - message: 'Could not reconnect to the terminal session.', - actions: [ - { label: 'Retry', action: 'retry' }, - { label: 'Create New', action: 'create-new' } - ] - } -}; -``` - -## Testing Strategy - -### Unit Tests - -**TmuxUtils**: -- Test session name parsing -- Test version detection -- Test session listing/filtering - -**TmuxControlClient**: -- Test control mode protocol parsing -- Test event emission -- Test command handling - -**TmuxSessionManager**: -- Test session lifecycle -- Test metadata persistence -- Test orphaned session detection - -### Integration Tests - -**Full Lifecycle**: -```typescript -test('create, detach, reattach session', async () => { - const manager = new TmuxSessionManager(); - - // Create - const session = await manager.createSession({ - terminalId: 'test-123', - cwd: '/tmp', - cols: 80, - rows: 24, - }); - expect(session.status).toBe('attached'); - - // Detach - await manager.detachSession('test-123'); - expect(await checkTmuxSession('superset-test-123')).toBe(true); - - // Reattach - const reattached = await manager.attachSession('test-123'); - expect(reattached.id).toBe('test-123'); - - // Cleanup - await manager.killSession('test-123'); -}); -``` - -**Reconnection After Restart**: -```typescript -test('reconnect after simulated restart', async () => { - // Create session - const manager1 = new TmuxSessionManager(); - await manager1.createSession({ terminalId: 'test-456', cwd: '/tmp', cols: 80, rows: 24 }); - manager1.detachAll(); - - // Simulate restart - const manager2 = new TmuxSessionManager(); - const orphaned = await manager2.listOrphanedSessions(); - expect(orphaned).toHaveLength(1); - expect(orphaned[0].id).toBe('test-456'); - - // Restore - await manager2.attachSession('test-456'); -}); -``` - -### Manual Testing Checklist - -- [ ] Install/uninstall tmux, verify graceful degradation -- [ ] Create terminal, close app, reopen, verify reconnection -- [ ] Create multiple terminals, close app, reopen, verify all reconnect -- [ ] Close terminal tab, verify session is killed -- [ ] Force quit app, verify sessions survive -- [ ] Kill tmux process manually (`tmux kill-server`), verify app handles gracefully -- [ ] Restart laptop, verify state-based recovery works -- [ ] Delete workspace with active terminal, verify cleanup -- [ ] Test with slow/laggy terminal output -- [ ] Test with large scrollback buffer -- [ ] Test session after 24 hours idle -- [ ] Test with 10+ concurrent sessions - -## Security Considerations - -1. **Session Isolation** - - Each user's tmux sessions isolated by OS user account - - tmux socket: `~/.tmux-/` (user-only permissions) - -2. **Socket Permissions** - - tmux automatically sets `0700` (user-only) on socket directory - - Verify on startup: check permissions and warn if compromised - -3. **Command Injection Prevention** - ```typescript - // ❌ UNSAFE - exec(`tmux new-session -s ${userInput}`); - - // ✅ SAFE - spawn('tmux', ['new-session', '-s', sanitizeSessionName(userInput)]); - ``` - -4. **Session Names** - - Use UUIDs (no sensitive data) - - Don't include: workspace names, file paths, user data - -5. **Output Sanitization** - - Be careful with terminal escape sequences - - Sanitize before rendering in Electron - - Use libraries like `ansi-regex` to filter dangerous sequences - -6. **Environment Variables** - - Don't persist sensitive env vars (tokens, passwords) - - Whitelist safe variables for recreation - -## Documentation - -### User-Facing Documentation - -**Feature Announcement**: -```markdown -# Persistent Terminal Sessions - -Your terminal sessions now survive app restarts! - -- Close the app without losing running processes -- Automatically reconnects when you reopen -- Long-running builds, servers, and watch commands keep running - -**Requirements**: tmux (pre-installed on macOS, installable on Linux) -**Enable in**: Settings → Terminal → Persistent Sessions -``` - -**Troubleshooting Guide**: -- "tmux not found" - installation instructions by platform -- "Failed to reconnect" - cleanup orphaned sessions -- "Performance issues" - adjust scrollback buffer size - -### Developer Documentation - -- Architecture (this document) -- Type-safe IPC guide (see `docs/TYPE_SAFE_IPC.md`) -- Contributing to terminal features -- tmux control mode protocol reference - -## Migration & Rollout - -### Rollout Phases - -**Alpha (Week 1-2)**: Internal testing -- Core functionality working -- Feature flag enabled for developers only -- Gather feedback on bugs and UX - -**Beta (Week 3-4)**: Limited user testing -- Polish UI/UX -- Add session management panel -- Gather user feedback -- Monitor for edge cases - -**General Availability (Week 5+)**: Full release -- Enable by default for macOS/Linux -- Full documentation -- Monitor support tickets -- Collect metrics - -### Feature Flag Strategy - -```typescript -// Settings schema -interface Settings { - terminal: { - persistSessions: boolean; // Master toggle - autoRestore: boolean; // Auto-restore on startup - sessionTTLDays: number; // Cleanup after N days - maxOrphanedSessions: number; // Limit - } -} - -// Gradual rollout -const ROLLOUT_CONFIG = { - alpha: { enabledByDefault: false, userGroup: 'internal' }, - beta: { enabledByDefault: false, userGroup: 'beta-testers' }, - ga: { enabledByDefault: true, userGroup: 'all' }, -}; -``` - -## Future Enhancements - -### Phase 5+ (Future Work) - -1. **Session Sharing** - - Share terminal sessions with team members - - Collaborative debugging - - Remote pair programming support - -2. **Cloud Sync** - - Sync session metadata across devices - - Very complex, requires backend infrastructure - -3. **Session Templates** - - Save and restore session configurations - - Predefined workspace setups - - "Start dev environment" with multiple terminals - -4. **Split Panes** - - Full tmux pane management - - Expose tmux splits in UI - - Synchronized panes - -5. **Session Recording** - - Record and replay terminal sessions - - Export to video (asciinema format) - - Share recordings - -6. **AI Integration** - - Analyze terminal output for errors - - Suggest fixes for common issues - - Semantic search across terminal history - -7. **Advanced tmux Features** - - Expose tmux keybindings - - Custom tmux configurations per workspace - - Window/pane management - -## Open Questions - -1. **Session TTL**: Should sessions auto-close after N days? Default value? - - **Recommendation**: Yes, default 7 days, configurable - -2. **Max Sessions**: Limit concurrent sessions per workspace? - - **Recommendation**: Soft limit (warn at 10), hard limit (20) - -3. **tmux Keybindings**: Expose to power users? - - **Recommendation**: Phase 2+, opt-in for advanced users - -4. **tmux Version**: Minimum required version? - - **Recommendation**: tmux 2.6+ (2017), check and warn - -5. **Bundle tmux**: Should we bundle tmux with the app? - - **Recommendation**: No (legal/complexity issues), provide install instructions - -6. **Windows Support**: Should we support Windows Terminal integration? - - **Recommendation**: Future work, focus on Unix-like systems first - -## Success Metrics - -Track these metrics to measure feature success: - -1. **Adoption**: % of users with persistent sessions enabled -2. **Reconnection Success**: % of sessions successfully reconnected after restart -3. **Session Lifetime**: Average session uptime (indicator of usefulness) -4. **Crash Rate**: Sessions lost due to crashes (vs intentional kills) -5. **User Feedback**: NPS score, support tickets, feature requests -6. **Performance**: CPU/memory impact, session count distribution - -**Targets**: -- >80% reconnection success rate -- <5% increase in memory usage -- <10 support tickets per 1000 users -- Average session lifetime >1 hour (indicates usefulness) - -## Timeline Estimate - -| Phase | Duration | Deliverables | -|-------|----------|--------------| -| Phase 1: Core Integration | 2-3 weeks | tmux utils, control client, session manager, basic IPC | -| Phase 2: Reconnection Logic | 1-2 weeks | Startup flow, restore UI, orphaned session handling | -| Phase 3: UI/UX | 1-2 weeks | Settings panel, status indicators, session management | -| Phase 4: Recovery (Optional) | 1-2 weeks | State-based recovery from reboot | -| Testing & Polish | 1-2 weeks | Bug fixes, edge cases, performance tuning | -| **Total** | **6-11 weeks** | Full feature with optional reboot recovery | - -## Conclusion - -The tmux-based approach provides the most robust, maintainable solution for persistent terminal sessions: - -- **Proven**: Leverages decades of production-tested technology -- **Simple**: Less code to maintain than custom daemon solutions -- **Reliable**: Built-in session management and recovery -- **Flexible**: Supports advanced features (split panes, recording) in future -- **Developer-friendly**: Many users already familiar with tmux - -The phased implementation allows for incremental delivery, user feedback, and validation at each step. By starting with core functionality and gradually adding features, we minimize risk while delivering value early. - ---- - -**Related Documentation**: -- [Type-Safe IPC System](./docs/TYPE_SAFE_IPC.md) -- [Desktop App Architecture](./README.md) -- tmux control mode: https://github.com/tmux/tmux/wiki/Control-Mode diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d00d73fcbeb..809450f0e7a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -50,6 +50,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-mosaic-component": "^6.1.1", + "react-resizable-panels": "^3.0.6", "react-router-dom": "^7.8.2", "react-syntax-highlighter": "^16.1.0", "tailwind-merge": "^2.6.0" @@ -62,6 +63,7 @@ "@types/react": "^19.1.11", "@types/react-dom": "^19.1.7", "@types/react-syntax-highlighter": "^15.5.13", + "@types/semver": "^7.7.1", "@vitejs/plugin-react": "^5.0.1", "code-inspector-plugin": "^1.2.2", "cross-env": "^10.0.0", @@ -70,6 +72,7 @@ "electron-builder": "^26.0.12", "electron-extension-installer": "^2.0.0", "electron-vite": "^4.0.0", + "open": "^10.2.0", "rimraf": "^6.0.1", "rollup-plugin-inject-process-env": "^1.3.1", "tailwindcss": "^4.0.9", diff --git a/apps/desktop/src/renderer/globals.css b/apps/desktop/src/renderer/globals.css index 2bd7848b9fd..01180ee0262 100644 --- a/apps/desktop/src/renderer/globals.css +++ b/apps/desktop/src/renderer/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; @import "@superset/ui/globals.css"; -@plugin 'tailwindcss-animate'; +@plugin "tailwindcss-animate"; @source "./**/*.{ts,tsx}"; diff --git a/apps/desktop/src/renderer/screens/main/MainScreen.tsx b/apps/desktop/src/renderer/screens/main/MainScreen.tsx index cc1c51ffdb5..e4465696613 100644 --- a/apps/desktop/src/renderer/screens/main/MainScreen.tsx +++ b/apps/desktop/src/renderer/screens/main/MainScreen.tsx @@ -238,12 +238,12 @@ function enrichWorktreesWithTasks( isPending: true, // Mark as pending for UI task: pending.taskData ? { - id: pending.id, - slug: pending.taskData.slug, - title: pending.taskData.name, - status: pending.taskData.status, - description: pending.description || "", - } + id: pending.id, + slug: pending.taskData.slug, + title: pending.taskData.name, + status: pending.taskData.status, + description: pending.description || "", + } : undefined, }), ); @@ -1024,10 +1024,10 @@ export function MainScreen() { {/* Main content panel */} {loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( + error || + !currentWorkspace || + !selectedTab || + !selectedWorktree ? ( { // Listen for workspace-opened event useEffect(() => { const handler = async (workspace: Workspace) => { - console.log( - "[MainLayout] Workspace opened event received:", - workspace, - ); + console.log("[MainLayout] Workspace opened event received:", workspace); setLoading(false); await window.ipcRenderer.invoke( @@ -1025,10 +1022,10 @@ export const MainLayout: React.FC = () => { {/* Main content panel */} {loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( + error || + !currentWorkspace || + !selectedTab || + !selectedWorktree ? ( -

Engineers love Superset

@@ -83,7 +82,9 @@ export function TestimonialsSection() {
{testimonial.author}
-
{testimonial.title}
+
+ {testimonial.title} +
@@ -135,7 +136,9 @@ export function TestimonialsSection() {
{testimonial.author}
-
{testimonial.title}
+
+ {testimonial.title} +
diff --git a/biome.jsonc b/biome.jsonc index f137dcd92b4..c4678cc9866 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -8,6 +8,12 @@ "formatter": { "formatWithErrors": true }, + "css": { + "parser": { + "cssModules": true, + "tailwindDirectives": true + } + }, "linter": { "rules": { "suspicious": { diff --git a/bun.lock b/bun.lock index f7042453e96..699c03c4a22 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@superset/repo", @@ -64,6 +65,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-mosaic-component": "^6.1.1", + "react-resizable-panels": "^3.0.6", "react-router-dom": "^7.8.2", "react-syntax-highlighter": "^16.1.0", "tailwind-merge": "^2.6.0", @@ -76,6 +78,7 @@ "@types/react": "^19.1.11", "@types/react-dom": "^19.1.7", "@types/react-syntax-highlighter": "^15.5.13", + "@types/semver": "^7.7.1", "@vitejs/plugin-react": "^5.0.1", "code-inspector-plugin": "^1.2.2", "cross-env": "^10.0.0", @@ -84,6 +87,7 @@ "electron-builder": "^26.0.12", "electron-extension-installer": "^2.0.0", "electron-vite": "^4.0.0", + "open": "^10.2.0", "rimraf": "^6.0.1", "rollup-plugin-inject-process-env": "^1.3.1", "tailwindcss": "^4.0.9", @@ -1098,6 +1102,8 @@ "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], @@ -1286,6 +1292,8 @@ "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], @@ -1488,12 +1496,18 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="], + + "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], @@ -2278,6 +2292,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "ora": ["ora@9.0.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0", "strip-ansi": "^7.1.2" } }, "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A=="], "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], @@ -2540,6 +2556,8 @@ "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], @@ -2866,6 +2884,8 @@ "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], diff --git a/conductor.json b/conductor.json index 1ff746037b9..518fc6e5e78 100644 --- a/conductor.json +++ b/conductor.json @@ -1,5 +1,5 @@ { - "scripts": { - "setup": "./conductor-setup.sh" - } + "scripts": { + "setup": "./conductor-setup.sh" + } } diff --git a/packages/models/package.json b/packages/models/package.json index a0b7492998a..071c5591f67 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -10,4 +10,4 @@ "bun-types": "^1.3.1", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 6425d171dee..e61ce74eb48 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -55,4 +55,4 @@ "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } -} \ No newline at end of file +} diff --git a/packages/ui/src/components/context-menu.tsx b/packages/ui/src/components/context-menu.tsx index 93d5a2a41d5..d186942f0b1 100644 --- a/packages/ui/src/components/context-menu.tsx +++ b/packages/ui/src/components/context-menu.tsx @@ -7,7 +7,13 @@ import { cn } from "../lib/utils"; function ContextMenu({ ...props }: React.ComponentProps) { - return ; + return ( + + ); } function ContextMenuTrigger({