Add terminal big mode, keyboard shortcuts menu, and UX refinements
- Reduce mobile terminal widths by 2 chars (portrait 42x24, landscape 86x24) - Add "Big mode" for mobile: desktop sizing (120x36) at 70% zoom - Click zoom percentage to reset to 100% - Add keyboard shortcuts submenu in session settings - Update PRD with all terminal features and documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -339,6 +339,38 @@ stream_session_response(session_id, callback)
|
|||||||
- Filters for `spiceflow-*` prefix
|
- Filters for `spiceflow-*` prefix
|
||||||
- Returns managed sessions only
|
- Returns managed sessions only
|
||||||
|
|
||||||
|
#### 4.1.5 External Tmux Session Import
|
||||||
|
|
||||||
|
Users can import existing tmux sessions not managed by Spiceflow:
|
||||||
|
|
||||||
|
1. **List External:** `GET /api/tmux/external` returns sessions without `spiceflow-` prefix
|
||||||
|
2. **Import:** `POST /api/tmux/import` with session name
|
||||||
|
3. **Rename:** Server renames `{name}` to `spiceflow-{name}`
|
||||||
|
4. **Setup:** Enables pipe-pane capture for imported session
|
||||||
|
5. **Return:** Session available in Spiceflow with new prefixed ID
|
||||||
|
|
||||||
|
#### 4.1.6 Session Rename
|
||||||
|
|
||||||
|
Sessions can be renamed via `PATCH /api/sessions/:id`:
|
||||||
|
|
||||||
|
**Claude/OpenCode:**
|
||||||
|
- Updates title in database
|
||||||
|
- Session ID remains unchanged
|
||||||
|
|
||||||
|
**Tmux:**
|
||||||
|
- Renames tmux session via `tmux rename-session`
|
||||||
|
- **Session ID changes** to new `spiceflow-{name}` format
|
||||||
|
- Response includes `idChanged: true` and new session object
|
||||||
|
- Client navigates to new URL with `replaceState`
|
||||||
|
|
||||||
|
#### 4.1.7 Session Eject (Tmux only)
|
||||||
|
|
||||||
|
Removes session from Spiceflow management while keeping it running:
|
||||||
|
|
||||||
|
1. Rename tmux session from `spiceflow-{name}` to `{name}`
|
||||||
|
2. Delete session from Spiceflow database
|
||||||
|
3. Session continues running, attachable via `tmux attach -t {name}`
|
||||||
|
|
||||||
### 4.2 Process Handle Structure
|
### 4.2 Process Handle Structure
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
@@ -572,6 +604,11 @@ Permissions are recorded as assistant messages with metadata:
|
|||||||
- **Examples:** `spiceflow-brave-fox-0042`, `spiceflow-calm-owl-1337`
|
- **Examples:** `spiceflow-brave-fox-0042`, `spiceflow-calm-owl-1337`
|
||||||
- **Purpose:** Human-readable, unique identifiers
|
- **Purpose:** Human-readable, unique identifiers
|
||||||
|
|
||||||
|
**Name Generation:**
|
||||||
|
- 30 adjectives × 30 nouns = 900 base combinations
|
||||||
|
- 4-digit random suffix (0000-9999)
|
||||||
|
- Docker-style naming convention
|
||||||
|
|
||||||
#### 7.1.2 Output Capture
|
#### 7.1.2 Output Capture
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -635,10 +672,96 @@ tmux capture-pane -t {session-name} -p -e -S -1000
|
|||||||
|
|
||||||
| Mode | Dimensions | Use Case |
|
| Mode | Dimensions | Use Case |
|
||||||
|------|------------|----------|
|
|------|------------|----------|
|
||||||
| `portrait` | 40x24 | Phone portrait |
|
| `portrait` | 42x24 | Phone portrait |
|
||||||
| `landscape` | 65x24 | Phone landscape |
|
| `landscape` | 86x24 | Phone landscape |
|
||||||
| `desktop` | 100x24 | Split screen |
|
| `desktop` | 120x36 | Split screen |
|
||||||
| `fullscreen` | 180x24 | Full terminal |
|
| `fullscreen` | 260x36 | Full terminal |
|
||||||
|
|
||||||
|
### 7.5 Auto Orientation Detection
|
||||||
|
|
||||||
|
On mobile devices, the terminal automatically resizes when the user rotates their phone:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
screen.orientation.addEventListener('change', () => {
|
||||||
|
const type = screen.orientation.type;
|
||||||
|
if (type.includes('portrait')) {
|
||||||
|
resizeScreen('portrait');
|
||||||
|
} else if (type.includes('landscape')) {
|
||||||
|
resizeScreen('landscape');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trigger Conditions:**
|
||||||
|
- Only on mobile (width < 640px or height < 450px)
|
||||||
|
- Uses `screen.orientation` API (modern browsers)
|
||||||
|
|
||||||
|
### 7.6 Font Zoom Control
|
||||||
|
|
||||||
|
Terminal text size can be adjusted from 50% to 150% in 5% increments:
|
||||||
|
|
||||||
|
| Control | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| `-` button | Decrease font scale |
|
||||||
|
| `+` button | Increase font scale |
|
||||||
|
| Percentage display | Shows current zoom; click to reset to 100% |
|
||||||
|
|
||||||
|
**Visibility:** Zoom controls hidden on mobile portrait, visible on landscape/desktop.
|
||||||
|
|
||||||
|
### 7.7 Big Mode
|
||||||
|
|
||||||
|
Mobile users can enable "Big mode" from the settings menu to view more terminal content:
|
||||||
|
|
||||||
|
- **Resize:** Sets terminal to desktop dimensions (120x36)
|
||||||
|
- **Zoom:** Sets font scale to 70%
|
||||||
|
- **Access:** Settings menu (gear icon) → "Big mode" button
|
||||||
|
- **Visibility:** Only shown for tmux sessions on mobile devices
|
||||||
|
|
||||||
|
### 7.8 Session Eject
|
||||||
|
|
||||||
|
Tmux sessions can be "ejected" from Spiceflow management while keeping them running:
|
||||||
|
|
||||||
|
1. User clicks "Eject session" in settings menu
|
||||||
|
2. Server renames session from `spiceflow-{name}` to `{name}`
|
||||||
|
3. Session removed from Spiceflow database
|
||||||
|
4. User can reattach manually via `tmux attach -t {name}`
|
||||||
|
|
||||||
|
**Use Case:** Transfer session to local terminal for continued work.
|
||||||
|
|
||||||
|
### 7.9 Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
|----------|--------|
|
||||||
|
| `Ctrl+Down` | Scroll to bottom |
|
||||||
|
| `Ctrl+Shift+V` | Paste from clipboard |
|
||||||
|
| `Shift+Enter` | Send literal newline |
|
||||||
|
| `Shift+Tab` | Send reverse-tab escape sequence |
|
||||||
|
| `^` toggle | Enable Ctrl mode (next letter sends control character) |
|
||||||
|
|
||||||
|
### 7.10 Quick Action Buttons
|
||||||
|
|
||||||
|
| Button | Function | Color |
|
||||||
|
|--------|----------|-------|
|
||||||
|
| `^` | Toggle Ctrl mode | Gray (cyan ring when active) |
|
||||||
|
| `^C` | Send interrupt | Red |
|
||||||
|
| `^D` | Send EOF | Amber |
|
||||||
|
| `y` | Send 'y' | Green |
|
||||||
|
| `n` | Send 'n' | Red |
|
||||||
|
| `1-4` | Send number | Gray |
|
||||||
|
| `⇥` | Send Tab | Cyan |
|
||||||
|
| `⇤` | Send Shift+Tab | Cyan |
|
||||||
|
| `↵` | Send Enter | Green |
|
||||||
|
| `📋` | Paste clipboard | Violet |
|
||||||
|
|
||||||
|
**Visibility:** `^`, `y`, `n`, `1-4`, and paste hidden on mobile portrait.
|
||||||
|
|
||||||
|
### 7.11 ANSI Color Rendering
|
||||||
|
|
||||||
|
Terminal output preserves ANSI escape sequences:
|
||||||
|
- Converted to HTML via `ansi-to-html` library
|
||||||
|
- Default foreground: green (#22c55e)
|
||||||
|
- Background: transparent
|
||||||
|
- Supports standard terminal colors
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -954,6 +1077,23 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### POST /api/sessions/:id/eject
|
||||||
|
|
||||||
|
Ejects a tmux session from Spiceflow management (tmux only).
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ejected",
|
||||||
|
"message": "Session ejected. Reattach with: tmux attach -t {name}",
|
||||||
|
"session-name": "my-session"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 400: Not a tmux session
|
||||||
|
- 404: Session not found
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. WebSocket Protocol
|
## 10. WebSocket Protocol
|
||||||
@@ -1085,6 +1225,24 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
- **Heartbeat:** Ping every 25 seconds
|
- **Heartbeat:** Ping every 25 seconds
|
||||||
- **Pong Timeout:** 10 seconds
|
- **Pong Timeout:** 10 seconds
|
||||||
|
|
||||||
|
### 10.5 Terminal Update Broadcasting
|
||||||
|
|
||||||
|
After tmux input, server broadcasts terminal updates:
|
||||||
|
|
||||||
|
1. Input received via `POST /api/sessions/:id/terminal/input`
|
||||||
|
2. Input sent to tmux immediately
|
||||||
|
3. 100ms delay for command execution
|
||||||
|
4. Fresh terminal content captured
|
||||||
|
5. Diff computed and broadcast via WebSocket
|
||||||
|
6. Broadcast always sent (even if unchanged) to ensure client sync
|
||||||
|
|
||||||
|
### 10.6 Full Frame Refresh
|
||||||
|
|
||||||
|
To handle potential drift, the server periodically sends full frames:
|
||||||
|
- Every 5 seconds during active streaming
|
||||||
|
- On explicit `fresh=true` request
|
||||||
|
- After terminal resize operations
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. User Interface
|
## 11. User Interface
|
||||||
@@ -1097,6 +1255,9 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ ☰ Spiceflow 🔔 + ↻ │
|
│ ☰ Spiceflow 🔔 + ↻ │
|
||||||
├─────────────────────────────────────────┤
|
├─────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ ● 2 sessions processing │ │ (green badge, pulsing)
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─────────────────────────────────┐ │
|
│ ┌─────────────────────────────────┐ │
|
||||||
│ │ ● My Claude Session claude │ │
|
│ │ ● My Claude Session claude │ │
|
||||||
@@ -1108,14 +1269,19 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
│ │ /home/user 1d ago │ │
|
│ │ /home/user 1d ago │ │
|
||||||
│ └─────────────────────────────────┘ │
|
│ └─────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ + Import tmux session │ │ (if external sessions exist)
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Elements:**
|
**Elements:**
|
||||||
- Header with branding and action buttons
|
- Header with branding and action buttons
|
||||||
|
- Processing sessions counter (green pulsing badge)
|
||||||
- Session cards with status indicators
|
- Session cards with status indicators
|
||||||
- Provider badges (color-coded)
|
- Provider badges (color-coded)
|
||||||
- Relative timestamps
|
- Relative timestamps
|
||||||
|
- Import button for external tmux sessions
|
||||||
|
|
||||||
#### Session Page (`/session/:id`)
|
#### Session Page (`/session/:id`)
|
||||||
|
|
||||||
@@ -1135,10 +1301,15 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
│ └─────────────────────────────────┘ │
|
│ └─────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
├─────────────────────────────────────────┤
|
├─────────────────────────────────────────┤
|
||||||
│ [Type a message... ] [➤] │
|
│ [⌨] [Type a message... ] [➤] │
|
||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Mobile Keyboard Toggle:**
|
||||||
|
- `⌨` button shows/hides mobile keyboard
|
||||||
|
- Addresses issue where keyboard can hide input field
|
||||||
|
- Toggles between up/down arrow indicators
|
||||||
|
|
||||||
**Terminal Mode (tmux):**
|
**Terminal Mode (tmux):**
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
@@ -1164,7 +1335,7 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
│ ⚠️ Claude needs permission │
|
│ ⚠️ Claude needs permission │
|
||||||
├─────────────────────────────────────────┤
|
├─────────────────────────────────────────┤
|
||||||
│ │
|
│ │
|
||||||
│ Write: /home/user/foo.md │
|
│ Write: /home/user/foo.md .md │ (file extension badge)
|
||||||
│ ┌─────────────────────────────────┐ │
|
│ ┌─────────────────────────────────┐ │
|
||||||
│ │ + 1 # My Haiku │ │
|
│ │ + 1 # My Haiku │ │
|
||||||
│ │ + 2 │ │
|
│ │ + 2 │ │
|
||||||
@@ -1177,6 +1348,14 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
└─────────────────────────────────────────┘
|
└─────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**File Diff Viewer:**
|
||||||
|
- **Write operations:** All lines shown as green additions (+)
|
||||||
|
- **Edit operations:** Old lines in red (-), new lines in green (+)
|
||||||
|
- **Line numbers:** Shown for both old and new content
|
||||||
|
- **File extension badge:** Displayed in top-right corner
|
||||||
|
- **Hover highlighting:** Lines highlight on mouse over
|
||||||
|
- **Tab handling:** Tabs rendered as 4 spaces
|
||||||
|
|
||||||
### 11.3 Component Inventory
|
### 11.3 Component Inventory
|
||||||
|
|
||||||
| Component | Purpose | Props |
|
| Component | Purpose | Props |
|
||||||
@@ -1194,9 +1373,65 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
|
|
||||||
| Breakpoint | Header | Layout |
|
| Breakpoint | Header | Layout |
|
||||||
|------------|--------|--------|
|
|------------|--------|--------|
|
||||||
| Portrait | Full header | Vertical stack |
|
| Portrait (<640px width, >450px height) | Full header | Vertical stack |
|
||||||
| Landscape mobile | Hamburger menu | Compact header |
|
| Landscape mobile (<450px height) | Hamburger menu | Compact header |
|
||||||
| Desktop | Full header | All controls visible |
|
| Desktop (≥640px width, ≥450px height) | Full header | All controls visible |
|
||||||
|
| XL Desktop (≥1280px width) | Full header | Mobile orientation buttons hidden |
|
||||||
|
|
||||||
|
### 11.5 Mobile-Specific UI Classes
|
||||||
|
|
||||||
|
| CSS Class | Behavior |
|
||||||
|
|-----------|----------|
|
||||||
|
| `portrait-hide` | Hidden on mobile portrait (width < 640px AND height > 450px) |
|
||||||
|
| `desktop-only` | Hidden on mobile (width < 640px OR height < 450px) |
|
||||||
|
| `mobile-only` | Hidden on XL desktop (width ≥ 1280px) |
|
||||||
|
| `landscape-mobile:hidden` | Hidden when height < 450px |
|
||||||
|
| `landscape-menu` | Shown only when height < 450px |
|
||||||
|
|
||||||
|
### 11.6 Message Condensing
|
||||||
|
|
||||||
|
Long messages (5+ lines) can be collapsed:
|
||||||
|
|
||||||
|
- **Threshold:** 5 lines minimum to show collapse toggle
|
||||||
|
- **Preview:** Shows first 3 lines when collapsed
|
||||||
|
- **Toggle:** Chevron indicator expands/collapses
|
||||||
|
- **Bulk action:** "Condense all" in settings menu
|
||||||
|
|
||||||
|
### 11.7 Thinking Indicator
|
||||||
|
|
||||||
|
Animated indicator when Claude is processing:
|
||||||
|
- Three bouncing dots (●●●)
|
||||||
|
- Separate from streaming content display
|
||||||
|
- Disappears when response completes or permission requested
|
||||||
|
|
||||||
|
### 11.8 Auto-Scroll Control
|
||||||
|
|
||||||
|
- **Default:** Enabled
|
||||||
|
- **Persistence:** Saved to localStorage (`spiceflow-auto-scroll`)
|
||||||
|
- **Toggle:** Available in session settings menu
|
||||||
|
- **Behavior:** Scrolls to bottom on new content
|
||||||
|
- **Override:** Ctrl+Down forces scroll regardless of setting
|
||||||
|
|
||||||
|
### 11.9 Session Status Indicators
|
||||||
|
|
||||||
|
| Status | Indicator |
|
||||||
|
|--------|-----------|
|
||||||
|
| Idle | Gray dot (static) |
|
||||||
|
| Processing | Green dot (pulsing) |
|
||||||
|
| Awaiting Permission | Amber dot (pulsing) |
|
||||||
|
| Tmux Alive | Green dot |
|
||||||
|
| Tmux Dead | Gray dot |
|
||||||
|
|
||||||
|
### 11.10 Markdown Rendering
|
||||||
|
|
||||||
|
Assistant messages render Markdown with:
|
||||||
|
- Headings (h1-h6) with appropriate sizing
|
||||||
|
- Code blocks with monospace font and background
|
||||||
|
- Inline code with background highlight
|
||||||
|
- Lists (ordered and unordered)
|
||||||
|
- Blockquotes with left border
|
||||||
|
- Links styled in orange (spice color)
|
||||||
|
- Line breaks preserved (`breaks: true`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1262,6 +1497,38 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext
|
|||||||
8. User taps notification → app opens to session
|
8. User taps notification → app opens to session
|
||||||
9. User sees permission UI, responds
|
9. User sees permission UI, responds
|
||||||
|
|
||||||
|
### 12.6 Import External Tmux Session
|
||||||
|
|
||||||
|
1. User has existing tmux session running (e.g., `dev-session`)
|
||||||
|
2. User opens Spiceflow home page
|
||||||
|
3. "Import tmux session" button appears
|
||||||
|
4. User clicks import → dropdown shows available sessions
|
||||||
|
5. User selects `dev-session`
|
||||||
|
6. Server renames to `spiceflow-dev-session`
|
||||||
|
7. Session appears in Spiceflow list
|
||||||
|
8. User can now manage session via Spiceflow
|
||||||
|
|
||||||
|
### 12.7 Eject Tmux Session
|
||||||
|
|
||||||
|
1. User opens tmux session in Spiceflow
|
||||||
|
2. User clicks settings gear (⚙️)
|
||||||
|
3. User clicks "Eject session"
|
||||||
|
4. Confirmation alert shows reattach command
|
||||||
|
5. Session renamed from `spiceflow-{name}` to `{name}`
|
||||||
|
6. User redirected to home page
|
||||||
|
7. Session removed from Spiceflow but continues running
|
||||||
|
8. User can reattach via `tmux attach -t {name}`
|
||||||
|
|
||||||
|
### 12.8 Rotate Phone (Terminal)
|
||||||
|
|
||||||
|
1. User viewing terminal session on phone
|
||||||
|
2. User rotates phone from portrait to landscape
|
||||||
|
3. `screen.orientation` change event fires
|
||||||
|
4. App detects orientation is now landscape
|
||||||
|
5. Terminal automatically resizes to landscape dimensions (88x24)
|
||||||
|
6. Fresh terminal content fetched after 150ms
|
||||||
|
7. UI updates to show landscape-specific controls
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Security Considerations
|
## 13. Security Considerations
|
||||||
|
|||||||
+26
-1
@@ -3,7 +3,11 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,4 +79,25 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide desktop orientation icons on mobile (width < 640px or landscape) */
|
||||||
|
@media (max-width: 639px), (max-height: 450px) {
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide extra buttons on mobile portrait */
|
||||||
|
@media (max-width: 639px) and (min-height: 451px) {
|
||||||
|
.portrait-hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide mobile orientation buttons on XL desktop (>= 1280px) */
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
export let autoAcceptEdits: boolean = false;
|
export let autoAcceptEdits: boolean = false;
|
||||||
export let autoScroll: boolean = true;
|
export let autoScroll: boolean = true;
|
||||||
export let provider: 'claude' | 'opencode' | 'tmux' = 'claude';
|
export let provider: 'claude' | 'opencode' | 'tmux' = 'claude';
|
||||||
|
export let showBigMode: boolean = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
toggleAutoAccept: boolean;
|
toggleAutoAccept: boolean;
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
condenseAll: void;
|
condenseAll: void;
|
||||||
refresh: void;
|
refresh: void;
|
||||||
eject: void;
|
eject: void;
|
||||||
|
bigMode: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function handleToggleAutoScroll() {
|
function handleToggleAutoScroll() {
|
||||||
@@ -18,6 +20,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
|
let keybindsHovered = false;
|
||||||
|
|
||||||
|
const keybinds = [
|
||||||
|
{ keys: 'Ctrl+Shift+R', action: 'Refresh session' },
|
||||||
|
{ keys: 'Ctrl+↓', action: 'Scroll to bottom', terminal: true },
|
||||||
|
{ keys: 'Ctrl+Shift+V', action: 'Paste', terminal: true },
|
||||||
|
{ keys: 'Ctrl+C', action: 'Interrupt', terminal: true }
|
||||||
|
];
|
||||||
|
|
||||||
function handleToggle() {
|
function handleToggle() {
|
||||||
const newValue = !autoAcceptEdits;
|
const newValue = !autoAcceptEdits;
|
||||||
@@ -39,6 +49,11 @@
|
|||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBigMode() {
|
||||||
|
dispatch('bigMode');
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (!target.closest('.settings-dropdown')) {
|
if (!target.closest('.settings-dropdown')) {
|
||||||
@@ -74,7 +89,7 @@
|
|||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="absolute right-0 top-full mt-1 bg-zinc-800 border border-zinc-700 rounded-lg shadow-xl min-w-[240px] overflow-hidden z-50"
|
class="absolute right-0 top-full mt-1 bg-zinc-800 border border-zinc-700 rounded-lg shadow-xl min-w-[240px] z-50"
|
||||||
>
|
>
|
||||||
<div class="px-3 py-2 border-b border-zinc-700">
|
<div class="px-3 py-2 border-b border-zinc-700">
|
||||||
<span class="text-sm font-medium text-zinc-200">Session Settings</span>
|
<span class="text-sm font-medium text-zinc-200">Session Settings</span>
|
||||||
@@ -98,6 +113,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- Big mode (tmux on mobile only) -->
|
||||||
|
{#if showBigMode}
|
||||||
|
<button
|
||||||
|
on:click={handleBigMode}
|
||||||
|
class="w-full flex items-start gap-3 px-3 py-2.5 hover:bg-zinc-700/50 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4 text-cyan-400 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
||||||
|
</svg>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<span class="text-sm text-zinc-200 block">Big mode</span>
|
||||||
|
<span class="text-xs text-zinc-500 block mt-0.5">Desktop sizing at 70% zoom</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Refresh button (all providers) -->
|
<!-- Refresh button (all providers) -->
|
||||||
<button
|
<button
|
||||||
on:click={handleRefresh}
|
on:click={handleRefresh}
|
||||||
@@ -140,6 +171,43 @@
|
|||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Keyboard shortcuts -->
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
on:mouseenter={() => (keybindsHovered = true)}
|
||||||
|
on:mouseleave={() => (keybindsHovered = false)}
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-700/50 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<svg class="h-4 w-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<rect x="2" y="6" width="20" height="12" rx="2" stroke-width="2" />
|
||||||
|
<path stroke-linecap="round" stroke-width="2" d="M6 10h2M10 10h4M18 10h-2M6 14h12" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm text-zinc-200">Keyboard shortcuts</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if keybindsHovered}
|
||||||
|
<div
|
||||||
|
class="absolute right-full top-0 mr-1 bg-zinc-800 border border-zinc-700 rounded-lg shadow-xl min-w-[220px] z-[60]"
|
||||||
|
>
|
||||||
|
<div class="px-3 py-2 border-b border-zinc-700">
|
||||||
|
<span class="text-sm font-medium text-zinc-200">Shortcuts</span>
|
||||||
|
</div>
|
||||||
|
{#each keybinds as kb}
|
||||||
|
<div class="flex items-center justify-between gap-3 px-3 py-2 text-sm">
|
||||||
|
<span class="text-zinc-400">{kb.action}{kb.terminal ? ' (terminal)' : ''}</span>
|
||||||
|
<kbd class="px-1.5 py-0.5 bg-zinc-700 border border-zinc-600 rounded text-xs text-zinc-300 font-mono whitespace-nowrap">{kb.keys}</kbd>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if provider === 'tmux'}
|
{#if provider === 'tmux'}
|
||||||
<button
|
<button
|
||||||
on:click={handleEject}
|
on:click={handleEject}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
let lastHash: number | null = null;
|
let lastHash: number | null = null;
|
||||||
let lastFrameId: number | null = null; // Track frame ordering to prevent out-of-order updates
|
let lastFrameId: number | null = null; // Track frame ordering to prevent out-of-order updates
|
||||||
let initialLoadComplete = false; // Track whether initial load has happened
|
let initialLoadComplete = false; // Track whether initial load has happened
|
||||||
let screenMode: 'fullscreen' | 'desktop' | 'landscape' | 'portrait' = 'landscape';
|
let screenMode: 'fullscreen' | 'desktop' | 'landscape' | 'portrait' | null = null;
|
||||||
let resizing = false;
|
let resizing = false;
|
||||||
let inputBuffer = '';
|
let inputBuffer = '';
|
||||||
let batchTimeout: ReturnType<typeof setTimeout> | null = null;
|
let batchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -61,6 +61,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Big mode: desktop sizing (120x36) with 70% zoom for mobile users who want more content
|
||||||
|
export function setBigMode(enabled: boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
fontScale = 0.7;
|
||||||
|
resizeScreen('desktop');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Computed content from lines
|
// Computed content from lines
|
||||||
$: terminalContent = terminalLines.join('\n');
|
$: terminalContent = terminalLines.join('\n');
|
||||||
|
|
||||||
@@ -372,6 +380,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleOrientationChange() {
|
||||||
|
// Only auto-switch on mobile (screen width < 640px or height < 450px)
|
||||||
|
const isMobile = window.innerWidth < 640 || window.innerHeight < 450;
|
||||||
|
if (!isMobile) return;
|
||||||
|
|
||||||
|
const orientation = screen.orientation?.type || '';
|
||||||
|
if (orientation.includes('portrait')) {
|
||||||
|
resizeScreen('portrait');
|
||||||
|
} else if (orientation.includes('landscape')) {
|
||||||
|
resizeScreen('landscape');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Initial fetch with fresh=true to ensure we get full current content
|
// Initial fetch with fresh=true to ensure we get full current content
|
||||||
await fetchTerminalContent(true);
|
await fetchTerminalContent(true);
|
||||||
@@ -387,6 +408,11 @@
|
|||||||
if (autoFocus) {
|
if (autoFocus) {
|
||||||
setTimeout(() => terminalInput?.focus(), 100);
|
setTimeout(() => terminalInput?.focus(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for orientation changes on mobile
|
||||||
|
if (screen.orientation) {
|
||||||
|
screen.orientation.addEventListener('change', handleOrientationChange);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -399,6 +425,9 @@
|
|||||||
if (batchTimeout) {
|
if (batchTimeout) {
|
||||||
clearTimeout(batchTimeout);
|
clearTimeout(batchTimeout);
|
||||||
}
|
}
|
||||||
|
if (screen.orientation) {
|
||||||
|
screen.orientation.removeEventListener('change', handleOrientationChange);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -431,7 +460,7 @@
|
|||||||
<button
|
<button
|
||||||
on:click={() => ctrlMode = !ctrlMode}
|
on:click={() => ctrlMode = !ctrlMode}
|
||||||
disabled={!isAlive}
|
disabled={!isAlive}
|
||||||
class="{btnBase} bg-zinc-700 hover:bg-zinc-600 text-zinc-200 {ctrlMode ? 'font-bold ring-1 ring-cyan-400 rounded' : 'rounded-sm'}"
|
class="portrait-hide {btnBase} bg-zinc-700 hover:bg-zinc-600 text-zinc-200 {ctrlMode ? 'font-bold ring-1 ring-cyan-400 rounded' : 'rounded-sm'}"
|
||||||
>^</button>
|
>^</button>
|
||||||
<button
|
<button
|
||||||
on:click={sendCtrlC}
|
on:click={sendCtrlC}
|
||||||
@@ -443,23 +472,23 @@
|
|||||||
disabled={!isAlive}
|
disabled={!isAlive}
|
||||||
class={btnAmber}
|
class={btnAmber}
|
||||||
>^D</button>
|
>^D</button>
|
||||||
<span class="w-px h-6 bg-zinc-700"></span>
|
<span class="w-px h-6 bg-zinc-700 portrait-hide"></span>
|
||||||
<button
|
<button
|
||||||
on:click={() => sendKey('y')}
|
on:click={() => sendKey('y')}
|
||||||
disabled={!isAlive}
|
disabled={!isAlive}
|
||||||
class={btnGreen}
|
class="portrait-hide {btnGreen}"
|
||||||
>y</button>
|
>y</button>
|
||||||
<button
|
<button
|
||||||
on:click={() => sendKey('n')}
|
on:click={() => sendKey('n')}
|
||||||
disabled={!isAlive}
|
disabled={!isAlive}
|
||||||
class={btnRed}
|
class="portrait-hide {btnRed}"
|
||||||
>n</button>
|
>n</button>
|
||||||
<span class="w-px h-6 bg-zinc-700"></span>
|
<span class="w-px h-6 bg-zinc-700 portrait-hide"></span>
|
||||||
{#each ['1', '2', '3', '4'] as num}
|
{#each ['1', '2', '3', '4'] as num}
|
||||||
<button
|
<button
|
||||||
on:click={() => sendKey(num)}
|
on:click={() => sendKey(num)}
|
||||||
disabled={!isAlive}
|
disabled={!isAlive}
|
||||||
class={btnDefault}
|
class="portrait-hide {btnDefault}"
|
||||||
>{num}</button>
|
>{num}</button>
|
||||||
{/each}
|
{/each}
|
||||||
<span class="w-px h-6 bg-zinc-700"></span>
|
<span class="w-px h-6 bg-zinc-700"></span>
|
||||||
@@ -478,11 +507,11 @@
|
|||||||
disabled={!isAlive}
|
disabled={!isAlive}
|
||||||
class={btnGreen}
|
class={btnGreen}
|
||||||
>↵</button>
|
>↵</button>
|
||||||
<span class="w-px h-6 bg-zinc-700"></span>
|
<span class="w-px h-6 bg-zinc-700 portrait-hide"></span>
|
||||||
<button
|
<button
|
||||||
on:click={pasteFromClipboard}
|
on:click={pasteFromClipboard}
|
||||||
disabled={!isAlive}
|
disabled={!isAlive}
|
||||||
class={btnViolet}
|
class="portrait-hide {btnViolet}"
|
||||||
title="Paste (Ctrl+Shift+V)"
|
title="Paste (Ctrl+Shift+V)"
|
||||||
>📋</button>
|
>📋</button>
|
||||||
<!-- Screen size selector (pushed right on wider screens) -->
|
<!-- Screen size selector (pushed right on wider screens) -->
|
||||||
@@ -495,7 +524,11 @@
|
|||||||
class={btnDefault}
|
class={btnDefault}
|
||||||
title="Zoom out"
|
title="Zoom out"
|
||||||
>-</button>
|
>-</button>
|
||||||
<span class="text-xs text-zinc-400 font-mono w-8 text-center">{Math.round(fontScale * 100)}%</span>
|
<button
|
||||||
|
on:click={() => fontScale = 1}
|
||||||
|
class="text-xs text-zinc-400 hover:text-zinc-200 font-mono w-8 text-center transition-colors"
|
||||||
|
title="Reset to 100%"
|
||||||
|
>{Math.round(fontScale * 100)}%</button>
|
||||||
<button
|
<button
|
||||||
on:click={zoomIn}
|
on:click={zoomIn}
|
||||||
disabled={fontScale >= fontScales[fontScales.length - 1]}
|
disabled={fontScale >= fontScales[fontScales.length - 1]}
|
||||||
@@ -507,8 +540,8 @@
|
|||||||
<button
|
<button
|
||||||
on:click={() => resizeScreen('portrait')}
|
on:click={() => resizeScreen('portrait')}
|
||||||
disabled={resizing}
|
disabled={resizing}
|
||||||
class="{btnBase} {screenMode === 'portrait' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
class="mobile-only {btnBase} {screenMode === 'portrait' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
||||||
title="Portrait (50x60)"
|
title="Portrait (42x24)"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-3 inline-block" fill="none" viewBox="0 0 10 16" stroke="currentColor" stroke-width="1.5">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-3 inline-block" fill="none" viewBox="0 0 10 16" stroke="currentColor" stroke-width="1.5">
|
||||||
<rect x="1" y="1" width="8" height="14" rx="1" />
|
<rect x="1" y="1" width="8" height="14" rx="1" />
|
||||||
@@ -517,8 +550,8 @@
|
|||||||
<button
|
<button
|
||||||
on:click={() => resizeScreen('landscape')}
|
on:click={() => resizeScreen('landscape')}
|
||||||
disabled={resizing}
|
disabled={resizing}
|
||||||
class="{btnBase} {screenMode === 'landscape' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
class="mobile-only {btnBase} {screenMode === 'landscape' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
||||||
title="Landscape (80x24)"
|
title="Landscape (86x24)"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-5 inline-block" fill="none" viewBox="0 0 16 10" stroke="currentColor" stroke-width="1.5">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-5 inline-block" fill="none" viewBox="0 0 16 10" stroke="currentColor" stroke-width="1.5">
|
||||||
<rect x="1" y="1" width="14" height="8" rx="1" />
|
<rect x="1" y="1" width="14" height="8" rx="1" />
|
||||||
@@ -527,8 +560,8 @@
|
|||||||
<button
|
<button
|
||||||
on:click={() => resizeScreen('desktop')}
|
on:click={() => resizeScreen('desktop')}
|
||||||
disabled={resizing}
|
disabled={resizing}
|
||||||
class="{btnBase} {screenMode === 'desktop' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
class="desktop-only {btnBase} {screenMode === 'desktop' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
||||||
title="Split screen (120x48)"
|
title="Split screen (120x36)"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-5 inline-block" fill="none" viewBox="0 0 20 14" stroke="currentColor" stroke-width="1.5">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-5 inline-block" fill="none" viewBox="0 0 20 14" stroke="currentColor" stroke-width="1.5">
|
||||||
<rect x="1" y="1" width="8" height="12" rx="1" />
|
<rect x="1" y="1" width="8" height="12" rx="1" />
|
||||||
@@ -538,8 +571,8 @@
|
|||||||
<button
|
<button
|
||||||
on:click={() => resizeScreen('fullscreen')}
|
on:click={() => resizeScreen('fullscreen')}
|
||||||
disabled={resizing}
|
disabled={resizing}
|
||||||
class="{btnBase} {screenMode === 'fullscreen' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
class="desktop-only {btnBase} {screenMode === 'fullscreen' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
|
||||||
title="Fullscreen (260x48)"
|
title="Fullscreen (260x36)"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-6 inline-block" fill="none" viewBox="0 0 22 14" stroke="currentColor" stroke-width="1.5">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-6 inline-block" fill="none" viewBox="0 0 22 14" stroke="currentColor" stroke-width="1.5">
|
||||||
<rect x="1" y="1" width="20" height="12" rx="1" />
|
<rect x="1" y="1" width="20" height="12" rx="1" />
|
||||||
|
|||||||
@@ -40,6 +40,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col bg-zinc-900 text-zinc-100 safe-top overflow-hidden" style="height: {containerHeight};">
|
<div class="flex flex-col w-full bg-zinc-900 text-zinc-100 safe-top overflow-hidden" style="height: {containerHeight};">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,14 +23,22 @@
|
|||||||
let menuOpen = false;
|
let menuOpen = false;
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let tmuxAlive = false;
|
let tmuxAlive = false;
|
||||||
|
let isMobile = false;
|
||||||
|
|
||||||
// Load auto-scroll preference from localStorage
|
// Load auto-scroll preference from localStorage and detect mobile
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const stored = localStorage.getItem('spiceflow-auto-scroll');
|
const stored = localStorage.getItem('spiceflow-auto-scroll');
|
||||||
if (stored !== null) {
|
if (stored !== null) {
|
||||||
autoScroll = stored === 'true';
|
autoScroll = stored === 'true';
|
||||||
}
|
}
|
||||||
|
// Detect mobile (screen width < 640px or height < 450px)
|
||||||
|
const checkMobile = () => {
|
||||||
|
isMobile = window.innerWidth < 640 || window.innerHeight < 450;
|
||||||
|
};
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
return () => window.removeEventListener('resize', checkMobile);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,6 +157,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBigMode() {
|
||||||
|
terminalView?.setBigMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
idle: 'bg-zinc-600',
|
idle: 'bg-zinc-600',
|
||||||
processing: 'bg-green-500 animate-pulse',
|
processing: 'bg-green-500 animate-pulse',
|
||||||
@@ -163,13 +175,20 @@
|
|||||||
$: statusIndicator = isTmuxSession
|
$: statusIndicator = isTmuxSession
|
||||||
? (tmuxAlive ? 'bg-green-500' : 'bg-zinc-600')
|
? (tmuxAlive ? 'bg-green-500' : 'bg-zinc-600')
|
||||||
: (statusColors[session?.status || 'idle'] || statusColors.idle);
|
: (statusColors[session?.status || 'idle'] || statusColors.idle);
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.ctrlKey && event.shiftKey && event.key === 'R') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{session?.title || `Session ${shortId}`} - Spiceflow</title>
|
<title>{session?.title || `Session ${shortId}`} - Spiceflow</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<svelte:window on:click={() => (menuOpen = false)} />
|
<svelte:window on:click={() => (menuOpen = false)} on:keydown={handleKeydown} />
|
||||||
|
|
||||||
<!-- Header - Full (portrait) -->
|
<!-- Header - Full (portrait) -->
|
||||||
<header class="flex-shrink-0 border-b border-zinc-800 px-4 py-3 landscape-mobile:hidden">
|
<header class="flex-shrink-0 border-b border-zinc-800 px-4 py-3 landscape-mobile:hidden">
|
||||||
@@ -218,13 +237,27 @@
|
|||||||
{autoAcceptEdits}
|
{autoAcceptEdits}
|
||||||
{autoScroll}
|
{autoScroll}
|
||||||
provider={session.provider}
|
provider={session.provider}
|
||||||
|
showBigMode={isTmuxSession && isMobile}
|
||||||
on:toggleAutoAccept={handleToggleAutoAccept}
|
on:toggleAutoAccept={handleToggleAutoAccept}
|
||||||
on:toggleAutoScroll={handleToggleAutoScroll}
|
on:toggleAutoScroll={handleToggleAutoScroll}
|
||||||
on:condenseAll={() => messageList?.condenseAll()}
|
on:condenseAll={() => messageList?.condenseAll()}
|
||||||
on:refresh={handleRefresh}
|
on:refresh={handleRefresh}
|
||||||
on:eject={handleEject}
|
on:eject={handleEject}
|
||||||
|
on:bigMode={handleBigMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Refresh button - desktop only -->
|
||||||
|
<button
|
||||||
|
on:click={handleRefresh}
|
||||||
|
class="hidden sm:block p-1.5 hover:bg-zinc-700 rounded transition-colors"
|
||||||
|
aria-label="Refresh"
|
||||||
|
title="Refresh"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<span class="text-xs font-medium uppercase {providerColors[session.provider] || 'text-zinc-400'}">
|
<span class="text-xs font-medium uppercase {providerColors[session.provider] || 'text-zinc-400'}">
|
||||||
{session.provider === 'tmux' ? 'terminal' : session.provider}
|
{session.provider === 'tmux' ? 'terminal' : session.provider}
|
||||||
</span>
|
</span>
|
||||||
@@ -273,6 +306,17 @@
|
|||||||
/>
|
/>
|
||||||
<span>Auto-scroll</span>
|
<span>Auto-scroll</span>
|
||||||
</label>
|
</label>
|
||||||
|
{#if isTmuxSession && isMobile}
|
||||||
|
<button
|
||||||
|
on:click={() => { menuOpen = false; handleBigMode(); }}
|
||||||
|
class="w-full px-3 py-2 text-left text-sm hover:bg-zinc-700 flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
||||||
|
</svg>
|
||||||
|
Big mode
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
<button
|
<button
|
||||||
on:click={() => { menuOpen = false; handleRefresh(); }}
|
on:click={() => { menuOpen = false; handleRefresh(); }}
|
||||||
class="w-full px-3 py-2 text-left text-sm hover:bg-zinc-700 flex items-center gap-2 transition-colors"
|
class="w-full px-3 py-2 text-left text-sm hover:bg-zinc-700 flex items-center gap-2 transition-colors"
|
||||||
|
|||||||
@@ -339,10 +339,10 @@
|
|||||||
|
|
||||||
;; Screen size presets for different device orientations
|
;; Screen size presets for different device orientations
|
||||||
(def ^:private screen-sizes
|
(def ^:private screen-sizes
|
||||||
{:fullscreen {:width 260 :height 48}
|
{:fullscreen {:width 260 :height 36}
|
||||||
:desktop {:width 120 :height 48}
|
:desktop {:width 120 :height 36}
|
||||||
:landscape {:width 80 :height 24}
|
:landscape {:width 86 :height 24}
|
||||||
:portrait {:width 40 :height 24}})
|
:portrait {:width 42 :height 24}})
|
||||||
|
|
||||||
(defn resize-session
|
(defn resize-session
|
||||||
"Resize a tmux session window to a preset size.
|
"Resize a tmux session window to a preset size.
|
||||||
|
|||||||
Reference in New Issue
Block a user