Compare commits

...

3 Commits

Author SHA1 Message Date
57a7cee544 Make topbar and tab bar sticky in web terminal
All checks were successful
Build App / compute-version (push) Successful in 3s
Build App / build-macos (push) Successful in 2m27s
Build App / build-windows (push) Successful in 4m13s
Build App / build-linux (push) Successful in 4m51s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 11s
Adds position:sticky to the topbar and tab bar so they stay pinned
at the top when the virtual keyboard opens on tablets. Also uses
100dvh (dynamic viewport height) so the layout properly shrinks
when the keyboard appears on mobile browsers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:15:31 -07:00
6369f7e0a8 Document web terminal feature across all docs
Adds web terminal documentation to README (architecture, key files),
HOW-TO-USE (setup guide, usage, security tips), TECHNICAL (system
diagram, communication flow, dependencies, project structure), and
CLAUDE.md (backend structure).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 20:09:05 -07:00
9ee0d34c19 Fix tablet keyboard input lag and add scroll-to-bottom button
All checks were successful
Build App / compute-version (push) Successful in 3s
Build App / build-macos (push) Successful in 2m29s
Build App / build-windows (push) Successful in 3m55s
Build App / build-linux (push) Successful in 4m34s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 9s
Adds a dedicated input bar at the bottom that bypasses mobile IME
composition buffering — characters are sent immediately on each input
event instead of waiting for word boundaries. Also adds helper buttons
(Enter, Tab, Ctrl+C) and a floating scroll-to-bottom button that
appears when scrolled up from the terminal output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 20:03:13 -07:00
5 changed files with 247 additions and 7 deletions

View File

@@ -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/`)

View File

@@ -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

View File

@@ -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 |

View File

@@ -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)

View File

@@ -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">&#8595;</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');