webtun 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,237 +1,139 @@
1
- # WebTun — Self-Hosted Web Terminal
2
-
3
- ![Terminal](https://img.shields.io/badge/Terminal-Web--Native-2D5B8E?style=for-the-badge)
4
- ![PWA](https://img.shields.io/badge/PWA-Installable-6BA428?style=for-the-badge)
5
- ![Cloudflare](https://img.shields.io/badge/Tunnel-Cloudflare-F38020?style=for-the-badge)
6
- ![Desktop](https://img.shields.io/badge/Desktop-Electron-47848F?style=for-the-badge)
7
- ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)
8
-
9
- **Access your server from any browser — no VPN, no SSH client, no installing anything.**
10
-
11
- [![Quick Start](https://img.shields.io/badge/Quick_Start-One_Command-2D5B8E?style=for-the-badge&logo=gnu-bash)](install.sh)
12
- [![Download](https://img.shields.io/github/v/release/unn-known1/webtun?style=for-the-badge&logo=github)](https://github.com/unn-known1/webtun/releases/latest)
13
-
14
- ---
15
-
16
- ## Why WebTun?
17
-
18
- | Traditional SSH | WebTun |
19
- |----------------|--------|
20
- | Need SSH client installed | Open any browser |
21
- | Configure VPN or port forwarding | Cloudflare Tunnel auto-configured |
22
- | Can't access from phone easily | PWA works on iOS/Android |
23
- | Share access requires key exchange | Web-based sharing in 1 click |
24
- | Corporate firewall blocks port 22 | Runs over HTTPS (port 443) |
25
-
26
- ---
27
-
28
- ## Features
1
+ <p align="center">
2
+ <img src="public/icon.svg" width="80" height="80" alt="WebTun">
3
+ </p>
29
4
 
30
- ### Core
31
- - **Real shell sessions** — node-pty backed, full bash/zsh support
32
- - **Multi-tab terminal** — drag tabs to reorder, side-by-side sessions like your desktop
33
- - **File explorer** — browse, upload, download, rename, delete, cut/copy/paste with conflict resolution (replace, merge, keep both, skip)
34
- - **WebSocket + xterm.js** — responsive, low-latency typing
35
- - **Bracketed paste** — Ctrl+V works correctly in TUI apps (vim, nano, htop, mc)
36
- - **Command history** — automatic capture with smart dedup, works with pasted commands
37
-
38
- ### Mobile-First UX
39
- - **Touch gestures** — swipe to close tabs, swipe from edge to open sidebar, pull-to-refresh file list
40
- - **Mobile keyboard bar** — ESC, Tab, arrow keys, Ctrl combos (Ctrl+C, Ctrl+D, etc.)
41
- - **Virtual keyboard aware** — terminal resizes when keyboard opens, no overlap
42
- - **Pinch-to-zoom** — adjust terminal font size (10–28px) with two fingers
43
- - **Selection mode** — toggle between terminal interaction and text selection
44
- - **Responsive** — optimized for phones (480px), tablets (1024px), and landscape orientation
45
-
46
- ### Settings
47
- - **Right-click menu toggle** — enable/disable custom terminal context menu (Settings > Terminal)
48
- - **Terminal scroll** — reliable mouse wheel scroll in all apps, including TUI apps with mouse tracking
5
+ <h1 align="center">WebTun</h1>
49
6
 
50
- ### File Explorer
51
- - **Multi-select** select all / deselect all, batch delete, download, zip, cut, and copy files
52
- - **Context menu on selection**right-click actions (zip, download, delete, cut, copy) operate on all selected files
53
- - **Zip / Extract** — right-click any file or folder to zip; right-click `.zip` files to extract (archiver v8)
54
- - **Folder download** — folders download as `.zip` archives automatically
55
- - **Image viewer** — click images (.png, .jpg, .gif, .svg, .webp, .bmp, .ico) to preview inline
56
- - **Cut, Copy, Paste** — multi-file support with conflict resolution dialog (replace, merge folders, keep both, skip, cancel)
57
- - **Upload with progress** — per-file progress bar via XHR
58
- - **Breadcrumb navigation** — tappable directory segments for quick navigation
59
- - **Long-press context menu** — rename, delete, download, copy on touch devices
60
- - **Text selection** — long-press file names to select and copy text on touch devices
7
+ <p align="center">
8
+ <strong>Self-hosted web terminal with file explorer, Cloudflare Tunnel, and PWA support</strong><br>
9
+ Access your server from any browser no VPN, no SSH client, no installing anything.
10
+ </p>
61
11
 
62
- ### System Stats
63
- - **CPU, Memory, Disk, Uptime** — real-time usage with progress bars
64
- - **GPU detection** — cross-platform (Linux: nvidia-smi/lspci, macOS: system_profiler, Windows: WMI)
65
- - **Multi-GPU support** — detects and displays all GPUs with VRAM, utilization, and temperature
66
- - **Top processes** — sorted by CPU usage
67
-
68
- ### Desktop App (Electron)
69
- - Cross-platform app for Linux & Windows
70
- - Runs the server as a child process — no terminal needed
71
- - Native window with system tray, copy/paste, and file dialogs
72
- - Download the latest release from [GitHub Releases](https://github.com/unn-known1/webtun/releases/latest)
73
- - Or build from source: `npm run dist:linux` / `npm run dist:win`
74
-
75
- ### PWA (Install as App)
76
- - Add to iOS/Android home screen — looks and feels like native app
77
- - Install banner prompts on first visit (Chrome) with iOS instructions fallback
78
- - Works offline (shows last session state)
79
- - Push notifications for tunnel status
80
-
81
- ### Cloudflare Tunnel (Zero-Config)
82
- - Create tunnel from UI — no CLI commands
83
- - Built-in health check warns if local server is unreachable before creating tunnel
84
- - Live/orphan status + target responsiveness indicator in tunnel list
85
- - Get public HTTPS URL instantly
86
- - Tunnels survive server restarts
87
- - Stop/kill tunnels from settings panel
12
+ <p align="center">
13
+ <a href="https://github.com/unn-known1/webtun/releases/latest"><img src="https://img.shields.io/github/v/release/unn-known1/webtun?style=flat-square&logo=github" alt="Release"></a>
14
+ <img src="https://img.shields.io/badge/Terminal-xterm.js-2D5B8E?style=flat-square" alt="Terminal">
15
+ <img src="https://img.shields.io/badge/PWA-Installable-6BA428?style=flat-square" alt="PWA">
16
+ <img src="https://img.shields.io/badge/Tunnel-Cloudflare-F38020?style=flat-square" alt="Tunnel">
17
+ <img src="https://img.shields.io/badge/Desktop-Electron-47848F?style=flat-square" alt="Electron">
18
+ <img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
19
+ </p>
88
20
 
89
21
  ---
90
22
 
91
23
  ## Quick Start
92
24
 
93
- ### npm (Recommended)
94
25
  ```bash
95
26
  npx webtun
96
27
  ```
97
28
 
98
- Install globally:
99
- ```bash
100
- npm install -g webtun
101
- webtun
102
- ```
29
+ That's it. Open `http://localhost:3000` in your browser.
103
30
 
104
- **Options:**
105
- | Flag | Description |
106
- |------|-------------|
107
- | `--port, -p` | Port (default: 3000) |
108
- | `--host, -h` | Host (default: 0.0.0.0) |
109
- | `--pin` | Authentication PIN |
110
- | `--tunnel, -t` | Start Cloudflare Tunnel |
111
- | `--help` | Show help |
112
- | `--version` | Show version |
113
-
114
- **Environment Variables:**
115
- | Variable | Description |
116
- |----------|-------------|
117
- | `PORT` | Server port (default: 3000) |
118
- | `HOST` | Bind address (default: 0.0.0.0) |
119
- | `PIN` | Authentication PIN (empty = no auth) |
120
- | `SHELL` | Shell to use (default: PowerShell on Windows, bash/sh elsewhere) |
121
- | `WORKSPACE_ROOT` | Root directory for file operations (default: ~) |
122
- | `TRUST_PROXY` | Set to `true` if behind a reverse proxy (default: loopback only) |
123
- | `WEBTUN_SHELL` | Override shell on Windows (e.g., `/usr/bin/bash` for Git Bash) |
124
-
125
- **Examples:**
126
31
  ```bash
127
- npx webtun # Start on port 3000
128
- npx webtun --port 8080 # Custom port
129
32
  npx webtun --pin secret123 # With PIN protection
130
- npx webtun --tunnel # With Cloudflare Tunnel
131
- npx webtun -p 4000 -t # Port 4000 + tunnel
33
+ npx webtun --tunnel # With Cloudflare Tunnel (public URL)
34
+ npx webtun -p 8080 -t # Custom port + tunnel
35
+ npx webtun --help # Show all options
36
+ npx webtun --version # Show version
132
37
  ```
133
38
 
134
- **Note:** npm 12+ blocks install scripts by default. Allow them for `node-pty` (native module):
135
- ```bash
136
- # Allow scripts once during install
137
- npm install -g --allow-scripts=webtun,node-pty webtun
39
+ ### Install globally
138
40
 
139
- # Or allow globally (one-time setup)
140
- npm config set allow-scripts=webtun,node-pty --location=user
41
+ ```bash
141
42
  npm install -g webtun
43
+ webtun
142
44
  ```
143
45
 
144
- Build tools are also required (for compiling native modules):
145
- ```bash
146
- # Debian/Ubuntu
147
- sudo apt-get install -y python3 make g++
46
+ > **npm 12+ note:** Install scripts are blocked by default. Allow them for `node-pty`:
47
+ > ```bash
48
+ > npm install -g --allow-scripts=webtun,node-pty webtun
49
+ > ```
50
+ > Build tools required for native modules: `sudo apt-get install -y python3 make g++` (Debian/Ubuntu) or `xcode-select --install` (macOS).
148
51
 
149
- # macOS
150
- xcode-select --install
151
- ```
52
+ ### Other install methods
152
53
 
153
- ### One-Command Install
154
54
  ```bash
55
+ # One-command install
155
56
  bash -c "$(curl -fsSL https://raw.githubusercontent.com/unn-Known1/webtun/main/install.sh)"
156
- ```
157
57
 
158
- ### Manual Setup
159
- ```bash
160
- git clone https://github.com/unn-Known1/webtun.git
161
- cd webtun
162
- chmod +x setup.sh && ./setup.sh
163
- npm start
164
- ```
165
-
166
- ### Google Colab (Instant Terminal)
167
- ```python
168
- !rm -rf webtun && git clone https://github.com/unn-Known1/webtun.git && cd webtun && npm install --loglevel=error && node server.js > /tmp/webtun.log 2>&1 & sleep 4 && for i in 1 2 3; do curl -sf http://localhost:3000/api/auth/required >/dev/null && break; sleep 2; done && curl -s -X POST http://localhost:3000/api/tunnel -H 'Content-Type: application/json' -d '{"url":"http://localhost:3000"}' --max-time 20 | python3 -c "import sys,json; d=json.load(sys.stdin); print('🌐 WebTun ready at:', d.get('url','Error: '+d.get('error','')))"
58
+ # Manual setup
59
+ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh && npm start
169
60
  ```
170
61
 
171
62
  ---
172
63
 
173
- ## Architecture
174
-
175
- ```
176
- ┌────────────────────────────────────────────────────────────────┐
177
- │ Your Browser │
178
- │ ┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐ │
179
- │ │ Terminal │ │ File │ │ Tunnel │ │ Settings │ │
180
- │ │ (xterm) │ │ Explorer │ │ Manager │ │ Panel │ │
181
- │ └─────┬────┘ └─────┬─────┘ └──────┬──────┘ └─────┬─────┘ │
182
- │ │ │ │ │ │
183
- └────────┼─────────────┼───────────────┼───────────────┼─────────┘
184
- │ WebSocket │ REST API │ │
185
- ▼ ▼ ▼ │
186
- ┌────────────────────────────────────────────────────────────────┐
187
- │ Node.js Server │
188
- │ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
189
- │ │ WebSocket │ │ File API │ │ Cloudflare Tunnel │ │
190
- │ │ Handler │ │ (read/write) │ │ Manager │ │
191
- │ └──────┬──────┘ └──────┬───────┘ └───────────┬───────────┘ │
192
- │ │ │ │ │
193
- │ ▼ ▼ ▼ │
194
- │ ┌──────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
195
- │ │ node-pty │ │ Local Filesystem │ │ cloudflared daemon │ │
196
- │ │ (shell) │ │ (your server) │ │ (exposes to internet)│ │
197
- │ └──────────┘ └──────────────────┘ └──────────────────────┘ │
198
- └────────────────────────────────────────────────────────────────┘
199
-
200
-
201
- ┌─────────────────┐
202
- │ Internet Users │
203
- │ (HTTPS URL) │
204
- └─────────────────┘
205
- ```
64
+ ## Features
206
65
 
207
- ---
66
+ ### Terminal
67
+ - **Real shell sessions** — node-pty backed, full bash/zsh/PowerShell support
68
+ - **Multi-tab** — drag to reorder, side-by-side sessions
69
+ - **Bracketed paste** — Ctrl+V works in TUI apps (vim, nano, htop)
70
+ - **Command history** — keystroke-based capture, strips ANSI/control sequences
71
+ - **tmux sessions** — persistent sessions survive page reload
208
72
 
209
- ## Use Cases
73
+ ### File Explorer
74
+ - Browse, upload, download, rename, delete files
75
+ - **Multi-select** — batch delete, download, zip, cut, copy
76
+ - **Zip/Extract** — right-click any file or folder
77
+ - **Image viewer** — preview inline (png, jpg, gif, svg, webp)
78
+ - **Conflict resolution** — replace, merge, keep both, skip
79
+ - **Breadcrumb navigation** — tappable directory segments
80
+
81
+ ### Mobile
82
+ - **Touch gestures** — swipe to close tabs, pull-to-refresh
83
+ - **Mobile keyboard bar** — ESC, Tab, arrows, Ctrl combos
84
+ - **Pinch-to-zoom** — adjust font size with two fingers
85
+ - **Virtual keyboard aware** — no overlap when keyboard opens
86
+ - **Selection mode** — toggle between interaction and text selection
87
+
88
+ ### Cloudflare Tunnel
89
+ - Create tunnels from the UI — zero CLI commands
90
+ - Built-in health check before tunnel creation
91
+ - Live status indicators, tunnels survive restarts
92
+ - Stop/kill tunnels from settings panel
210
93
 
211
- | Scenario | Why WebTun |
212
- |----------|------------|
213
- | **Home server access** | Access from anywhere without opening ports |
214
- | **Emergency debugging** | Phone/laptop without SSH client |
215
- | **Share terminal with friend** | No key exchange, just send URL |
216
- | **Colab power-up** | Persistent terminal alongside Python notebooks |
217
- | **Demo environment** | Spin up temp shell for presentations |
218
- | **Corporate restrictions** | HTTPS works where SSH is blocked |
94
+ ### System Stats
95
+ - CPU, Memory, Disk, Uptime — real-time with progress bars
96
+ - GPU detection (nvidia-smi, system_profiler, WMI)
97
+ - Multi-GPU support with VRAM, utilization, temperature
98
+ - Top processes sorted by CPU
99
+
100
+ ### Desktop (Electron)
101
+ - Cross-platform app for Linux, Windows, macOS
102
+ - Native window with system tray and file dialogs
103
+ - Build: `npm run dist:linux` / `npm run dist:win` / `npm run dist:mac`
104
+ - [Download latest release](https://github.com/unn-known1/webtun/releases/latest)
105
+
106
+ ### PWA
107
+ - Install to iOS/Android home screen
108
+ - Works offline with last session state
109
+ - Install banner with iOS instructions fallback
219
110
 
220
111
  ---
221
112
 
222
- ## Tunnel Manager
113
+ ## Configuration
223
114
 
224
- 1. Open **Settings** (gear icon)
225
- 2. Go to **Tunnel** section
226
- 3. Enter URL (default: `http://localhost:3000`)
227
- 4. Click **Create** — a health check pings the target first, warns if unreachable
228
- 5. Copy the public URL — share it with anyone
229
- 6. View all tunnels with live status indicators (green/orange)
115
+ ### CLI Flags
230
116
 
231
- **Note:** Tunnels created before a server restart need manual cleanup:
232
- ```bash
233
- kill $(pgrep -f 'cloudflared tunnel')
234
- ```
117
+ | Flag | Description | Default |
118
+ |------|-------------|---------|
119
+ | `--port, -p` | Server port | `3000` |
120
+ | `--host, -h` | Bind address | `0.0.0.0` |
121
+ | `--pin` | Authentication PIN | none |
122
+ | `--tunnel, -t` | Start Cloudflare Tunnel | off |
123
+ | `--help` | Show help | — |
124
+ | `--version` | Show version | — |
125
+
126
+ ### Environment Variables
127
+
128
+ | Variable | Description | Default |
129
+ |----------|-------------|---------|
130
+ | `PORT` | Server port | `3000` |
131
+ | `HOST` | Bind address | `0.0.0.0` |
132
+ | `PIN` | Auth PIN (empty = no auth) | none |
133
+ | `SHELL` | Shell to use | platform default |
134
+ | `WORKSPACE_ROOT` | File explorer root | `~` |
135
+ | `TRUST_PROXY` | Trust `X-Forwarded-For` from first proxy | `false` |
136
+ | `WEBTUN_SHELL` | Override shell on Windows | PowerShell |
235
137
 
236
138
  ---
237
139
 
@@ -239,10 +141,8 @@ kill $(pgrep -f 'cloudflared tunnel')
239
141
 
240
142
  | Layer | Technology |
241
143
  |-------|------------|
242
- | Frontend | HTML + Vanilla JS (PWA, prefers-color-scheme, visualViewport, Drag & Drop) |
243
- | Backend | Node.js + node-pty + Express |
244
- | Terminal | xterm.js + fit addon |
245
- | Protocol | WebSocket (ws) |
144
+ | Frontend | Vanilla JS, xterm.js, CodeMirror, PWA |
145
+ | Backend | Node.js, Express, node-pty |
246
146
  | Tunnel | Cloudflare Tunnel (cloudflared) |
247
147
  | Desktop | Electron + electron-builder |
248
148
  | Archives | archiver v8 + yauzl |
@@ -251,11 +151,11 @@ kill $(pgrep -f 'cloudflared tunnel')
251
151
 
252
152
  ## Security
253
153
 
254
- - **npm audit clean** 0 known vulnerabilities
255
- - **PIN authentication** optional, protects all API endpoints
256
- - **CSP headers** restricts script sources to CDN
257
- - **WebSocket origin check** prevents cross-site WebSocket hijacking
258
- - **Rate limiting**per-IP rate limits on auth and search endpoints
154
+ - PIN authentication on all API endpoints (`x-pin-token` header or `?token=` query)
155
+ - CSP headers restrict script sources to CDN
156
+ - WebSocket origin check prevents cross-site hijacking
157
+ - Per-IP rate limiting on auth and search endpoints
158
+ - npm audit clean 0 known vulnerabilities
259
159
 
260
160
  ---
261
161
 
@@ -263,31 +163,31 @@ kill $(pgrep -f 'cloudflared tunnel')
263
163
 
264
164
  | Problem | Solution |
265
165
  |---------|----------|
266
- | Tunnel URL not loading | Check Cloudflare account quota at [dash.cloudflare.com](https://dash.cloudflare.com) |
267
- | Permission denied on shell | Ensure user has shell access: `chsh -s /bin/bash` |
268
- | File upload fails | Check `public/uploads/` permissions: `chmod 755 public/uploads/` |
269
- | Port 3000 in use | Change port: `PORT=3001 npm start` |
270
- | Windows terminal opens PowerShell instead of Git Bash | Set `WEBTUN_SHELL=/usr/bin/bash` in `.env` |
271
- | Behind reverse proxy, wrong client IP | Set `TRUST_PROXY=true` in `.env` |
272
- | Tunnel persists after server restart | Tunnels are auto-managed; manual cleanup: `kill $(pgrep -f 'cloudflared tunnel')` |
166
+ | Tunnel not loading | Check quota at [dash.cloudflare.com](https://dash.cloudflare.com) |
167
+ | Port 3000 in use | `PORT=3001 npm start` |
168
+ | Shell permission denied | `chsh -s /bin/bash` |
169
+ | Behind reverse proxy | Set `TRUST_PROXY=true` in `.env` |
170
+ | Windows shell wrong | Set `WEBTUN_SHELL=/usr/bin/bash` in `.env` |
171
+ | Tunnel persists after restart | `kill $(pgrep -f 'cloudflared tunnel')` |
273
172
 
274
173
  ---
275
174
 
276
- ## Contributing
277
-
278
- 1. Fork → Branch → Commit → PR
279
- 2. Follow existing code style (ES6+, no frameworks)
280
- 3. Test locally with `npm start`
281
- 4. Update this README if adding features
282
-
283
- ---
175
+ ## Changelog
284
176
 
285
- ## License
177
+ ### v1.5.1
178
+ - Fixed command history saving terminal garbage instead of actual commands
179
+ - Fixed history 401 errors on Cloudflare tunnel URLs
180
+ - Fixed VT/ANSI parameter sequences leaking into history
181
+ - Fixed CodeMirror `defineSimpleMode` error
182
+ - Fixed console accessibility warnings (labels, form fields)
183
+ - Added safe `localStorage` wrapper for Tracking Prevention compatibility
184
+ - New terminal icon matching in-app logo
286
185
 
287
- MIT — do whatever you want with it.
186
+ ### v1.5.0
187
+ - Initial release
288
188
 
289
189
  ---
290
190
 
291
191
  <p align="center">
292
- <sub>Made with ❤️ by <a href="https://github.com/unn-known1">Gaurang Patel</a></sub>
192
+ <sub>Built by <a href="https://github.com/unn-known1">Gaurang Patel</a> · MIT License</sub>
293
193
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webtun",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Self-hosted web terminal with Cloudflare Tunnel, file explorer, and PWA support",
5
5
  "author": {
6
6
  "name": "Gaurang Patel",
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#c0caf5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"></rect><line x1="6" y1="8" x2="10" y2="8"></line><line x1="6" y1="12" x2="10" y2="12"></line><line x1="12" y1="8" x2="18" y2="8"></line><line x1="12" y1="12" x2="18" y2="12"></line></svg>
package/public/index.html CHANGED
@@ -4,9 +4,11 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
6
  <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="mobile-web-app-capable" content="yes">
7
8
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
8
9
  <meta name="theme-color" content="#1a1b26">
9
10
  <title>WebTun</title>
11
+ <link rel="icon" href="/favicon.png" type="image/png">
10
12
  <link rel="manifest" href="/manifest.json">
11
13
  <link rel="apple-touch-icon" href="/icon-192.png">
12
14
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
@@ -32,6 +34,7 @@
32
34
  <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/yaml/yaml.min.js"></script>
33
35
  <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/sql/sql.min.js"></script>
34
36
  <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/go/go.min.js"></script>
37
+ <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/addon/mode/simple.min.js"></script>
35
38
  <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/rust/rust.min.js"></script>
36
39
  <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/properties/properties.min.js"></script>
37
40
  <style>
@@ -857,9 +860,15 @@ svg { display: block; }
857
860
  </head>
858
861
  <body data-theme="light">
859
862
  <script>
863
+ // Safe localStorage wrapper — degrades gracefully when Tracking Prevention blocks storage
864
+ const safeStorage = {
865
+ getItem(k) { try { return localStorage.getItem(k); } catch(e) { return null; } },
866
+ setItem(k, v) { try { localStorage.setItem(k, v); } catch(e) {} },
867
+ removeItem(k) { try { localStorage.removeItem(k); } catch(e) {} }
868
+ };
860
869
  (function() {
861
870
  try {
862
- const settings = JSON.parse(localStorage.getItem('wt-settings')) || {};
871
+ const settings = JSON.parse(safeStorage.getItem('wt-settings')) || {};
863
872
  let theme = settings.theme || 'light';
864
873
  if (theme === 'system') {
865
874
  theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dracula' : 'light';
@@ -1061,7 +1070,7 @@ svg { display: block; }
1061
1070
  </div>
1062
1071
  <div id="editor-area">
1063
1072
  <textarea id="editor-textarea" spellcheck="false"></textarea>
1064
- <div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-same-origin"></iframe></div>
1073
+ <div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-same-origin allow-scripts"></iframe></div>
1065
1074
  </div>
1066
1075
  </div>
1067
1076
  </div>
@@ -1143,7 +1152,7 @@ svg { display: block; }
1143
1152
  <div class="settings-section">
1144
1153
  <h3>Appearance</h3>
1145
1154
  <div class="setting-row">
1146
- <label>Theme</label>
1155
+ <label for="s-theme">Theme</label>
1147
1156
  <select id="s-theme" onchange="applyTheme(this.value)">
1148
1157
  <option value="system">System</option>
1149
1158
  <option value="light">Light</option>
@@ -1155,11 +1164,11 @@ svg { display: block; }
1155
1164
  </select>
1156
1165
  </div>
1157
1166
  <div class="setting-row">
1158
- <label>Font Size</label>
1167
+ <label for="s-fontsize">Font Size</label>
1159
1168
  <input type="number" id="s-fontsize" min="8" max="32" value="14" onchange="applyFontSize(+this.value)">
1160
1169
  </div>
1161
1170
  <div class="setting-row">
1162
- <label>Font Family</label>
1171
+ <label for="s-font">Font Family</label>
1163
1172
  <select id="s-font" onchange="applyFont(this.value)">
1164
1173
  <option value="'SF Mono', 'Fira Code', Consolas, monospace">System Mono</option>
1165
1174
  <option value="'Fira Code', monospace">Fira Code</option>
@@ -1169,7 +1178,7 @@ svg { display: block; }
1169
1178
  </select>
1170
1179
  </div>
1171
1180
  <div class="setting-row">
1172
- <label>Cursor Style</label>
1181
+ <label for="s-cursor">Cursor Style</label>
1173
1182
  <select id="s-cursor" onchange="applyCursor(this.value)">
1174
1183
  <option value="block">Block</option>
1175
1184
  <option value="underline">Underline</option>
@@ -1177,48 +1186,48 @@ svg { display: block; }
1177
1186
  </select>
1178
1187
  </div>
1179
1188
  <div class="setting-row">
1180
- <label>Cursor Blink</label>
1181
- <div class="toggle" id="s-blink" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('blink')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('blink')}"></div>
1189
+ <label for="s-blink-cb">Cursor Blink</label>
1190
+ <div class="toggle" id="s-blink" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('blink')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('blink')}"><input type="checkbox" id="s-blink-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
1182
1191
  </div>
1183
1192
  </div>
1184
1193
  <div class="settings-section">
1185
1194
  <h3>Terminal</h3>
1186
1195
  <div class="setting-row">
1187
- <label>Scrollback Lines</label>
1196
+ <label for="s-scrollback">Scrollback Lines</label>
1188
1197
  <input type="number" id="s-scrollback" min="100" max="100000" value="5000" onchange="applyScrollback(+this.value)">
1189
1198
  </div>
1190
1199
  <div class="setting-row">
1191
- <label>Bell Sound</label>
1192
- <div class="toggle" id="s-bell" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('bell')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('bell')}"></div>
1200
+ <label for="s-bell-cb">Bell Sound</label>
1201
+ <div class="toggle" id="s-bell" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('bell')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('bell')}"><input type="checkbox" id="s-bell-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
1193
1202
  </div>
1194
1203
  <div class="setting-row">
1195
- <label>Right-Click Menu</label>
1196
- <div class="toggle on" id="s-termRightClick" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('termRightClick')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('termRightClick')}"></div>
1204
+ <label for="s-termRightClick-cb">Right-Click Menu</label>
1205
+ <div class="toggle on" id="s-termRightClick" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('termRightClick')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('termRightClick')}"><input type="checkbox" id="s-termRightClick-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
1197
1206
  </div>
1198
1207
  </div>
1199
1208
  <div class="settings-section">
1200
1209
  <h3>Interface</h3>
1201
1210
  <div class="setting-row">
1202
- <label>Show Mobile Keys</label>
1203
- <div class="toggle" id="s-mobilekeys" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('mobilekeys')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('mobilekeys')}"></div>
1211
+ <label for="s-mobilekeys-cb">Show Mobile Keys</label>
1212
+ <div class="toggle" id="s-mobilekeys" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('mobilekeys')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('mobilekeys')}"><input type="checkbox" id="s-mobilekeys-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
1204
1213
  </div>
1205
1214
  <div class="setting-row">
1206
- <label>Confirm on Close</label>
1207
- <div class="toggle on" id="s-confirmclose" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('confirmclose')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('confirmclose')}"></div>
1215
+ <label for="s-confirmclose-cb">Confirm on Close</label>
1216
+ <div class="toggle on" id="s-confirmclose" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('confirmclose')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('confirmclose')}"><input type="checkbox" id="s-confirmclose-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
1208
1217
  </div>
1209
1218
  <div class="setting-row">
1210
- <label>Data Saver</label>
1211
- <div class="toggle" id="s-datasaver" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('datasaver')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('datasaver')}"></div>
1219
+ <label for="s-datasaver-cb">Data Saver</label>
1220
+ <div class="toggle" id="s-datasaver" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('datasaver')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('datasaver')}"><input type="checkbox" id="s-datasaver-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
1212
1221
  </div>
1213
1222
  <div class="setting-row electron-only" style="display:none">
1214
- <label>Start on Login</label>
1215
- <div class="toggle" id="s-autostart" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('autostart')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('autostart')}"></div>
1223
+ <label for="s-autostart-cb">Start on Login</label>
1224
+ <div class="toggle" id="s-autostart" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('autostart')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('autostart')}"><input type="checkbox" id="s-autostart-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
1216
1225
  </div>
1217
1226
  </div>
1218
1227
  <div class="settings-section">
1219
1228
  <h3>Tunnel</h3>
1220
1229
  <div class="setting-row" style="flex-wrap:wrap;gap:6px">
1221
- <label style="flex:0 0 auto">Local URL</label>
1230
+ <label for="tunnel-url" style="flex:0 0 auto">Local URL</label>
1222
1231
  <div style="display:flex;flex:1;gap:6px;min-width:0">
1223
1232
  <input type="text" id="tunnel-url" value="http://localhost:3000" placeholder="http://localhost:PORT" style="flex:1;min-width:0">
1224
1233
  <button class="btn btn-primary" style="height:28px;padding:0 12px;font-size:12px;flex-shrink:0" onclick="createTunnel()">Create</button>
@@ -1510,12 +1519,12 @@ function preventDoubleTap(el, fn, delay = 1000) {
1510
1519
 
1511
1520
  function saveTabState() {
1512
1521
  const state = tabs.map(t => ({ id: t.id, sessionId: t.sessionId, title: t.title }));
1513
- try { localStorage.setItem('wt-tabs', JSON.stringify(state)); } catch(e) { console.warn('Failed to save tabs:', e); }
1522
+ try { safeStorage.setItem('wt-tabs', JSON.stringify(state)); } catch(e) { console.warn('Failed to save tabs:', e); }
1514
1523
  }
1515
1524
 
1516
1525
  function loadTabState() {
1517
1526
  try {
1518
- const val = JSON.parse(localStorage.getItem('wt-tabs'));
1527
+ const val = JSON.parse(safeStorage.getItem('wt-tabs'));
1519
1528
  return Array.isArray(val) ? val : [];
1520
1529
  } catch(e) { console.warn('loadTabState error:', e); return []; }
1521
1530
  }
@@ -1738,6 +1747,8 @@ function setupTabDragDrop(tabEl, id) {
1738
1747
  function setupTabInlineRename(tabTitleSpan, tab) {
1739
1748
  const startRename = (span) => {
1740
1749
  const input = document.createElement('input');
1750
+ input.id = 'tab-rename-input';
1751
+ input.name = 'tab-rename';
1741
1752
  input.type = 'text';
1742
1753
  input.value = tab.title;
1743
1754
  input.style.cssText = `width:${Math.max(60, tab.title.length * 9)}px;height:22px;font-size:12px;padding:0 6px`;
@@ -1912,7 +1923,7 @@ function connectWebSocket(tab, isReconnect = false) {
1912
1923
  const ch = data.charCodeAt(i);
1913
1924
  if (ch === 13 || ch === 10) { // Enter
1914
1925
  try {
1915
- // Prefer stored paste text (avoids single-row truncation from getLine)
1926
+ // Prefer stored paste text
1916
1927
  let text = tab._lastPasteText;
1917
1928
  tab._lastPasteText = null;
1918
1929
  if (text) {
@@ -1921,30 +1932,13 @@ function connectWebSocket(tab, isReconnect = false) {
1921
1932
  text = lines[lines.length - 1] || lines[lines.length - 2] || text;
1922
1933
  text = text.trim();
1923
1934
  } else {
1924
- // Read from terminal buffer collect all rows from prompt to cursor
1925
- const buf = tab.term.buffer.active;
1926
- const cursorRow = buf.baseY + buf.cursorY;
1927
- let collected = '';
1928
- // Scan upward to find the prompt line (contains $, #, or %)
1929
- for (let r = cursorRow; r >= Math.max(0, cursorRow - 20); r--) {
1930
- const row = buf.getLine(r);
1931
- if (!row) break;
1932
- const rowText = row.translateToString(false);
1933
- // Prompt detection: line ends with $, #, or % followed by space
1934
- if (/\s*[#$%]\s*$/.test(rowText)) {
1935
- const promptEnd = rowText.search(/[#$%]\s*$/);
1936
- if (promptEnd >= 0) {
1937
- collected = rowText.substring(promptEnd + 1) + collected;
1938
- }
1939
- break;
1940
- }
1941
- collected = rowText + collected;
1942
- }
1943
- text = collected.trim();
1944
- // Strip any trailing prompt artifacts from multi-row
1945
- text = text.replace(/\s*[#$%]\s*$/, '').trim();
1935
+ // Use the tracked input buffer (more reliable than buffer scanning)
1936
+ text = (tab._currentInput || '').trim();
1937
+ // Strip any leaked VT/ANSI parameter junk that bypassed the escape parser
1938
+ text = text.replace(/^[>;\d\s]+(?=[a-zA-Z/\\~\-.])/, '').replace(/^[>;\d\s]+$/, '').trim();
1946
1939
  }
1947
1940
  if (text) addToCmdHist(text);
1941
+ tab._currentInput = ''; // Clear after history save
1948
1942
  } catch {}
1949
1943
  }
1950
1944
  }
@@ -1952,6 +1946,27 @@ function connectWebSocket(tab, isReconnect = false) {
1952
1946
  const buf = new Uint8Array(1 + enc.length);
1953
1947
  buf[0] = 0x00; buf.set(enc, 1);
1954
1948
  ws.send(buf.buffer);
1949
+
1950
+ // Update the keystroke buffer — skip all escape sequences and control characters
1951
+ for (let i = 0; i < data.length; i++) {
1952
+ const ch = data.charCodeAt(i);
1953
+ if (ch === 13 || ch === 10) { // Enter (handled above)
1954
+ continue;
1955
+ } else if (ch === 127 || ch === 8) { // Backspace / Delete
1956
+ if (tab._currentInput) tab._currentInput = tab._currentInput.slice(0, -1);
1957
+ } else if (ch === 27) { // ESC — skip entire escape sequence (CSI, SS3, OSC, DCS, etc.)
1958
+ i++; // skip first char after ESC (usually '[', 'O', ']', 'P')
1959
+ // Skip until we find a final char or run out of data
1960
+ while (i < data.length) {
1961
+ const c = data.charCodeAt(i);
1962
+ if (c >= 0x40 && c <= 0x7E) { i++; break; } // final char — sequence done
1963
+ i++;
1964
+ }
1965
+ } else if (ch >= 32 && ch < 0x7F) { // Printable ASCII only
1966
+ tab._currentInput = (tab._currentInput || '') + data[i];
1967
+ }
1968
+ // All other control chars (0x00-0x1F except 0x0D/0x0A) are silently dropped
1969
+ }
1955
1970
  };
1956
1971
  const sendResize = (cols, rows) => {
1957
1972
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -2185,6 +2200,10 @@ function initTerminal(tab) {
2185
2200
  tab.searchAddon = searchAddon;
2186
2201
  tab.closed = false;
2187
2202
  tab.textarea = term.textarea || term.element?.querySelector('.xterm-textarea, textarea');
2203
+ if (tab.textarea && !tab.textarea.id) {
2204
+ tab.textarea.id = 'xterm-helper-' + tab.id;
2205
+ tab.textarea.name = 'terminal-input';
2206
+ }
2188
2207
 
2189
2208
  // Intercept paste events to use bracketed paste mode (required for TUI apps like vim, nano, mc)
2190
2209
  if (tab.textarea) {
@@ -2370,9 +2389,9 @@ function initTerminal(tab) {
2370
2389
  if (tab.fitAddon) tab.fitAddon.fit();
2371
2390
  if (!window._pinchSaveTimer) {
2372
2391
  window._pinchSaveTimer = setTimeout(() => {
2373
- const persisted = JSON.parse(localStorage.getItem('wt-settings') || '{}');
2392
+ const persisted = JSON.parse(safeStorage.getItem('wt-settings') || '{}');
2374
2393
  persisted.fontSize = tab.term.options.fontSize;
2375
- localStorage.setItem('wt-settings', JSON.stringify(persisted));
2394
+ safeStorage.setItem('wt-settings', JSON.stringify(persisted));
2376
2395
  window._pinchSaveTimer = null;
2377
2396
  }, 500);
2378
2397
  }
@@ -3576,7 +3595,7 @@ async function openFileEditor(path) {
3576
3595
  const splitArea = document.getElementById('editor-split-area');
3577
3596
  const orientToggle = document.getElementById('editor-split-toggle');
3578
3597
  try {
3579
- const orient = localStorage.getItem('wt-editor-split-orientation');
3598
+ const orient = safeStorage.getItem('wt-editor-split-orientation');
3580
3599
  const isHoriz = orient === 'horizontal';
3581
3600
  if (splitArea) splitArea.classList.toggle('horizontal', isHoriz);
3582
3601
  if (orientToggle) {
@@ -3600,7 +3619,7 @@ async function openFileEditor(path) {
3600
3619
  try {
3601
3620
  const isHoriz = splitArea && splitArea.classList.contains('horizontal');
3602
3621
  const key = isHoriz ? 'wt-editor-width' : 'wt-editor-height';
3603
- const saved = localStorage.getItem(key);
3622
+ const saved = safeStorage.getItem(key);
3604
3623
  if (saved) {
3605
3624
  document.documentElement.style.setProperty('--editor-size', saved + 'px');
3606
3625
  }
@@ -3933,7 +3952,7 @@ function searchKeydown(e) {
3933
3952
  // ═══════════════════════════════════════════════════════
3934
3953
  function loadSettings() {
3935
3954
  try {
3936
- const s = JSON.parse(localStorage.getItem('wt-settings'));
3955
+ const s = JSON.parse(safeStorage.getItem('wt-settings'));
3937
3956
  if (s && typeof s === 'object' && !Array.isArray(s)) {
3938
3957
  if (typeof s.theme === 'string' && ['system','tokyonight','light','solarized','gruvbox','dracula','monokai'].includes(s.theme)) settings.theme = s.theme;
3939
3958
  if (typeof s.fontSize === 'number' && s.fontSize >= 8 && s.fontSize <= 32) settings.fontSize = s.fontSize;
@@ -3965,7 +3984,7 @@ function loadSettings() {
3965
3984
  }
3966
3985
  }
3967
3986
 
3968
- function saveSettings() { localStorage.setItem('wt-settings', JSON.stringify(settings)); }
3987
+ function saveSettings() { safeStorage.setItem('wt-settings', JSON.stringify(settings)); }
3969
3988
 
3970
3989
  function saveSetting(k, v) { settings[k] = v; saveSettings(); }
3971
3990
 
@@ -4265,7 +4284,7 @@ function setupSidebarResize() {
4265
4284
  document.removeEventListener('mouseup', stopResize);
4266
4285
  document.removeEventListener('touchmove', doResize);
4267
4286
  document.removeEventListener('touchend', stopResize);
4268
- try { localStorage.setItem('wt-sidebar-width', String(sidebar.offsetWidth)); } catch(e) { console.warn(e); }
4287
+ try { safeStorage.setItem('wt-sidebar-width', String(sidebar.offsetWidth)); } catch(e) { console.warn(e); }
4269
4288
  }
4270
4289
  handle.addEventListener('mousedown', startResize);
4271
4290
  handle.addEventListener('touchstart', startResize, { passive: false });
@@ -4274,7 +4293,7 @@ function setupSidebarResize() {
4274
4293
  function restoreSidebarWidth() {
4275
4294
  if (window.innerWidth <= 768) return;
4276
4295
  try {
4277
- const savedW = localStorage.getItem('wt-sidebar-width');
4296
+ const savedW = safeStorage.getItem('wt-sidebar-width');
4278
4297
  if (savedW) {
4279
4298
  const w = Math.max(150, Math.min(600, parseInt(savedW)));
4280
4299
  if (w) {
@@ -4352,7 +4371,7 @@ function setupEditorResize() {
4352
4371
  const v = document.documentElement.style.getPropertyValue('--editor-size');
4353
4372
  if (v) {
4354
4373
  const key = isHorizontal() ? 'wt-editor-width' : 'wt-editor-height';
4355
- localStorage.setItem(key, v.replace('px', ''));
4374
+ safeStorage.setItem(key, v.replace('px', ''));
4356
4375
  }
4357
4376
  } catch(e) { console.warn(e); }
4358
4377
  }
@@ -4391,7 +4410,7 @@ function toggleEditorSplit() {
4391
4410
  // Restore saved size for this orientation
4392
4411
  try {
4393
4412
  const key = isHoriz ? 'wt-editor-width' : 'wt-editor-height';
4394
- const saved = localStorage.getItem(key);
4413
+ const saved = safeStorage.getItem(key);
4395
4414
  if (saved) {
4396
4415
  document.documentElement.style.setProperty('--editor-size', saved + 'px');
4397
4416
  }
@@ -4403,7 +4422,7 @@ function toggleEditorSplit() {
4403
4422
  });
4404
4423
 
4405
4424
  // Persist orientation
4406
- try { localStorage.setItem('wt-editor-split-orientation', isHoriz ? 'horizontal' : 'vertical'); } catch(e) { console.warn(e); }
4425
+ try { safeStorage.setItem('wt-editor-split-orientation', isHoriz ? 'horizontal' : 'vertical'); } catch(e) { console.warn(e); }
4407
4426
  }
4408
4427
 
4409
4428
  // ═══════════════════════════════════════════════════════
@@ -4679,6 +4698,8 @@ function pasteToTerminal() {
4679
4698
  function fallbackPaste(callback) {
4680
4699
  if (!_pasteInput) {
4681
4700
  _pasteInput = document.createElement('textarea');
4701
+ _pasteInput.id = 'fallback-paste-area';
4702
+ _pasteInput.name = 'paste-input';
4682
4703
  _pasteInput.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;z-index:-1';
4683
4704
  document.body.appendChild(_pasteInput);
4684
4705
  _pasteInput.addEventListener('paste', () => {
@@ -4912,7 +4933,7 @@ function closeOverlay(id) {
4912
4933
  overlay.classList.remove('open');
4913
4934
  removeFocusTrap();
4914
4935
  if (lastFocusedElement) {
4915
- setTimeout(() => lastFocusedElement.focus(), 50);
4936
+ setTimeout(() => lastFocusedElement?.focus(), 50);
4916
4937
  lastFocusedElement = null;
4917
4938
  }
4918
4939
  }
@@ -5092,9 +5113,9 @@ function showPinScreen() {
5092
5113
  // BOOKMARKS
5093
5114
  // ═══════════════════════════════════════════════════════
5094
5115
  function getBookmarks() {
5095
- try { return JSON.parse(localStorage.getItem('wt-bookmarks')) || []; } catch(e) { console.warn('Failed to parse bookmarks:', e); return []; }
5116
+ try { return JSON.parse(safeStorage.getItem('wt-bookmarks')) || []; } catch(e) { console.warn('Failed to parse bookmarks:', e); return []; }
5096
5117
  }
5097
- function saveBookmarks(bm) { localStorage.setItem('wt-bookmarks', JSON.stringify(bm)); renderBookmarks(); }
5118
+ function saveBookmarks(bm) { safeStorage.setItem('wt-bookmarks', JSON.stringify(bm)); renderBookmarks(); }
5098
5119
 
5099
5120
  function renderBookmarks() {
5100
5121
  const list = document.getElementById('bookmarks-list');
@@ -5143,11 +5164,11 @@ function bookmarkCurrentDir() {
5143
5164
  toast('Bookmarked', 'success');
5144
5165
  }
5145
5166
 
5146
- let bookmarksOpen = (() => { try { const v = localStorage.getItem('wt-bm-open'); return v !== null ? v === 'true' : true; } catch(e) { console.warn('Failed to read bookmarksOpen:', e); return true; } })();
5167
+ let bookmarksOpen = (() => { try { const v = safeStorage.getItem('wt-bm-open'); return v !== null ? v === 'true' : true; } catch(e) { console.warn('Failed to read bookmarksOpen:', e); return true; } })();
5147
5168
  function toggleBookmarks() {
5148
5169
  bookmarksOpen = !bookmarksOpen;
5149
5170
  document.getElementById('bookmarks-list').classList.toggle('open', bookmarksOpen);
5150
- try { localStorage.setItem('wt-bm-open', bookmarksOpen); } catch(e) { console.warn(e); }
5171
+ try { safeStorage.setItem('wt-bm-open', bookmarksOpen); } catch(e) { console.warn(e); }
5151
5172
  }
5152
5173
 
5153
5174
  // ═══════════════════════════════════════════════════════
@@ -5220,10 +5241,10 @@ function finderKeydown(e) {
5220
5241
  const DEFAULT_CMDS = window.DEFAULT_CMDS || [];
5221
5242
 
5222
5243
  function getCmdLib() {
5223
- try { return JSON.parse(localStorage.getItem('wt-cmdlib')) || []; } catch(e) { return []; }
5244
+ try { return JSON.parse(safeStorage.getItem('wt-cmdlib')) || []; } catch(e) { return []; }
5224
5245
  }
5225
5246
  function saveCmdLib(cmds) {
5226
- localStorage.setItem('wt-cmdlib', JSON.stringify(cmds));
5247
+ safeStorage.setItem('wt-cmdlib', JSON.stringify(cmds));
5227
5248
  }
5228
5249
 
5229
5250
  function toggleCmdLib() {
@@ -5383,10 +5404,10 @@ let cmdHistMax = 50;
5383
5404
  let _cmdHistCache = [];
5384
5405
 
5385
5406
  function histHeaders() {
5386
- return { 'Content-Type': 'application/json', 'x-pin-token': localStorage.getItem('wt-pin') || '' };
5407
+ return { 'Content-Type': 'application/json', 'x-pin-token': authToken || '' };
5387
5408
  }
5388
5409
  function histHeadersDelete() {
5389
- return { 'x-pin-token': localStorage.getItem('wt-pin') || '' };
5410
+ return { 'x-pin-token': authToken || '' };
5390
5411
  }
5391
5412
  async function getCmdHist() {
5392
5413
  try {
@@ -5406,10 +5427,15 @@ function loadHistMax() {
5406
5427
  }
5407
5428
  async function addToCmdHist(cmd) {
5408
5429
  if (!cmd || !cmd.trim()) return;
5409
- // Strip ANSI escapes and control characters, keep only printable + space + tab
5410
- const clean = cmd.replace(/\x1b\[[0-9;]*[a-zA-Z~]/g, '')
5411
- .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
5412
- .replace(/[\x00-\x08\x0e-\x1f]/g, '')
5430
+ // Strip all ANSI/VT escape sequences and control characters
5431
+ const clean = cmd
5432
+ .replace(/\x1b[^a-zA-Z0-9]*[a-zA-Z0-9~]/g, '') // CSI: ESC [ ... final
5433
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC: ESC ] ... BEL/ST
5434
+ .replace(/\x1b[OPP][^\x40-\x7e]*[\x40-\x7e]/g, '') // SS3/DCS: ESC O/P ... final
5435
+ .replace(/\x1b./g, '') // Any other ESC sequence
5436
+ .replace(/[\x00-\x1f\x7f]/g, '') // All remaining control chars
5437
+ .replace(/^[>;\d\s]+(?=[a-zA-Z/\\~\-.])/, '') // VT params before a real command char
5438
+ .replace(/^[>;\d\s]+$/, '') // Pure VT params, no command at all
5413
5439
  .trim();
5414
5440
  if (!clean) return;
5415
5441
  try {
@@ -5627,7 +5653,7 @@ async function refreshSystemStats(showLoading) {
5627
5653
  // ═══════════════════════════════════════════════════════
5628
5654
  let deferredPrompt = null;
5629
5655
  let installBannerDismissed = false;
5630
- try { installBannerDismissed = localStorage.getItem('wt-install-dismissed') === 'true'; } catch(e) { console.warn(e); }
5656
+ try { installBannerDismissed = safeStorage.getItem('wt-install-dismissed') === 'true'; } catch(e) { console.warn(e); }
5631
5657
 
5632
5658
  window.addEventListener('beforeinstallprompt', e => {
5633
5659
  e.preventDefault();
@@ -5666,7 +5692,7 @@ function installPWA() {
5666
5692
  function dismissInstall() {
5667
5693
  document.getElementById('install-banner').style.display = 'none';
5668
5694
  document.getElementById('toast-container').classList.remove('banner-visible');
5669
- try { localStorage.setItem('wt-install-dismissed', 'true'); } catch(e) { console.warn(e); }
5695
+ try { safeStorage.setItem('wt-install-dismissed', 'true'); } catch(e) { console.warn(e); }
5670
5696
  installBannerDismissed = true;
5671
5697
  }
5672
5698