Compare commits

..

2 Commits

Author SHA1 Message Date
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 240 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

@@ -156,6 +156,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 +264,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 +297,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 +469,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 +487,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 +563,7 @@
requestAnimationFrame(() => { requestAnimationFrame(() => {
session.fitAddon.fit(); session.fitAddon.fit();
session.term.focus(); session.term.focus();
updateScrollButton();
}); });
} }
} }
@@ -504,6 +589,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');