diff --git a/resources/css/app.css b/resources/css/app.css
index 936e0c713..de92bf0c9 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -53,6 +53,13 @@
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
+
+@layer components {
+ .terminal-mobile-key {
+ @apply min-h-10 rounded-md border border-white/10 bg-white/10 px-2 py-2 text-sm font-semibold text-white shadow-inner active:bg-white/25;
+ }
+}
+
@layer base {
*,
diff --git a/resources/js/terminal.js b/resources/js/terminal.js
index 7a7fc8536..8498f4356 100644
--- a/resources/js/terminal.js
+++ b/resources/js/terminal.js
@@ -52,6 +52,7 @@ export function initializeTerminalComponent() {
// Visibility handling - prevent disconnects when tab loses focus
isDocumentVisible: true,
wasConnectedBeforeHidden: false,
+ mobileToolbarCollapsed: false,
init() {
this.setupTerminal();
@@ -538,6 +539,64 @@ export function initializeTerminalComponent() {
});
},
+
+ sendTerminalInput(data) {
+ if (!this.term || !this.terminalActive) {
+ return;
+ }
+
+ this.term.focus();
+ this.sendMessage({ message: data });
+ },
+
+ sendTerminalControl(sequence) {
+ const terminalSequences = {
+ arrowUp: '\x1b[A',
+ arrowDown: '\x1b[B',
+ arrowRight: '\x1b[C',
+ arrowLeft: '\x1b[D',
+ tab: '\t',
+ escape: '\x1b',
+ ctrlC: '\x03'
+ };
+
+ if (terminalSequences[sequence]) {
+ this.sendTerminalInput(terminalSequences[sequence]);
+ }
+ },
+
+ async pasteFromClipboard() {
+ if (!navigator.clipboard?.readText) {
+ this.$wire.dispatch('error', 'Clipboard paste is not available in this browser.');
+ return;
+ }
+
+ try {
+ const text = await navigator.clipboard.readText();
+ if (text) {
+ this.sendTerminalInput(text);
+ }
+ } catch (error) {
+ logTerminal('warn', '[Terminal] Clipboard paste failed:', error);
+ this.$wire.dispatch('error', 'Clipboard paste permission was denied.');
+ }
+ },
+
+ async copyTerminalSelection() {
+ const selection = this.term?.getSelection();
+ if (!selection) {
+ this.$wire.dispatch('error', 'Select terminal text before copying.');
+ return;
+ }
+
+ try {
+ await navigator.clipboard.writeText(selection);
+ } catch (error) {
+ logTerminal('warn', '[Terminal] Clipboard copy failed:', error);
+ this.$wire.dispatch('error', 'Clipboard copy permission was denied.');
+ }
+ },
+
keepAlive() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.sendMessage({ ping: true });
@@ -629,15 +688,20 @@ export function initializeTerminalComponent() {
// Force a refresh of the fit addon dimensions
this.fitAddon.fit();
- // Get fresh dimensions after fit
- const wrapperHeight = this.$refs.terminalWrapper.clientHeight;
- const wrapperWidth = this.$refs.terminalWrapper.clientWidth;
+ // Get fresh dimensions from the terminal element itself. The mobile
+ // toolbar can live beside the terminal in normal flow, so wrapper dimensions
+ // would include controls that should not be counted as terminal rows.
+ const terminalElement = document.getElementById('terminal');
+ const terminalHeight = terminalElement?.clientHeight || this.$refs.terminalWrapper.clientHeight;
+ const terminalWidth = terminalElement?.clientWidth || this.$refs.terminalWrapper.clientWidth;
- // Account for terminal container padding (px-2 py-1 = 8px left/right, 4px top/bottom)
- const horizontalPadding = 16; // 8px * 2 (left + right)
- const verticalPadding = 8; // 4px * 2 (top + bottom)
- const height = wrapperHeight - verticalPadding;
- const width = wrapperWidth - horizontalPadding;
+ // Account for terminal container padding. In fullscreen mobile mode,
+ // the fixed toolbar sits over the terminal container, so reserve its height
+ // when calculating rows to keep the prompt above the controls.
+ const horizontalPadding = 16; // px-2 = 8px * 2 (left + right)
+ const verticalPadding = 8; // py-1 = 4px * 2 (top + bottom)
+ const height = terminalHeight - verticalPadding;
+ const width = terminalWidth - horizontalPadding;
// Check if dimensions are valid
if (height <= 0 || width <= 0) {
diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php
index c46c5f316..0adb92db6 100644
--- a/resources/views/livewire/project/shared/terminal.blade.php
+++ b/resources/views/livewire/project/shared/terminal.blade.php
@@ -18,10 +18,32 @@
@endif
+ :class="fullscreen ? 'fixed inset-0 z-[9999] m-0 h-[100dvh] w-screen max-w-none overflow-hidden rounded-none !bg-black p-0' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
+ :class="fullscreen ? (mobileToolbarCollapsed ? 'h-[calc(100dvh-3.5rem)] mb-14 px-2 py-1 bg-black' : 'h-[calc(100dvh-6rem)] mb-[6rem] px-2 py-1 bg-black') : 'h-[510px] max-h-[calc(100dvh-10rem)] overflow-hidden px-2 py-1 rounded-sm bg-black'"
+ x-show="terminalActive">
+
+
+
+
+ Terminal keys
+
+
+
+
+
+
+
+
+
+
+