Compare commits
3 Commits
v0.2.22-wi
...
v0.2.25
| Author | SHA1 | Date | |
|---|---|---|---|
| 57a7cee544 | |||
| 6369f7e0a8 | |||
| 9ee0d34c19 |
@@ -62,7 +62,7 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
|||||||
- **`components/terminal/TerminalView.tsx`** — xterm.js integration with WebGL rendering, URL detection for OAuth flow
|
- **`components/terminal/TerminalView.tsx`** — xterm.js integration with WebGL rendering, URL detection for OAuth flow
|
||||||
- **`components/layout/`** — TopBar (tabs + status), Sidebar (project list), StatusBar
|
- **`components/layout/`** — TopBar (tabs + status), Sidebar (project list), StatusBar
|
||||||
- **`components/projects/`** — ProjectCard, ProjectList, AddProjectDialog
|
- **`components/projects/`** — ProjectCard, ProjectList, AddProjectDialog
|
||||||
- **`components/settings/`** — Settings panels for API keys, Docker, AWS
|
- **`components/settings/`** — Settings panels for API keys, Docker, AWS, Web Terminal
|
||||||
|
|
||||||
### Backend Structure (`app/src-tauri/src/`)
|
### Backend Structure (`app/src-tauri/src/`)
|
||||||
|
|
||||||
@@ -72,7 +72,11 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
|||||||
- `container.rs` — Container lifecycle (create, start, stop, remove, inspect)
|
- `container.rs` — Container lifecycle (create, start, stop, remove, inspect)
|
||||||
- `exec.rs` — PTY exec sessions with bidirectional stdin/stdout streaming
|
- `exec.rs` — PTY exec sessions with bidirectional stdin/stdout streaming
|
||||||
- `image.rs` — Image build/pull with progress streaming
|
- `image.rs` — Image build/pull with progress streaming
|
||||||
- **`models/`** — Serde structs (`Project`, `Backend`, `BedrockConfig`, `OllamaConfig`, `OpenAiCompatibleConfig`, `ContainerInfo`, `AppSettings`). These define the IPC contract with the frontend.
|
- **`web_terminal/`** — Remote terminal access via axum HTTP+WebSocket server:
|
||||||
|
- `server.rs` — Axum server lifecycle (start/stop), serves embedded HTML and handles WS upgrades
|
||||||
|
- `ws_handler.rs` — Per-connection WebSocket handler with JSON protocol, session management, cleanup on disconnect
|
||||||
|
- `terminal.html` — Self-contained xterm.js web UI embedded via `include_str!()`
|
||||||
|
- **`models/`** — Serde structs (`Project`, `Backend`, `BedrockConfig`, `OllamaConfig`, `OpenAiCompatibleConfig`, `ContainerInfo`, `AppSettings`, `WebTerminalSettings`). These define the IPC contract with the frontend.
|
||||||
- **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs`
|
- **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs`
|
||||||
|
|
||||||
### Container (`container/`)
|
### Container (`container/`)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Triple-C (Claude-Code-Container) is a desktop application that runs Claude Code
|
|||||||
- [Ollama Configuration](#ollama-configuration)
|
- [Ollama Configuration](#ollama-configuration)
|
||||||
- [OpenAI Compatible Configuration](#openai-compatible-configuration)
|
- [OpenAI Compatible Configuration](#openai-compatible-configuration)
|
||||||
- [Settings](#settings)
|
- [Settings](#settings)
|
||||||
|
- [Web Terminal (Remote Access)](#web-terminal-remote-access)
|
||||||
- [Terminal Features](#terminal-features)
|
- [Terminal Features](#terminal-features)
|
||||||
- [Scheduled Tasks (Inside the Container)](#scheduled-tasks-inside-the-container)
|
- [Scheduled Tasks (Inside the Container)](#scheduled-tasks-inside-the-container)
|
||||||
- [What's Inside the Container](#whats-inside-the-container)
|
- [What's Inside the Container](#whats-inside-the-container)
|
||||||
@@ -480,6 +481,17 @@ Instructions applied to **all** projects. Written to `~/.claude/CLAUDE.md` in ev
|
|||||||
|
|
||||||
Environment variables applied to **all** project containers. Per-project variables with the same key take precedence.
|
Environment variables applied to **all** project containers. Per-project variables with the same key take precedence.
|
||||||
|
|
||||||
|
### Web Terminal
|
||||||
|
|
||||||
|
Enable remote access to your project terminals from any device on the local network (tablets, phones, other computers).
|
||||||
|
|
||||||
|
- **Toggle** — Click ON/OFF to start or stop the web terminal server.
|
||||||
|
- **URL** — When running, shows the full URL including the access token. Click **Copy URL** to copy it to your clipboard, then open it in a browser on your tablet or phone.
|
||||||
|
- **Token** — An access token is auto-generated on first enable. Click **Copy** to copy the token, or **Regenerate** to create a new one (this disconnects existing web sessions).
|
||||||
|
- **Port** — Defaults to 7681. Configurable in `settings.json` if needed.
|
||||||
|
|
||||||
|
The web terminal server auto-starts on app launch if it was previously enabled, and stops when the app closes.
|
||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
|
|
||||||
- **Current Version** — The installed version of Triple-C.
|
- **Current Version** — The installed version of Triple-C.
|
||||||
@@ -490,6 +502,43 @@ When an update is available, a pulsing **Update** button appears in the top bar.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Web Terminal (Remote Access)
|
||||||
|
|
||||||
|
The web terminal lets you access your running project terminals from a tablet, phone, or any other device on the local network — no app installation required, just a web browser.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Go to **Settings** in the sidebar.
|
||||||
|
2. Find the **Web Terminal** section and click the toggle to **ON**.
|
||||||
|
3. A URL appears (e.g., `http://192.168.1.100:7681?token=...`). Click **Copy URL**.
|
||||||
|
4. Open the URL in a browser on your tablet or other device.
|
||||||
|
|
||||||
|
### Using the Web Terminal
|
||||||
|
|
||||||
|
The web terminal UI mirrors the desktop app's terminal experience:
|
||||||
|
|
||||||
|
- **Project picker** — Select a running project from the dropdown at the top.
|
||||||
|
- **Claude / Bash buttons** — Open a new Claude Code or bash session for the selected project.
|
||||||
|
- **Tab bar** — Switch between multiple open sessions. Click the **x** on a tab to close it.
|
||||||
|
- **Input bar** — A text input at the bottom optimized for mobile/tablet keyboards. Characters are sent immediately without waiting for autocomplete. Helper buttons for **Enter**, **Tab**, and **^C** (Ctrl+C) are provided for keys that are awkward on virtual keyboards.
|
||||||
|
- **Scroll to bottom** — A floating arrow button appears when you scroll up, letting you jump back to the latest output.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Access requires a token in the URL query string. Without the correct token, connections are rejected.
|
||||||
|
- The token is auto-generated (32 bytes, base64url-encoded) and can be regenerated at any time from Settings.
|
||||||
|
- The server only listens on port 7681 (configurable) — make sure this port is not exposed to the public internet.
|
||||||
|
- All sessions opened from a browser tab are automatically cleaned up when the tab is closed or the WebSocket disconnects.
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
- **Bookmark the URL** on your tablet for quick access.
|
||||||
|
- The web terminal works best in landscape orientation on tablets.
|
||||||
|
- If the connection drops (e.g., Wi-Fi interruption), the web terminal auto-reconnects after 2 seconds.
|
||||||
|
- Regenerating the token invalidates all existing browser sessions — you'll need to update bookmarks with the new URL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Terminal Features
|
## Terminal Features
|
||||||
|
|
||||||
### Multiple Sessions
|
### Multiple Sessions
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -85,6 +85,18 @@ Triple-C supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io
|
|||||||
|
|
||||||
Optional per-project integration with [Flight Control](https://github.com/msieurthenardier/mission-control) — an AI-first development methodology. When enabled, the repo is cloned into the container, skills are installed, and workflow instructions are injected into CLAUDE.md.
|
Optional per-project integration with [Flight Control](https://github.com/msieurthenardier/mission-control) — an AI-first development methodology. When enabled, the repo is cloned into the container, skills are installed, and workflow instructions are injected into CLAUDE.md.
|
||||||
|
|
||||||
|
### Web Terminal (Remote Access)
|
||||||
|
|
||||||
|
Triple-C includes an optional web terminal server for accessing project terminals from tablets, phones, or other devices on the local network. When enabled in Settings, an axum HTTP+WebSocket server starts inside the Tauri process, serving a standalone xterm.js-based terminal UI.
|
||||||
|
|
||||||
|
- **URL**: `http://<LAN_IP>:7681?token=...` (port configurable)
|
||||||
|
- **Authentication**: Token-based (auto-generated, copyable from Settings)
|
||||||
|
- **Protocol**: JSON over WebSocket with base64-encoded terminal data
|
||||||
|
- **Features**: Project picker, multiple tabs (Claude + bash sessions), mobile-optimized input bar, scroll-to-bottom button
|
||||||
|
- **Session cleanup**: All terminal sessions are closed when the browser disconnects
|
||||||
|
|
||||||
|
The web terminal shares the existing `ExecSessionManager` via `Arc`-wrapped stores — same Docker exec sessions, different transport (WebSocket instead of Tauri IPC events).
|
||||||
|
|
||||||
### Docker Socket Path
|
### Docker Socket Path
|
||||||
|
|
||||||
The socket path is OS-aware:
|
The socket path is OS-aware:
|
||||||
@@ -108,7 +120,8 @@ Users can override this in Settings via the global `docker_socket_path` option.
|
|||||||
| `app/src/components/projects/ContainerProgressModal.tsx` | Real-time container operation progress |
|
| `app/src/components/projects/ContainerProgressModal.tsx` | Real-time container operation progress |
|
||||||
| `app/src/components/mcp/McpPanel.tsx` | MCP server library (global configuration) |
|
| `app/src/components/mcp/McpPanel.tsx` | MCP server library (global configuration) |
|
||||||
| `app/src/components/mcp/McpServerCard.tsx` | Individual MCP server configuration card |
|
| `app/src/components/mcp/McpServerCard.tsx` | Individual MCP server configuration card |
|
||||||
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, and global settings |
|
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, web terminal, and global settings |
|
||||||
|
| `app/src/components/settings/WebTerminalSettings.tsx` | Web terminal toggle, URL, token management |
|
||||||
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste |
|
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste |
|
||||||
| `app/src/components/terminal/TerminalTabs.tsx` | Tab bar for multiple terminal sessions (claude + bash) |
|
| `app/src/components/terminal/TerminalTabs.tsx` | Tab bar for multiple terminal sessions (claude + bash) |
|
||||||
| `app/src/hooks/useTerminal.ts` | Terminal session management (claude and bash modes) |
|
| `app/src/hooks/useTerminal.ts` | Terminal session management (claude and bash modes) |
|
||||||
@@ -124,7 +137,11 @@ Users can override this in Settings via the global `docker_socket_path` option.
|
|||||||
| `app/src-tauri/src/commands/mcp_commands.rs` | MCP server CRUD Tauri commands |
|
| `app/src-tauri/src/commands/mcp_commands.rs` | MCP server CRUD Tauri commands |
|
||||||
| `app/src-tauri/src/models/project.rs` | Project struct (backend, Docker access, MCP servers, Mission Control) |
|
| `app/src-tauri/src/models/project.rs` | Project struct (backend, Docker access, MCP servers, Mission Control) |
|
||||||
| `app/src-tauri/src/models/mcp_server.rs` | MCP server struct (transport, Docker image, env vars) |
|
| `app/src-tauri/src/models/mcp_server.rs` | MCP server struct (transport, Docker image, env vars) |
|
||||||
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, microphone) |
|
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, web terminal) |
|
||||||
|
| `app/src-tauri/src/web_terminal/server.rs` | Axum HTTP+WS server for remote terminal access |
|
||||||
|
| `app/src-tauri/src/web_terminal/ws_handler.rs` | WebSocket connection handler and session management |
|
||||||
|
| `app/src-tauri/src/web_terminal/terminal.html` | Embedded web UI (xterm.js, project picker, tabs) |
|
||||||
|
| `app/src-tauri/src/commands/web_terminal_commands.rs` | Web terminal start/stop/status Tauri commands |
|
||||||
| `app/src-tauri/src/storage/mcp_store.rs` | MCP server persistence (JSON with atomic writes) |
|
| `app/src-tauri/src/storage/mcp_store.rs` | MCP server persistence (JSON with atomic writes) |
|
||||||
| `container/Dockerfile` | Ubuntu 24.04 sandbox image with Claude Code + dev tools + clipboard/audio shims |
|
| `container/Dockerfile` | Ubuntu 24.04 sandbox image with Claude Code + dev tools + clipboard/audio shims |
|
||||||
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, MCP injection, Mission Control setup |
|
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, MCP injection, Mission Control setup |
|
||||||
|
|||||||
23
TECHNICAL.md
23
TECHNICAL.md
@@ -100,9 +100,13 @@ Tauri uses a Rust backend paired with a web-based frontend rendered by the OS-na
|
|||||||
│ │ Project Management │◄─┤ ProjectsStore │ │
|
│ │ Project Management │◄─┤ ProjectsStore │ │
|
||||||
│ │ Settings UI │ │ bollard Docker Client │ │
|
│ │ Settings UI │ │ bollard Docker Client │ │
|
||||||
│ │ │ │ keyring Credential Mgr │ │
|
│ │ │ │ keyring Credential Mgr │ │
|
||||||
│ └───────────┬───────────┘ └────────────┬─────────────┘ │
|
│ └───────────┬───────────┘ │ Web Terminal Server │ │
|
||||||
|
│ │ └────────────┬─────────────┘ │
|
||||||
│ │ Tauri IPC (invoke/emit) │ │
|
│ │ Tauri IPC (invoke/emit) │ │
|
||||||
│ └───────────┬───────────────┘ │
|
│ └───────────┬───────────────┘ │
|
||||||
|
│ ▲ │
|
||||||
|
│ axum HTTP+WS│(port 7681) │
|
||||||
|
│ │ │
|
||||||
└──────────────────────────┼───────────────────────────────┘
|
└──────────────────────────┼───────────────────────────────┘
|
||||||
│ Docker Socket
|
│ Docker Socket
|
||||||
▼
|
▼
|
||||||
@@ -129,6 +133,8 @@ The application uses two IPC mechanisms between the React frontend and Rust back
|
|||||||
|
|
||||||
**Request/Response** (`invoke()`): Used for discrete operations — starting containers, saving settings, listing projects. The frontend calls `invoke("command_name", { args })` and awaits a typed result.
|
**Request/Response** (`invoke()`): Used for discrete operations — starting containers, saving settings, listing projects. The frontend calls `invoke("command_name", { args })` and awaits a typed result.
|
||||||
|
|
||||||
|
**WebSocket Streaming** (Web Terminal): Used for remote terminal access from browsers on the local network. An axum HTTP+WebSocket server runs inside the Tauri process, sharing the same `ExecSessionManager` via `Arc`-wrapped stores. The WebSocket uses a JSON protocol with base64-encoded terminal data. Each browser connection can open multiple terminal sessions; all sessions are cleaned up when the WebSocket disconnects.
|
||||||
|
|
||||||
**Event Streaming** (`emit()`/`listen()`): Used for continuous data — terminal I/O. When a terminal session is opened, the Rust backend spawns two tokio tasks:
|
**Event Streaming** (`emit()`/`listen()`): Used for continuous data — terminal I/O. When a terminal session is opened, the Rust backend spawns two tokio tasks:
|
||||||
1. **Output reader** — Reads from the Docker exec stdout stream and emits `terminal-output-{sessionId}` events to the frontend.
|
1. **Output reader** — Reads from the Docker exec stdout stream and emits `terminal-output-{sessionId}` events to the frontend.
|
||||||
2. **Input writer** — Listens on an `mpsc::unbounded_channel` for data sent from the frontend via `invoke("terminal_input")` and writes it to the Docker exec stdin.
|
2. **Input writer** — Listens on an `mpsc::unbounded_channel` for data sent from the frontend via `invoke("terminal_input")` and writes it to the Docker exec stdin.
|
||||||
@@ -263,7 +269,7 @@ triple-c/
|
|||||||
│ ├── projects/ # ProjectCard, ProjectList, AddProjectDialog,
|
│ ├── projects/ # ProjectCard, ProjectList, AddProjectDialog,
|
||||||
│ │ # FileManagerModal, ContainerProgressModal, modals
|
│ │ # FileManagerModal, ContainerProgressModal, modals
|
||||||
│ ├── settings/ # SettingsPanel, DockerSettings, AwsSettings,
|
│ ├── settings/ # SettingsPanel, DockerSettings, AwsSettings,
|
||||||
│ │ # UpdateDialog
|
│ │ # WebTerminalSettings, UpdateDialog
|
||||||
│ └── terminal/ # TerminalView (xterm.js), TerminalTabs, UrlToast
|
│ └── terminal/ # TerminalView (xterm.js), TerminalTabs, UrlToast
|
||||||
│
|
│
|
||||||
└── src-tauri/ # Rust backend
|
└── src-tauri/ # Rust backend
|
||||||
@@ -282,7 +288,13 @@ triple-c/
|
|||||||
│ ├── project_commands.rs # Start/stop/rebuild containers
|
│ ├── project_commands.rs # Start/stop/rebuild containers
|
||||||
│ ├── settings_commands.rs # Settings CRUD
|
│ ├── settings_commands.rs # Settings CRUD
|
||||||
│ ├── terminal_commands.rs # Terminal I/O, resize
|
│ ├── terminal_commands.rs # Terminal I/O, resize
|
||||||
│ └── update_commands.rs # App update checking
|
│ ├── update_commands.rs # App update checking
|
||||||
|
│ └── web_terminal_commands.rs # Web terminal start/stop/status
|
||||||
|
├── web_terminal/ # Remote terminal access
|
||||||
|
│ ├── mod.rs # Module root
|
||||||
|
│ ├── server.rs # Axum HTTP+WS server lifecycle
|
||||||
|
│ ├── ws_handler.rs # WebSocket connection handler
|
||||||
|
│ └── terminal.html # Embedded xterm.js web UI
|
||||||
├── docker/ # Docker API layer
|
├── docker/ # Docker API layer
|
||||||
│ ├── client.rs # bollard singleton connection
|
│ ├── client.rs # bollard singleton connection
|
||||||
│ ├── container.rs # Create, start, stop, remove, fingerprinting
|
│ ├── container.rs # Create, start, stop, remove, fingerprinting
|
||||||
@@ -323,6 +335,11 @@ triple-c/
|
|||||||
| `tar` | 0.4 | In-memory tar archives for Docker build context |
|
| `tar` | 0.4 | In-memory tar archives for Docker build context |
|
||||||
| `dirs` | 6.x | Cross-platform app data directory paths |
|
| `dirs` | 6.x | Cross-platform app data directory paths |
|
||||||
| `serde` / `serde_json` | 1.x | Serialization for IPC and persistence |
|
| `serde` / `serde_json` | 1.x | Serialization for IPC and persistence |
|
||||||
|
| `axum` | 0.8 | HTTP+WebSocket server for web terminal |
|
||||||
|
| `tower-http` | 0.6 | CORS middleware for web terminal |
|
||||||
|
| `base64` | 0.22 | Terminal data encoding over WebSocket |
|
||||||
|
| `rand` | 0.9 | Access token generation |
|
||||||
|
| `local-ip-address` | 0.6 | LAN IP detection for web terminal URL |
|
||||||
|
|
||||||
### JavaScript (Frontend)
|
### JavaScript (Frontend)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
height: 100dvh; /* dynamic viewport height — shrinks when mobile keyboard opens */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -46,6 +47,9 @@
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-title {
|
.topbar-title {
|
||||||
@@ -101,6 +105,9 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
|
position: sticky;
|
||||||
|
top: 42px; /* below .topbar min-height */
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@@ -156,6 +163,69 @@
|
|||||||
}
|
}
|
||||||
.terminal-container.active { display: block; }
|
.terminal-container.active { display: block; }
|
||||||
|
|
||||||
|
/* ── Input Bar (mobile/tablet) ──────────── */
|
||||||
|
.input-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-bar input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 16px; /* prevents iOS zoom on focus */
|
||||||
|
font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', monospace;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
.input-bar input:focus { border-color: var(--accent); }
|
||||||
|
|
||||||
|
.input-bar .key-btn {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Scroll-to-bottom FAB ──────────────── */
|
||||||
|
.scroll-bottom-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12px;
|
||||||
|
right: 16px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||||
|
z-index: 10;
|
||||||
|
padding: 0;
|
||||||
|
min-width: unset;
|
||||||
|
min-height: unset;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.scroll-bottom-btn:hover { background: var(--accent-hover); }
|
||||||
|
.scroll-bottom-btn.visible { display: flex; }
|
||||||
|
|
||||||
/* ── Empty State ─────────────────────────── */
|
/* ── Empty State ─────────────────────────── */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -201,6 +271,17 @@
|
|||||||
<div>Select a project and open a terminal session</div>
|
<div>Select a project and open a terminal session</div>
|
||||||
<div class="hint">Use the buttons above to start a Claude or Bash session</div>
|
<div class="hint">Use the buttons above to start a Claude or Bash session</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="scroll-bottom-btn" id="scrollBottomBtn" title="Scroll to bottom">↓</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Bar for mobile/tablet -->
|
||||||
|
<div class="input-bar" id="inputBar">
|
||||||
|
<input type="text" id="mobileInput" placeholder="Type here..."
|
||||||
|
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||||
|
enterkeyhint="send" inputmode="text">
|
||||||
|
<button class="key-btn" id="btnEnter">Enter</button>
|
||||||
|
<button class="key-btn" id="btnTab">Tab</button>
|
||||||
|
<button class="key-btn" id="btnCtrlC">^C</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -223,6 +304,11 @@
|
|||||||
const tabbar = document.getElementById('tabbar');
|
const tabbar = document.getElementById('tabbar');
|
||||||
const terminalArea = document.getElementById('terminalArea');
|
const terminalArea = document.getElementById('terminalArea');
|
||||||
const emptyState = document.getElementById('emptyState');
|
const emptyState = document.getElementById('emptyState');
|
||||||
|
const mobileInput = document.getElementById('mobileInput');
|
||||||
|
const btnEnter = document.getElementById('btnEnter');
|
||||||
|
const btnTab = document.getElementById('btnTab');
|
||||||
|
const btnCtrlC = document.getElementById('btnCtrlC');
|
||||||
|
const scrollBottomBtn = document.getElementById('scrollBottomBtn');
|
||||||
|
|
||||||
// ── WebSocket ──────────────────────────────
|
// ── WebSocket ──────────────────────────────
|
||||||
function connect() {
|
function connect() {
|
||||||
@@ -390,6 +476,9 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track scroll position for scroll-to-bottom button
|
||||||
|
term.onScroll(() => updateScrollButton());
|
||||||
|
|
||||||
// Store session
|
// Store session
|
||||||
sessions[sessionId] = { term, fitAddon, projectName, type: sessionType, container };
|
sessions[sessionId] = { term, fitAddon, projectName, type: sessionType, container };
|
||||||
|
|
||||||
@@ -405,6 +494,8 @@
|
|||||||
if (!session) return;
|
if (!session) return;
|
||||||
const bytes = Uint8Array.from(atob(b64data), c => c.charCodeAt(0));
|
const bytes = Uint8Array.from(atob(b64data), c => c.charCodeAt(0));
|
||||||
session.term.write(bytes);
|
session.term.write(bytes);
|
||||||
|
// Update scroll button if this is the active session
|
||||||
|
if (sessionId === activeSessionId) updateScrollButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSessionExit(sessionId) {
|
function onSessionExit(sessionId) {
|
||||||
@@ -479,6 +570,7 @@
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
session.fitAddon.fit();
|
session.fitAddon.fit();
|
||||||
session.term.focus();
|
session.term.focus();
|
||||||
|
updateScrollButton();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,6 +596,67 @@
|
|||||||
resizeTimeout = setTimeout(handleResize, 100);
|
resizeTimeout = setTimeout(handleResize, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Send helper ─────────────────────────────
|
||||||
|
function sendTerminalInput(str) {
|
||||||
|
if (!activeSessionId) return;
|
||||||
|
const bytes = new TextEncoder().encode(str);
|
||||||
|
const b64 = btoa(String.fromCharCode(...bytes));
|
||||||
|
send({
|
||||||
|
type: 'input',
|
||||||
|
session_id: activeSessionId,
|
||||||
|
data: b64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Input bar (mobile/tablet) ──────────────
|
||||||
|
// Send characters immediately, bypassing IME composition buffering.
|
||||||
|
// Clearing value on each input event cancels any active composition.
|
||||||
|
mobileInput.addEventListener('input', () => {
|
||||||
|
const val = mobileInput.value;
|
||||||
|
if (val) {
|
||||||
|
sendTerminalInput(val);
|
||||||
|
mobileInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Catch Enter in the input field itself
|
||||||
|
mobileInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const val = mobileInput.value;
|
||||||
|
if (val) {
|
||||||
|
sendTerminalInput(val);
|
||||||
|
mobileInput.value = '';
|
||||||
|
}
|
||||||
|
sendTerminalInput('\r');
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
sendTerminalInput('\t');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnEnter.onclick = () => { sendTerminalInput('\r'); mobileInput.focus(); };
|
||||||
|
btnTab.onclick = () => { sendTerminalInput('\t'); mobileInput.focus(); };
|
||||||
|
btnCtrlC.onclick = () => { sendTerminalInput('\x03'); mobileInput.focus(); };
|
||||||
|
|
||||||
|
// ── Scroll to bottom ──────────────────────
|
||||||
|
function updateScrollButton() {
|
||||||
|
if (!activeSessionId || !sessions[activeSessionId]) {
|
||||||
|
scrollBottomBtn.classList.remove('visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const term = sessions[activeSessionId].term;
|
||||||
|
const isAtBottom = term.buffer.active.viewportY >= term.buffer.active.baseY;
|
||||||
|
scrollBottomBtn.classList.toggle('visible', !isAtBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollBottomBtn.onclick = () => {
|
||||||
|
if (activeSessionId && sessions[activeSessionId]) {
|
||||||
|
sessions[activeSessionId].term.scrollToBottom();
|
||||||
|
scrollBottomBtn.classList.remove('visible');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── Event listeners ────────────────────────
|
// ── Event listeners ────────────────────────
|
||||||
btnClaude.onclick = () => openSession('claude');
|
btnClaude.onclick = () => openSession('claude');
|
||||||
btnBash.onclick = () => openSession('bash');
|
btnBash.onclick = () => openSession('bash');
|
||||||
|
|||||||
Reference in New Issue
Block a user