raw.githubusercontent.com URLs so they render on Packagist and the Filament plugins page (which don't resolve repo-relative paths); the hero image carries filament-hidden to avoid duplicating the plugin-page banner.composer require mwguerra/web-terminal-stream) — the pre-publication private-VCS-repository instructions were removed.docs/images/mwguerra-web-terminal-stream.jpg, 16:9 2880×1620 JPEG). Docs-only; docs/ is export-ignored, so dist archives are unchanged.First stable release.
Http\Controllers\TerminalWebSocketController previously minted a WebSocket token for any client-supplied connectionConfig with no gate and no restriction on the target, letting an authenticated user open an arbitrary local shell or SSH connection through the server. Issuance (both the REST route and the Livewire path) now (1) enforces the optional useStreamTerminal gate, (2) runs a new Security\ConnectionPolicy — security.allow_local toggles local shells, security.ssh_allowed_hosts is an SSH destination allow-list (bare host or host:port) — and (3) rate-limits the ws-token route (security.token_rate_limit). The policy is re-checked on the WebSocket server before a PTY starts, so a token minted for a disallowed target is still refused. The cached connection config (which may hold SSH credentials) is now encrypted at rest and decrypted by the server on pull.Security\SshHostKeyVerifier verifies the host key before authentication, controlled by security.ssh_host_key.mode: off (documented insecure default), fingerprints (SHA256/MD5 pins per host or host:port), or known_hosts (an OpenSSH file, plaintext and hashed |1| entries, [host]:port for non-standard ports). A mismatch aborts the connection.Origin header of every browser handshake against the (re-added) stream.allowed_origins config key (default [env('APP_URL', 'http://localhost')]). Disallowed origins are rejected with HTTP 403 — and logged at warning level — before the single-use auth token is consumed. Requests without an Origin header (non-browser clients) still pass through to token auth; a literal '*' entry (or an empty list) disables the check.composer stress). The first registered benchmark: 100 concurrent token-authenticated WebSocket→SSH-PTY sessions (deterministic 3–10s lifetimes, remotely-expanded echo markers proving real round-trips) swept across latency profiles — Toxiproxy on the client leg (client-10ms/client-500ms) and tc netem on the sshd egress (ssh-10ms/ssh-500ms) plus a baseline — with an over-cap probe asserting stream.max_connections refusals (HTTP 503) and a PtySessionRegistry drain check after every run. Results merge into docs/benchmarks/stress-latest.json. Opt-in via WTS_STRESS=1; never runs in CI or composer bench. New dev dependency: ratchet/pawl.grid.spec.js e2e coverage for TerminalGrid — side-by-side columns(2) geometry, per-pane WebSocket output isolation, and the pure-CSS :focus-within focus ring following clicks between panes.stream config knobs: max_connections (total live PTYs, default 100), max_sessions_per_user (per authenticated user, default 10), and max_handshake_bytes (caps the pre-handshake buffer, default 16 KB). The server also reaps a session whose shell exited (previously it leaked in the bridge map forever and never told the client), closes sockets whose PTY outlived max_session_lifetime, tears down every PTY/SSH channel on SIGINT/SIGTERM (needs ext-pcntl), and records each PID's kernel start-time so a stale-session reap never signals a recycled PID belonging to an unrelated process.terminal-stream:serve capability preflight. On boot the server warns when ext-posix is missing (local-shell PTY resize + orphan cleanup disabled), ext-pcntl is missing (no graceful shutdown), or the host is not Linux (local PTY resize relies on /proc + stty). ext-posix / ext-pcntl are declared in composer suggest (not required, so SSH-only installs work without them) and the posix_kill calls are guarded so a missing extension degrades instead of fataling.Schemas\Components\TerminalDashboard — toggle-driven terminals with auto-layout. A roster of named terminal sources (each a distinct connection), each opened or closed by a button; the open panes are auto-arranged by a count-to-preset map (->arrangement([2 => 'columns', 3 => 'main-left'], default: 'tiled'), capped by ->maxOpen(), up to 4). Closing a source destroys its pane (WebSocket + PTY). Backed by Livewire\StreamDashboard (Locked source roster; toggle() re-checks the useStreamTerminal gate on open and enforces the cap) and new layout presets in LayoutTree::arrange($paneIds, $preset) — tiled (2 columns; 3 = one tall left + two stacked right; 4 = even 2x2 grid), columns, rows, main-left, main-top, all with even ratios. Same flat-keyed-pane isolation as the workspace, so toggling one source never re-renders the others.Schemas\Components\TerminalWorkspace — tmux-style dynamic tiling. One terminal that splits into arbitrarily nested panes at runtime: prefix-key shortcuts (default ctrl+b + %/"/x/z/arrows/hjkl, fully remappable via keymap()/config), drag-resizable dividers, keyboard resize, pane zoom (siblings stay live), directional focus navigation, a per-pane close (×) button shown whenever more than one pane exists (the sole remaining pane has none and fills the workspace), and an empty-state respawn. Backed by Livewire\StreamWorkspace (pane roster + binary split tree as Locked props; every mutation server-validated: useStreamTerminal gate, maxPanes ceiling, ratio clamping) and Data\Layout\LayoutTree (pure split/collapse/ratio operations, persistence-ready wire shape). New panes clone their split source (tmux semantics) or a defaultPane() template evaluated at schema build. Panes render as a flat keyed list of absolutely-positioned siblings with Alpine-bound geometry, so a live pane's canvas and WebSocket never re-render when a sibling splits or closes.Themes\TerminalTheme + shipped presets. An extendable theme object controls terminal font (family, size — previously hardcoded in the view), colors (background, foreground, cursor, selection, ANSI palette), and pane divider styling (width, line style, color, focus highlight). Fluent partial overrides keep every other default: TerminalTheme::make()->fontSize(15)->dividerColor('#7aa2f7'). Ship a theme by subclassing (class BrandTheme extends TerminalTheme { protected string $background = '#0b1021'; }) — make() returns the subclass and it stays fluently tweakable. Presets Themes\TokyoNight and Themes\Dracula included. Apply via ->theme(TokyoNight::make()) on WebTerminalStream, TerminalWorkspace, or TerminalGrid (grid/workspace forward the terminal look to panes and emit divider CSS custom properties — --wts-divider-width/-style/-color/-focus, --wts-terminal-bg — on the container). A raw colors array still works for just the ghostty theme; theme() now accepts TerminalTheme|array|Closure, and StreamTerminal gained fontFamily/fontSize props.PaneAction::SplitLeft/SplitRight/SplitUp/SplitDown place the new pane on the named side of the focused one (LayoutTree::splitPane() and StreamWorkspace::splitPane() gained a $before flag; left/up insert the new pane before its sibling). Unbound in the default keymap — opt in, e.g. Keymap::default()->bind(PaneAction::SplitLeft, 'ctrl+arrowleft'). The keyboard close action now refuses to close the last remaining pane (the workspace always keeps one), matching the per-pane × button.Enums\PaneAction and Data\Keymap — the configurable keyboard-shortcut model for the upcoming TerminalWorkspace tiling component. Keymap::tmux() ships the tmux preset (prefix ctrl+b, %/" splits, x close, z zoom, arrows/hjkl focus, ctrl+arrows resize); ->prefix(), ->bind(), ->unbind() customize it; Keymap::fromArray() hydrates the new workspace.shortcuts config section. New workspace config block: shortcuts, max_panes, min_pane_ratio, resize_step.ConnectionConfig::toTransportArray() — the full wire-format array (credentials included) handed to the PTY bridge; toArray() stays credential-free for logging/display.Schemas\Components\TerminalGrid — first increment of the tiling layer. Lays out N WebTerminalStream panes in a flush CSS grid: TerminalGrid::make()->panes([...]) with Filament-style responsive columns() (default 2), paneGap(int $px) (positive gaps render as pane dividers via the container background), height(string) with equally shared rows, auto-applied frameless() + squareCorners() per pane (a pane's explicit setting wins), grid-level connectionBehavior() forwarded to panes without their own, a CSS :focus-within focused-pane ring, and per-pane wire:key isolation. Manual composition of frameless terminals in your own Grid still works.connectionBehavior() is now fully implemented instead of merely accepted. ConnectionBehavior::Manual renders a themed, centered Connect affordance and opens the WebSocket (and PTY) only on click — never-opened panes cost no server process and write no connection log rows. Auto auto-connects and adds a connect/disconnect toggle (header action, or floating overlay control when frameless()), with a Reconnect affordance after disconnect. The schema component default remains Always (auto-connect, no controls), so existing code is unaffected. Disconnect closes the WebSocket with a clean client close (code 1000).mwguerra/web-terminal (its feature/frameless branch) as a standalone, Stream-mode-only package.WebTerminalStream::make() schema component, WebTerminalStreamPlugin with a Terminal page and a Terminal Logs resource.StreamTerminal dispatches TerminalConnectedEvent / TerminalDisconnectedEvent and records them to the terminal_stream_logs table via TerminalLogger.terminal-stream:install, terminal-stream:serve, terminal-stream:make-page, terminal-stream:logs:cleanup.composer test:integration): a throwaway alpine sshd container (tests/docker/, committed test-only keys) exercises real SSH password/key/passphrase auth, echo round-trips, remote PTY resize, and termination; a Linux php container covers the Linux-only local-PTY resize path; a full-stack test boots terminal-stream:serve and drives it with a real WebSocket client (token auth, Origin rejection, single-use token replay).npm run test:e2e): scripts/e2e/setup.sh scaffolds a dedicated Laravel 13 + Filament 5 app into the gitignored tests/e2e-app/; specs assert per-pane WebSocket frame isolation, manual-connect laziness, workspace split/close/zoom/drag-resize/focus-navigation via real Ctrl+B chords, and that a live pane's socket survives sibling splits unblinked. GitHub Actions workflows run unit (PHP 8.3/8.4 × Laravel 12/13), integration, and e2e jobs.disconnect() only fires on a clean teardown, so a killed tab or dropped network left a session showing as "connected" forever. The WebSocket server now records the disconnect on socket close — the one place every disconnect is observed — de-duplicated against a disconnect the browser may already have logged.TerminalLogResource did not scope to the tenant. With multi-tenancy configured (logging.tenant_column + a resolver), the log list is now filtered to the resolved tenant via getEloquentQuery(), so one tenant can no longer read another's terminal history.--with-tenant migration's tenant_id is now nullable + nullOnDelete (a resolver that legitimately returns null no longer silently drops the row), both migrations gained a standalone created_at index for the retention cleanup, and the always-empty command / output / exit-code / duration columns, filters, infolist sections, and widget stats were removed from the Logs UI (this package logs connection lifecycle only).terminal-stream:logs:cleanup --days rejected a non-numeric value and, critically, a negative one — --days=-5 fed olderThan(-5) a date in the future and silently deleted every log entry.terminal-stream:install --force wrote a second timestamped migration alongside an existing one, so migrate failed on a duplicate Schema::create(); it now overwrites the existing migration in place.terminal-stream:make-page wrote the given name verbatim into the class declaration; an invalid name (spaces, dashes, leading digit) produced uncompilable PHP. It is now validated.terminal-stream:serve bound to an empty host / port 0 when passed an empty --host= / --port=; these now fall back to config.composer analyse was broken — it referenced a phpstan binary that was never a dependency. PHPStan (level 5 on src, with a baseline of pre-existing findings) is now wired up and green..gitattributes export-ignore set now keeps the throwaway SSH test keys, the 14k-file e2e app, docs, and CI tooling out of the distributed package.$el magic inside the divider's pointermove handler, where $el is the 8px divider element rather than the workspace container — so the pointer-to-ratio math divided by 8px instead of the workspace width and the split ratio oscillated between its min/max clamps instead of tracking the cursor. The component now captures the workspace root once at init and uses it for all geometry. Divider dragging is now smooth and tracks the pointer.setWindowSize() only stores dimensions for the pty-req sent when the shell channel opens — calling it on an open shell never reaches the remote PTY. WebSocket\InteractiveSsh (an SSH2 subclass) now sends the RFC 4254 §6.7 window-change channel request, so browser resizes propagate to remote shells. Proven by an integration test against a real sshd container.TerminalBuilder silently dropped SSH credentials when given a ConnectionConfig value object (the old withConfig() went through the credential-stripping toArray()), so SSH terminals built that way could never authenticate. ->connection($config) now routes through toTransportArray().WebTerminalStream::ssh() named its first parameter $config, so the documented named-parameter form (->ssh(host: '…', username: '…')) failed with "Unknown named parameter". The parameter is now $host; positional array/Closure usage is unchanged.The bundled JavaScript is now registered with the namespaced Filament asset id web-terminal-stream (was stream-terminal) so co-installing mwguerra/web-terminal cannot collide (Filament keys scripts globally by id). After upgrading, run php artisan filament:assets so the browser loads the asset at its new published path — otherwise the old file 404s and the terminal fails to boot.
Breaking — pre-1.0 fluent API clean break. One connection vocabulary for both the schema component and TerminalBuilder (shared ConfiguresConnection trait): ssh() with named args is canonical (privateKey: replaces key:; port defaults to null → 22), local(?workingDirectory, environment) gains parameters, workingDirectory() exists on both surfaces; sshWithPassword()/sshWithKey()/withConfig()/connection(type, array) were removed. streamTheme() → theme(). ConnectionBehavior::AutoWithButton/AutoHidden → Auto ('auto')/Always ('always'); getEffectiveConnectionBehavior() → non-null getConnectionBehavior(); toFlags() removed. Deprecated windowControls()/startConnected()/autoConnect() aliases, the EmitsDeprecationNotices machinery, and the deprecations config block were deleted. StreamTerminal lost the redundant showWindowControls/autoConnect props (derived from chrome/connectionBehavior) and its streamTheme prop is now theme. log() is named-args only. Default height unified at 400px (fluent builders previously said 350px). TerminalGrid::terminals() → panes(), gap(int) → paneGap(int) (Filament's boolean gap() is no longer overridden). Plugin only() removed; withoutTerminalPage()/withoutTerminalLogs() now subtract from the components() whitelist instead of being ignored. See UPGRADING.md §4b for the full old → new table.
filament/filament (^5.0) added to require-dev (dev-only — the production suggest is unchanged) so the Filament-dependent tests (schema components, plugin) run for real instead of self-skipping. The previously skipped plugin tests were rewritten against the actual plugin API; the skipping had hidden stale assertions of Classic-era methods (isEnabled(), disabled(), allowedCommands()) that never existed in this package. composer.lock is now committed.
The WebSocket server's ReactPHP stack (react/socket, react/event-loop, ratchet/rfc6455) moved from require-dev/suggest to require — it is the package's core and now installs automatically.
Breaking (vs. mwguerra/web-terminal): Classic command-by-command mode was removed entirely — Stream is the only rendering path. The command whitelist/sanitizer/rate-limit security layer, interactive TUI sessions, mode toggles (mode(), dual(), streamTerminal(), classicTerminal()), and the related fluent knobs and config keys do not exist in this package.
Package renamed to mwguerra/web-terminal-stream with namespace MWGuerra\WebTerminalStream\, config file config/web-terminal-stream.php, view/translation namespace web-terminal-stream::, terminal-stream:* Artisan commands, terminal-stream/ws-token route, and terminal_stream_logs table, so it can be installed side-by-side with mwguerra/web-terminal.
How can I help you explore Laravel packages today?