Fix linter formatting issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-06-17 01:52:31 +02:00
parent eac1d8251a
commit c04cb26f4e
9 changed files with 904 additions and 125 deletions

View file

@ -1,115 +1,179 @@
# VibeTunnel Web Frontend
A web interface for the VibeTunnel terminal multiplexer. This frontend allows you to view and interact with multiple terminal sessions through a browser interface.
A modern web interface for the VibeTunnel terminal multiplexer built with TypeScript, Lit Elements, and XTerm.js. Provides professional terminal emulation with mobile-optimized controls and real-time session management.
## Features
- View multiple terminal processes in a web interface
- Real-time terminal output using asciinema player
- Interactive terminal input via WebSocket connections
- Terminal-themed UI with monospace fonts
- Mock server for development without backend dependencies
- **Professional Terminal Emulation** using XTerm.js with full VT compatibility
- **Real-time Session Management** with live streaming via Server-Sent Events
- **Mobile-Optimized Interface** with touch controls and responsive design
- **Session Snapshots** for previewing terminal output in card view
- **Interactive Terminal Input** with full keyboard support and mobile input overlay
- **VS Code Dark Theme** with consistent styling throughout
- **Custom Font Support** using Fira Code with programming ligatures
- **File Browser** for selecting working directories
- **Session Lifecycle Management** (create, monitor, kill, cleanup)
## Setup
## Quick Start
1. **Install dependencies:**
```bash
npm install
```
2. **Build CSS:**
```bash
npm run build-css
```
## Development
1. **Start the development server:**
2. **Start development server:**
```bash
npm run dev
```
2. **Open your browser:**
3. **Open browser:**
Navigate to `http://localhost:3000`
3. **CSS Development:**
For live CSS rebuilding during development, run in a separate terminal:
```bash
npm run build-css
```
## Development Scripts
The development server includes:
- Mock API endpoints for process management
- Mock WebSocket connections with sample terminal data
- Hot reloading for server changes (restart with `npm run dev`)
```bash
# Development (auto-rebuild and watch)
npm run dev # Start full dev environment
npm run watch:server # Watch server TypeScript only
npm run watch:css # Watch CSS changes only
## Build for Deployment
# Building
npm run build # Build everything for production
npm run build:server # Build server TypeScript
npm run build:client # Build client TypeScript
npm run build:css # Build Tailwind CSS
To build the project for production deployment:
# Bundling (ES modules)
npm run bundle # Bundle client code
npm run bundle:watch # Watch and bundle client code
1. **Compile TypeScript:**
# Code Quality
npm run lint # Check ESLint issues
npm run lint:fix # Auto-fix ESLint issues
npm run format # Format code with Prettier
npm run format:check # Check code formatting
npm run pre-commit # Run all quality checks
# Testing
npm run test # Run Jest tests
npm run test:watch # Watch and run tests
```
## Architecture
### Client-Side Components
Built with **Lit Elements** (Web Components):
```
src/client/
├── app.ts # Main application controller
├── components/
│ ├── app-header.ts # Main navigation and controls
│ ├── session-list.ts # Session grid with cards
│ ├── session-card.ts # Individual session preview
│ ├── session-view.ts # Full terminal view
│ ├── session-create-form.ts # New session modal
│ └── file-browser.ts # Directory selection
├── renderer.ts # XTerm.js terminal renderer
└── scale-fit-addon.ts # Custom terminal scaling
```
### Server-Side Architecture
**Express.js** server with **tty-fwd integration**:
```
src/
├── server.ts # Main Express server
└── input.css # Tailwind source styles
```
### Build Output
```
dist/ # Compiled TypeScript
public/
├── bundle/
│ ├── client-bundle.js # Bundled client code
│ ├── renderer.js # Terminal renderer
│ └── output.css # Compiled styles
├── fonts/ # Fira Code font files
└── index.html # Main HTML
```
## API Reference
### Session Management
```
GET /api/sessions # List all sessions
POST /api/sessions # Create new session
DELETE /api/sessions/:id # Kill session
DELETE /api/sessions/:id/cleanup # Clean up session files
POST /api/cleanup-exited # Clean all exited sessions
```
### Terminal I/O
```
GET /api/sessions/:id/stream # Live session stream (SSE)
GET /api/sessions/:id/snapshot # Session snapshot (cast format)
POST /api/sessions/:id/input # Send input to session
```
### File System
```
GET /api/fs/browse?path=<path> # Browse directories
POST /api/mkdir # Create directory
```
## Technology Stack
- **Frontend Framework:** Lit Elements (Web Components)
- **Terminal Emulation:** XTerm.js with custom addons
- **Styling:** Tailwind CSS with VS Code theme
- **Typography:** Fira Code Variable Font
- **Backend:** Express.js + TypeScript
- **Terminal Backend:** tty-fwd (Rust binary)
- **Build Tools:** TypeScript, ESBuild, Tailwind
- **Code Quality:** ESLint, Prettier, Pre-commit hooks
## Mobile Support
- **Touch-optimized scrolling** with proper overscroll prevention
- **Mobile input overlay** with virtual keyboard support
- **Responsive design** with mobile-first approach
- **Gesture navigation** (swipe from edge to go back)
- **Pull-to-refresh prevention** during terminal interaction
## Browser Compatibility
- **Modern ES6+ browsers** (Chrome 63+, Firefox 67+, Safari 13+)
- **Mobile browsers** with full touch support
- **Progressive enhancement** with graceful degradation
## Deployment
1. **Build for production:**
```bash
npm run build
```
2. **Build CSS:**
```bash
npm run build-css
```
3. **Create deployment files:**
```bash
# Create dist directory structure
mkdir -p dist/public
# Copy static files
cp public/index.html dist/public/
cp public/app.js dist/public/
cp public/output.css dist/public/
# The compiled server will be in dist/server.js
```
After building, your `dist/` folder will contain:
- `dist/server.js` - Compiled Express server
- `dist/public/index.html` - Main HTML file
- `dist/public/app.js` - Client-side JavaScript
- `dist/public/output.css` - Compiled Tailwind CSS
## Deployment
1. **Production server:**
2. **Start production server:**
```bash
npm start
```
2. **Environment variables:**
3. **Environment variables:**
- `PORT` - Server port (default: 3000)
- `TTY_FWD_CONTROL_DIR` - tty-fwd control directory
## Project Structure
## Development Notes
```
src/
├── server.ts # Express server with mock API and WebSocket
├── input.css # Tailwind CSS source
public/
├── index.html # Main HTML interface
└── app.js # Client-side terminal management
dist/ # Built files (created after build)
├── server.js # Compiled server
└── public/ # Static assets
```
## API Endpoints
The mock server provides these endpoints:
- `GET /api/processes` - List all processes
- `GET /api/processes/:id` - Get specific process details
- `WebSocket /?processId=:id` - Connect to process terminal stream
## Technology Stack
- **Frontend:** Vanilla JavaScript, Tailwind CSS, Asciinema Player
- **Backend:** Express.js, WebSocket, TypeScript
- **Build Tools:** TypeScript Compiler, Tailwind CSS, PostCSS
- **Hot reload** enabled in development
- **TypeScript strict mode** with comprehensive type checking
- **ESLint + Prettier** enforced via pre-commit hooks
- **Component-based architecture** for maintainability
- **Mobile-first responsive design** principles

623
web/package-lock.json generated
View file

@ -34,7 +34,9 @@
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.1",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^16.1.2",
"postcss": "^8.5.5",
"prettier": "^3.5.3",
"puppeteer": "^21.0.0",
@ -3448,6 +3450,93 @@
"dev": true,
"license": "MIT"
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
"dev": true,
"license": "MIT",
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/cli-truncate/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/cli-truncate/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -3557,6 +3646,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -4031,6 +4127,19 @@
"node": ">=6"
}
},
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -4524,6 +4633,13 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true,
"license": "MIT"
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -5039,6 +5155,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -5387,6 +5516,22 @@
"node": ">=10.17.0"
}
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -6519,6 +6664,185 @@
"dev": true,
"license": "MIT"
},
"node_modules/lint-staged": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz",
"integrity": "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^5.4.1",
"commander": "^14.0.0",
"debug": "^4.4.1",
"lilconfig": "^3.1.3",
"listr2": "^8.3.3",
"micromatch": "^4.0.8",
"nano-spawn": "^1.0.2",
"pidtree": "^0.6.0",
"string-argv": "^0.3.2",
"yaml": "^2.8.0"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/lint-staged/node_modules/chalk": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/lint-staged/node_modules/commander": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz",
"integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/lint-staged/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/lint-staged/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/listr2": {
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz",
"integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
"log-update": "^6.1.0",
"rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/listr2/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/listr2/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/listr2/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/listr2/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/listr2/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/listr2/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/lit": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz",
@ -6584,6 +6908,160 @@
"dev": true,
"license": "MIT"
},
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-escapes": "^7.0.0",
"cli-cursor": "^5.0.0",
"slice-ansi": "^7.1.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-escapes": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
"integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/log-update/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/log-update/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
"integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
"integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@ -6750,6 +7228,19 @@
"node": ">=6"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -6811,6 +7302,19 @@
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
"license": "MIT"
},
"node_modules/nano-spawn": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz",
"integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -7278,6 +7782,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pidtree": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dev": true,
"license": "MIT",
"bin": {
"pidtree": "bin/pidtree.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@ -7901,6 +8418,52 @@
"node": ">=10"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor/node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@ -7912,6 +8475,13 @@
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true,
"license": "MIT"
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -8195,6 +8765,49 @@
"node": ">=8"
}
},
"node_modules/slice-ansi": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -8352,6 +8965,16 @@
"bare-events": "^2.2.0"
}
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",

View file

@ -22,7 +22,8 @@
"lint": "eslint 'src/**/*.{ts,tsx}'",
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
"format": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,md}'",
"format:check": "prettier --check 'src/**/*.{ts,tsx,js,jsx,json,css,md}'"
"format:check": "prettier --check 'src/**/*.{ts,tsx,js,jsx,json,css,md}'",
"pre-commit": "./scripts/pre-commit-check.sh"
},
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
@ -50,7 +51,9 @@
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.1",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^16.1.2",
"postcss": "^8.5.5",
"prettier": "^3.5.3",
"puppeteer": "^21.0.0",

50
web/scripts/pre-commit-check.sh Executable file
View file

@ -0,0 +1,50 @@
#!/bin/bash
# VibeTunnel Pre-commit Check Script
# Can be run manually: npm run pre-commit
set -e
echo "🔍 Running pre-commit checks..."
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "❌ Not in a git repository"
exit 1
fi
# Get staged files or all TypeScript files if not in a commit
if git diff --cached --quiet; then
echo "📁 No staged files, checking all TypeScript files..."
TS_FILES=$(find src -name "*.ts" -o -name "*.tsx" | tr '\n' ' ')
else
echo "📁 Checking staged TypeScript files..."
TS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx)$' | tr '\n' ' ')
fi
if [ -z "$TS_FILES" ]; then
echo "✅ No TypeScript files to check"
exit 0
fi
echo "Files to check: $TS_FILES"
# Run ESLint
echo "🔧 Running ESLint..."
npm run lint
if [ $? -ne 0 ]; then
echo "❌ ESLint failed. Run 'npm run lint:fix' to auto-fix issues."
exit 1
fi
# Run Prettier check
echo "✨ Checking Prettier formatting..."
npm run format:check
if [ $? -ne 0 ]; then
echo "❌ Prettier formatting issues found. Run 'npm run format' to fix."
exit 1
fi
echo "✅ All checks passed!"

View file

@ -45,39 +45,57 @@ export class AppHeader extends LitElement {
<div class="flex flex-col gap-3 sm:hidden">
<!-- Centered VibeTunnel title -->
<div class="text-vs-user font-mono text-sm text-center">-=[ VibeTunnel ]=-</div>
<!-- Controls row: hide exited on left, buttons on right -->
<div class="flex items-center justify-between">
<label class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors">
<label
class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors"
>
<div class="relative">
<input
type="checkbox"
class="sr-only"
.checked=${this.hideExited}
@change=${(e: Event) => this.dispatchEvent(new CustomEvent('hide-exited-change', { detail: (e.target as HTMLInputElement).checked }))}
@change=${(e: Event) =>
this.dispatchEvent(
new CustomEvent('hide-exited-change', {
detail: (e.target as HTMLInputElement).checked,
})
)}
/>
<div
class="w-4 h-4 border border-vs-border rounded bg-vs-bg-secondary flex items-center justify-center transition-all ${this
.hideExited
? 'bg-vs-user border-vs-user'
: 'hover:border-vs-accent'}"
>
<div class="w-4 h-4 border border-vs-border rounded bg-vs-bg-secondary flex items-center justify-center transition-all ${
this.hideExited ? 'bg-vs-user border-vs-user' : 'hover:border-vs-accent'
}">
${this.hideExited ? html`
<svg class="w-3 h-3 text-vs-bg" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
` : ''}
${this.hideExited
? html`
<svg class="w-3 h-3 text-vs-bg" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
></path>
</svg>
`
: ''}
</div>
</div>
hide exited
</label>
<div class="flex gap-1">
${runningSessions.length > 0 && !this.killingAll ? html`
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-2 py-1.5 border-none rounded transition-colors text-xs whitespace-nowrap"
@click=${this.handleKillAll}
>
KILL (${runningSessions.length})
</button>
` : ''}
${runningSessions.length > 0 && !this.killingAll
? html`
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-2 py-1.5 border-none rounded transition-colors text-xs whitespace-nowrap"
@click=${this.handleKillAll}
>
KILL (${runningSessions.length})
</button>
`
: ''}
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-2 py-1.5 border-none rounded transition-colors text-xs whitespace-nowrap"
@click=${this.handleCreateSession}
@ -87,12 +105,14 @@ export class AppHeader extends LitElement {
</div>
</div>
</div>
<!-- Desktop layout: single row -->
<div class="hidden sm:flex sm:items-center sm:justify-between">
<div class="text-vs-user font-mono text-sm">-=[ VibeTunnel ]=-</div>
<div class="flex items-center gap-3">
<label class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors">
<label
class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors"
>
<div class="relative">
<input
type="checkbox"

View file

@ -54,7 +54,7 @@ export class SessionCard extends LitElement {
if (!playerElement) return;
// Create single renderer for this card
this.renderer = new Renderer(playerElement, 80, 24, 10000, 4, false);
this.renderer = new Renderer(playerElement, 80, 24, 10000, 4);
// Always use snapshot endpoint for cards
const url = `/api/sessions/${this.session.id}/snapshot`;

View file

@ -187,10 +187,16 @@ export class SessionView extends LitElement {
}, delay);
// Listen for session exit events
terminalElement.addEventListener('session-exit', this.handleSessionExit.bind(this) as EventListener);
terminalElement.addEventListener(
'session-exit',
this.handleSessionExit.bind(this) as EventListener
);
// Listen for terminal resize events to capture dimensions
terminalElement.addEventListener('terminal-resize', this.handleTerminalResize.bind(this) as EventListener);
terminalElement.addEventListener(
'terminal-resize',
this.handleTerminalResize.bind(this) as EventListener
);
}
private async handleKeyboardInput(e: KeyboardEvent) {
@ -383,14 +389,16 @@ export class SessionView extends LitElement {
controls.style.transition = 'transform 0.3s ease';
// Calculate available space to match closed keyboard layout
const header = this.querySelector('.flex.items-center.justify-between.p-4.border-b') as HTMLElement;
const header = this.querySelector(
'.flex.items-center.justify-between.p-4.border-b'
) as HTMLElement;
const headerHeight = header?.offsetHeight || 60;
const controlsHeight = controls?.offsetHeight || 120;
// Calculate exact space to maintain same gap as when keyboard is closed
const availableHeight = viewportHeight - headerHeight - controlsHeight;
const inputArea = textarea.parentElement as HTMLElement;
if (inputArea && availableHeight > 0) {
// Set the input area to exactly fill the space, maintaining natural flex behavior
inputArea.style.height = `${availableHeight}px`;
@ -717,11 +725,16 @@ export class SessionView extends LitElement {
<span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
${this.session.status.toUpperCase()}
</span>
${this.terminalCols > 0 && this.terminalRows > 0 ? html`
<span class="text-vs-muted text-xs opacity-60" style="font-size: 10px; line-height: 1;">
${this.terminalCols}×${this.terminalRows}
</span>
` : ''}
${this.terminalCols > 0 && this.terminalRows > 0
? html`
<span
class="text-vs-muted text-xs opacity-60"
style="font-size: 10px; line-height: 1;"
>
${this.terminalCols}×${this.terminalRows}
</span>
`
: ''}
</div>
</div>

View file

@ -160,7 +160,7 @@ export class Renderer {
parseCastFile(content: string): void {
const lines = content.trim().split('\n');
let outputEvents: string[] = [];
const outputEvents: string[] = [];
// Clear terminal
this.terminal.clear();
@ -194,7 +194,7 @@ export class Renderer {
console.warn('Failed to parse cast line:', line);
}
}
// Write all output at once, then scroll when rendering is complete
if (outputEvents.length > 0) {
const allOutput = outputEvents.join('');
@ -236,11 +236,11 @@ export class Renderer {
this.terminal.resize(width, height);
// Always use ScaleFitAddon for consistent scaling behavior
this.scaleFitAddon.fit();
// Emit custom event with terminal dimensions
const event = new CustomEvent('terminal-resize', {
detail: { cols: width, rows: height },
bubbles: true
bubbles: true,
});
this.container.dispatchEvent(event);
}

View file

@ -8,18 +8,22 @@
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url('/fonts/FiraCode-VF.woff2') format('woff2-variations'),
url('/fonts/FiraCode-VF.ttf') format('truetype-variations');
src:
url('/fonts/FiraCode-VF.woff2') format('woff2-variations'),
url('/fonts/FiraCode-VF.ttf') format('truetype-variations');
font-variation-settings: 'wght' 400;
}
/* Override Tailwind's font-mono to use Fira Code */
.font-mono {
font-family: 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-family:
'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo,
monospace !important;
}
/* Mobile scroll and touch behavior fixes */
html, body {
html,
body {
overscroll-behavior: none;
touch-action: pan-x pan-y;
-webkit-overflow-scrolling: touch;
@ -76,7 +80,9 @@ body {
/* XTerm terminal styling */
.xterm {
padding: 0 !important;
font-family: 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-family:
'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo,
monospace !important;
}
.xterm .xterm-viewport {