From a2e10688bffb8a400608cde67c0f87e5591d76f2 Mon Sep 17 00:00:00 2001 From: Adam Jeniski Date: Wed, 21 Jan 2026 00:20:40 -0500 Subject: [PATCH] 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 --- PRD.md | 285 +++++++++++++++++- client/src/app.css | 27 +- .../src/lib/components/SessionSettings.svelte | 70 ++++- client/src/lib/components/TerminalView.svelte | 69 +++-- client/src/routes/+layout.svelte | 2 +- client/src/routes/session/[id]/+page.svelte | 48 ++- server/src/spiceflow/adapters/tmux.clj | 8 +- 7 files changed, 473 insertions(+), 36 deletions(-) diff --git a/PRD.md b/PRD.md index 7ec456d..eaef34c 100644 --- a/PRD.md +++ b/PRD.md @@ -339,6 +339,38 @@ stream_session_response(session_id, callback) - Filters for `spiceflow-*` prefix - 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 ```clojure @@ -572,6 +604,11 @@ Permissions are recorded as assistant messages with metadata: - **Examples:** `spiceflow-brave-fox-0042`, `spiceflow-calm-owl-1337` - **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 ```bash @@ -635,10 +672,96 @@ tmux capture-pane -t {session-name} -p -e -S -1000 | Mode | Dimensions | Use Case | |------|------------|----------| -| `portrait` | 40x24 | Phone portrait | -| `landscape` | 65x24 | Phone landscape | -| `desktop` | 100x24 | Split screen | -| `fullscreen` | 180x24 | Full terminal | +| `portrait` | 42x24 | Phone portrait | +| `landscape` | 86x24 | Phone landscape | +| `desktop` | 120x36 | Split screen | +| `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 @@ -1085,6 +1225,24 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext - **Heartbeat:** Ping every 25 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 @@ -1097,6 +1255,9 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext ┌─────────────────────────────────────────┐ │ ☰ Spiceflow 🔔 + ↻ │ ├─────────────────────────────────────────┤ +│ ┌─────────────────────────────────┐ │ +│ │ ● 2 sessions processing │ │ (green badge, pulsing) +│ └─────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ ● My Claude Session claude │ │ @@ -1108,14 +1269,19 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext │ │ /home/user 1d ago │ │ │ └─────────────────────────────────┘ │ │ │ +│ ┌─────────────────────────────────┐ │ +│ │ + Import tmux session │ │ (if external sessions exist) +│ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` **Elements:** - Header with branding and action buttons +- Processing sessions counter (green pulsing badge) - Session cards with status indicators - Provider badges (color-coded) - Relative timestamps +- Import button for external tmux sessions #### 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):** ``` ┌─────────────────────────────────────────┐ @@ -1164,7 +1335,7 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext │ ⚠️ Claude needs permission │ ├─────────────────────────────────────────┤ │ │ -│ Write: /home/user/foo.md │ +│ Write: /home/user/foo.md .md │ (file extension badge) │ ┌─────────────────────────────────┐ │ │ │ + 1 # My Haiku │ │ │ │ + 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 | Component | Purpose | Props | @@ -1194,9 +1373,65 @@ salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext | Breakpoint | Header | Layout | |------------|--------|--------| -| Portrait | Full header | Vertical stack | -| Landscape mobile | Hamburger menu | Compact header | -| Desktop | Full header | All controls visible | +| Portrait (<640px width, >450px height) | Full header | Vertical stack | +| Landscape mobile (<450px height) | Hamburger menu | Compact header | +| 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 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 diff --git a/client/src/app.css b/client/src/app.css index e2f901a..5392076 100644 --- a/client/src/app.css +++ b/client/src/app.css @@ -3,7 +3,11 @@ @tailwind utilities; @layer base { - html { + html, body { + margin: 0; + padding: 0; + width: 100%; + min-height: 100%; -webkit-tap-highlight-color: transparent; } @@ -75,4 +79,25 @@ 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; + } + } } diff --git a/client/src/lib/components/SessionSettings.svelte b/client/src/lib/components/SessionSettings.svelte index 78ec67e..8e9cf58 100644 --- a/client/src/lib/components/SessionSettings.svelte +++ b/client/src/lib/components/SessionSettings.svelte @@ -4,6 +4,7 @@ export let autoAcceptEdits: boolean = false; export let autoScroll: boolean = true; export let provider: 'claude' | 'opencode' | 'tmux' = 'claude'; + export let showBigMode: boolean = false; const dispatch = createEventDispatcher<{ toggleAutoAccept: boolean; @@ -11,6 +12,7 @@ condenseAll: void; refresh: void; eject: void; + bigMode: void; }>(); function handleToggleAutoScroll() { @@ -18,6 +20,14 @@ } 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() { const newValue = !autoAcceptEdits; @@ -39,6 +49,11 @@ open = false; } + function handleBigMode() { + dispatch('bigMode'); + open = false; + } + function handleClickOutside(event: MouseEvent) { const target = event.target as HTMLElement; if (!target.closest('.settings-dropdown')) { @@ -74,7 +89,7 @@ {#if open}
Session Settings @@ -98,6 +113,22 @@
+ + {#if showBigMode} + + {/if} + + + {#if keybindsHovered} +
+
+ Shortcuts +
+ {#each keybinds as kb} +
+ {kb.action}{kb.terminal ? ' (terminal)' : ''} + {kb.keys} +
+ {/each} +
+ {/if} +
+ {#if provider === 'tmux'} - + - + {#each ['1', '2', '3', '4'] as num} {/each} @@ -478,11 +507,11 @@ disabled={!isAlive} class={btnGreen} >↵ - + @@ -495,7 +524,11 @@ class={btnDefault} title="Zoom out" >- - {Math.round(fontScale * 100)}% + + {session.provider === 'tmux' ? 'terminal' : session.provider} @@ -273,6 +306,17 @@ /> Auto-scroll + {#if isTmuxSession && isMobile} + + {/if}