Cursor blinking (ACTUALLY fixed this time):
- handleFocus() was STILL missing due to botched duplicate removal in
v1.12.1. Now defined once at line 80, no duplicates.
- vue-tsc --noEmit now runs clean (was erroring with TS2393)
Tool windows:
- App.vue now has onErrorCaptured + error display overlay so tool window
crashes show the error instead of silently closing
- defineAsyncComponent uses object form with onError callback for logging
Selection highlighting:
- Changed from rgba(88,166,255,0.4) transparent to solid #264f78
(VS Code's selection color) — always visible on dark backgrounds
- Applied to default theme, TerminalView applyTheme, LocalTerminalView
CSP:
- Simplified to 'self' 'unsafe-inline' asset: for default-src
- Separate connect-src for IPC protocols
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: Every GraphicsUpdate copied the full 8.3MB decoded image
into the front buffer, cloned all 8.3MB for IPC, and transferred all
8.3MB to the frontend — even when only a 100x100 pixel region changed.
During window drag, this created a 25MB/frame pipeline backup.
Fix:
- Track dirty rectangles from ironrdp's GraphicsUpdate(InclusiveRectangle)
- Write path: only copy changed rows from decoded image to front buffer
(e.g. 100 rows × 1920 pixels = 768KB vs 8.3MB full frame)
- Accumulate dirty region as union of all rects since last get_frame
- Read path: if dirty region < 50% of frame, extract only the dirty
rectangle bytes; otherwise fall back to full frame
- Binary IPC format: 8-byte header [x,y,w,h as u16 LE] + pixel data
- Frontend: putImageData at dirty rect offset instead of full frame
- Status bar: h-9 text-sm for 3440x1440 readability
During window drag (typical 300x400 dirty rect):
Before: 8.3MB write + 8.3MB clone + 8.3MB IPC = 24.9MB per frame
After: 480KB write + 480KB extract + 480KB IPC = 1.4MB per frame
~17x reduction in data movement per frame.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Popup windows (tools/editor/help):
- CSP script-src 'self' blocked Tauri's inline IPC bridge scripts in
child WebviewWindows. Added 'unsafe-inline' to script-src. Still
restrictive (was null before SEC-4).
Theme application:
- Watcher on sessionStore.activeTheme needed { deep: true } — Pinia
reactive proxy identity doesn't change on object replacement
- LocalTerminalView.vue had ZERO theme support — added full applyTheme()
with watcher and mount-time application
- Container background now syncs with theme (was stuck on CSS variable)
Cursor blink:
- terminal.focus() after mount in useTerminal.ts — terminal opened
without focus, xterm.js rendered static outline instead of blinking block
Selection highlighting:
- applyTheme() was overwriting theme without selectionBackground/
selectionForeground/selectionInactiveBackground — selection invisible
after any theme change
- Removed !important from terminal.css that overrode canvas selection
- Bumped default selection opacity 0.3 → 0.4
Status bar:
- h-6 text-[10px] → h-8 text-xs (24px/10px → 32px/12px)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: RDP was unresponsive due to frame pipeline bottleneck.
- get_frame() held tokio::Mutex while cloning 8.3MB, blocking the
RDP session thread from writing new frames (mutex contention)
- Frontend fetched on every backend event with no coalescing
- Every GraphicsUpdate emitted an IPC event, flooding the frontend
Fix:
- Double-buffer: back_buffer (tokio::Mutex, write path) and
front_buffer (std::sync::RwLock, read path) — reads never block writes
- get_frame() now synchronous, reads from front_buffer via RwLock
- Backend throttles frame events to every other GraphicsUpdate
- Frontend coalesces events via requestAnimationFrame
- RdpView props now reactive (computed) for correct resize behavior
- rdp_get_frame command no longer async (no .await needed)
- screenshot_png_base64 no longer async
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract handleKeydown into useKeyboardShortcuts.ts composable; reduces
MainLayout by ~20 lines and isolates keyboard logic cleanly
- ConnectionTree: watch groups for additions and auto-expand new entries
- MonitorBar: generation counter prevents stale event listeners on rapid
tab switching
- NetworkScanner: revoke blob URL after CSV export click (memory leak)
- TransferProgress: implement the auto-expand/collapse watcher that was
only commented but never wired up
- FileTree: block binary/large file uploads with clear user error rather
than silently corrupting — backend sftp_write_file is text-only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TabBar: move listen() into onMounted, store UnlistenFn
- Session store: track per-session unlisten fns, clean on close
- useRdp: store frame unlisten in composable scope, call in stopFrameLoop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resize fix:
- ResizeObserver now checks contentRect dimensions before fitting
- Ignores resize events when container width/height < 50px (hidden tab)
- Prevents xterm.js from calculating 8-char columns on zero-width containers
Flickering fix:
- markActivity throttled to once per second per session
- Was firing on every single data event (hundreds per second during
active output), triggering Vue reactivity updates on the sessions
array, causing tab bar and session container re-renders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RDP performance overhaul:
- Switched from polling (rAF loop calling rdp_get_frame every tick)
to event-driven rendering (backend emits rdp:frame:{id} when
frame buffer updates, frontend fetches on demand)
- Eliminates thousands of empty IPC round-trips per second when
the screen is idle
- Backend passes AppHandle into run_active_session for event emission
- Frontend uses listen() instead of requestAnimationFrame polling
MCP terminal fix:
- terminal_type and terminal_execute now send \r (carriage return)
instead of \n (newline) — PTY terminals expect CR to submit
- Fixes commands not auto-sending, requiring manual Enter press
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues:
1. convertEol was false for PTY sessions, but Windows ConPTY sends
bare \n without \r. Now enabled on Windows PTY sessions (checked
via navigator.platform). Unix PTY still false (driver handles it).
2. LocalTerminalView had no ResizeObserver, so the terminal never
reflowed when the container size changed. Added ResizeObserver
matching the SSH TerminalView pattern. Also added proper cleanup
on unmount.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tab activity notifications:
- Background tabs pulse blue when new output arrives
- Clears when you switch to the tab
- useTerminal marks activity on every data event
Session persistence:
- Workspace saved to DB on window close (connection IDs + positions)
- Restored on launch — auto-reconnects saved sessions in order
- workspace_commands: save_workspace, load_workspace
Docker Manager (Tools → Docker Manager):
- Containers tab: list all, start/stop/restart/remove/logs
- Images tab: list all, remove
- Volumes tab: list all, remove
- One-click Builder Prune and System Prune buttons
- All operations via SSH exec channels — no Docker socket exposure
Sidebar drag-and-drop:
- Drag groups to reorder
- Drag connections between groups
- Drag connections within a group to reorder
- Blue border indicator on drop targets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/home doesn't exist on macOS (home dirs are /Users/). Changed default
SFTP path to / so it always loads. OSC 7 parser now strips stray
quotes from shell printf output that produced paths like /"/path".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SFTP tab switch fix:
- Removed :key on FileTree that destroyed component on every switch
- useSftp now accepts a reactive Ref<string> sessionId
- Watches sessionId changes and reinitializes without destroying state
- Per-session path memory via sessionPaths map — switching back to a
tab restores exactly where you were browsing
CWD following fix (macOS + all platforms):
- Injects OSC 7 prompt hook into the shell after SSH connect
- zsh: precmd() emits \e]7;file://host/path\e\\
- bash: PROMPT_COMMAND emits the same sequence
- Sent via the PTY channel so it configures the interactive shell
- The passive OSC 7 parser in the output loop picks it up
- SFTP sidebar auto-navigates to the current working directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ripped out the Gemini API stub (ai/mod.rs, ai_commands.rs, GeminiPanel.vue)
and replaced it with a local PTY terminal in the sidebar panel. Users select
a shell (bash/zsh/sh on Unix, PowerShell/CMD/Git Bash on Windows), launch it,
and run claude/gemini/codex or any CLI tool directly.
Backend:
- New PtyService module using portable-pty (cross-platform PTY)
- DashMap session registry (same pattern as SshService)
- spawn_blocking output loop (portable-pty reader is synchronous)
- 5 Tauri commands: list_available_shells, spawn_local_shell, pty_write,
pty_resize, disconnect_pty
Frontend:
- Parameterized useTerminal composable: backend='ssh'|'pty'
- convertEol=false for PTY (PTY driver handles LF→CRLF)
- CopilotPanel.vue with shell selector, launch/kill, session ended prompt
- Ctrl+Shift+G toggle preserved
Tests: 87 total (5 new PTY tests), zero warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Clipboard paste (rdp_send_clipboard): simulates typing each character
via scancode key press/release events. Full ASCII coverage including
all symbols, numbers, and shifted characters. Handles 32-char
generated passwords without manual typing.
2. Keyboard grab defaults to ON so RDP sessions accept keyboard input
immediately without requiring the user to click the toolbar toggle.
3. Frame dirty flag: GraphicsUpdate sets an AtomicBool, get_frame only
encodes + returns base64 when dirty (returns empty string otherwise).
Eliminates ~8MB/frame base64 encoding on unchanged frames at 30fps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rust RDP service: ironrdp client with full connection handshake
(TCP -> TLS -> CredSSP -> NLA), pixel buffer frame delivery,
mouse/keyboard input via scancode mapping, graceful disconnect.
Runs in dedicated thread with own tokio runtime to avoid Send
lifetime issues with ironrdp trait objects.
Vue frontend: RdpView canvas renderer with 30fps polling,
mouse/keyboard capture, RdpToolbar with Ctrl+Alt+Del and
clipboard. SessionContainer handles both SSH and RDP tabs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rust SSH service: russh async client, DashMap session registry,
TOFU host key verification, CWD tracking via separate exec channel
(never touches terminal stream), base64 event emission for terminal
I/O. 52/52 tests passing.
Vue 3 frontend: ported from Wails v3 to Tauri v2 — useTerminal
composable with streaming TextDecoder + rAF batching, session store
with multi-connection support, connection store/tree, sidebar, tab
bar, status bar, keyboard shortcuts. All Wails imports replaced
with Tauri API equivalents.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>