Add web terminal for remote tablet/phone access to project terminals
All checks were successful
Build App / compute-version (push) Successful in 3s
Build App / build-macos (push) Successful in 2m36s
Build App / build-windows (push) Successful in 4m39s
Build App / build-linux (push) Successful in 5m56s
Build App / create-tag (push) Successful in 2s
Build App / sync-to-github (push) Successful in 10s
All checks were successful
Build App / compute-version (push) Successful in 3s
Build App / build-macos (push) Successful in 2m36s
Build App / build-windows (push) Successful in 4m39s
Build App / build-linux (push) Successful in 5m56s
Build App / create-tag (push) Successful in 2s
Build App / sync-to-github (push) Successful in 10s
Adds an axum HTTP+WebSocket server that runs alongside the Tauri app, serving a standalone xterm.js-based terminal UI accessible from any browser on the local network. Shares the existing ExecSessionManager via Arc-wrapped stores, with token-based authentication and automatic session cleanup on disconnect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import EnvVarsModal from "../projects/EnvVarsModal";
|
||||
import { detectHostTimezone } from "../../lib/tauri-commands";
|
||||
import type { EnvVar } from "../../lib/types";
|
||||
import Tooltip from "../ui/Tooltip";
|
||||
import WebTerminalSettings from "./WebTerminalSettings";
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const { appSettings, saveSettings } = useSettings();
|
||||
@@ -116,6 +117,9 @@ export default function SettingsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Web Terminal */}
|
||||
<WebTerminalSettings />
|
||||
|
||||
{/* Updates section */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Updates<Tooltip text="Check for new versions of the Triple-C app and container image." /></label>
|
||||
|
||||
128
app/src/components/settings/WebTerminalSettings.tsx
Normal file
128
app/src/components/settings/WebTerminalSettings.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { startWebTerminal, stopWebTerminal, getWebTerminalStatus, regenerateWebTerminalToken } from "../../lib/tauri-commands";
|
||||
import type { WebTerminalInfo } from "../../lib/types";
|
||||
import Tooltip from "../ui/Tooltip";
|
||||
|
||||
export default function WebTerminalSettings() {
|
||||
const [info, setInfo] = useState<WebTerminalInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getWebTerminalStatus().then(setInfo).catch(console.error);
|
||||
}, []);
|
||||
|
||||
const handleToggle = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (info?.running) {
|
||||
await stopWebTerminal();
|
||||
const updated = await getWebTerminalStatus();
|
||||
setInfo(updated);
|
||||
} else {
|
||||
const updated = await startWebTerminal();
|
||||
setInfo(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Web terminal toggle failed:", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
try {
|
||||
const updated = await regenerateWebTerminalToken();
|
||||
setInfo(updated);
|
||||
} catch (e) {
|
||||
console.error("Token regeneration failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyUrl = async () => {
|
||||
if (info?.url) {
|
||||
await navigator.clipboard.writeText(info.url);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToken = async () => {
|
||||
if (info?.access_token) {
|
||||
await navigator.clipboard.writeText(info.access_token);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Web Terminal
|
||||
<Tooltip text="Access your terminals from a tablet or phone on the local network via a web browser." />
|
||||
</label>
|
||||
<p className="text-xs text-[var(--text-secondary)] mb-2">
|
||||
Serves a browser-based terminal UI on your local network for remote access to running projects.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
disabled={loading}
|
||||
className={`px-2 py-0.5 text-xs rounded transition-colors ${
|
||||
info?.running
|
||||
? "bg-[var(--success)] text-white"
|
||||
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
|
||||
}`}
|
||||
>
|
||||
{loading ? "..." : info?.running ? "ON" : "OFF"}
|
||||
</button>
|
||||
<span className="text-xs text-[var(--text-secondary)]">
|
||||
{info?.running
|
||||
? `Running on port ${info.port}`
|
||||
: "Stopped"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* URL + Copy */}
|
||||
{info?.running && info.url && (
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-xs text-[var(--accent)] bg-[var(--bg-primary)] px-2 py-1 rounded border border-[var(--border-color)] truncate flex-1">
|
||||
{info.url}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyUrl}
|
||||
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||
>
|
||||
{copied ? "Copied!" : "Copy URL"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Token */}
|
||||
{info && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--text-secondary)]">Token:</span>
|
||||
<code className="text-xs text-[var(--text-primary)] bg-[var(--bg-primary)] px-2 py-0.5 rounded border border-[var(--border-color)] truncate max-w-[160px]">
|
||||
{info.access_token ? `${info.access_token.slice(0, 12)}...` : "None"}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyToken}
|
||||
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRegenerate}
|
||||
className="text-xs px-2 py-0.5 text-[var(--warning,#f59e0b)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Project, ProjectPath, ContainerInfo, SiblingContainer, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer, FileEntry } from "./types";
|
||||
import type { Project, ProjectPath, ContainerInfo, SiblingContainer, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer, FileEntry, WebTerminalInfo } from "./types";
|
||||
|
||||
// Docker
|
||||
export const checkDocker = () => invoke<boolean>("check_docker");
|
||||
@@ -88,3 +88,13 @@ export const checkImageUpdate = () =>
|
||||
|
||||
// Help
|
||||
export const getHelpContent = () => invoke<string>("get_help_content");
|
||||
|
||||
// Web Terminal
|
||||
export const startWebTerminal = () =>
|
||||
invoke<WebTerminalInfo>("start_web_terminal");
|
||||
export const stopWebTerminal = () =>
|
||||
invoke<void>("stop_web_terminal");
|
||||
export const getWebTerminalStatus = () =>
|
||||
invoke<WebTerminalInfo>("get_web_terminal_status");
|
||||
export const regenerateWebTerminalToken = () =>
|
||||
invoke<WebTerminalInfo>("regenerate_web_terminal_token");
|
||||
|
||||
@@ -118,6 +118,21 @@ export interface AppSettings {
|
||||
timezone: string | null;
|
||||
default_microphone: string | null;
|
||||
dismissed_image_digest: string | null;
|
||||
web_terminal: WebTerminalSettings;
|
||||
}
|
||||
|
||||
export interface WebTerminalSettings {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
access_token: string | null;
|
||||
}
|
||||
|
||||
export interface WebTerminalInfo {
|
||||
running: boolean;
|
||||
port: number;
|
||||
access_token: string;
|
||||
local_ip: string | null;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateInfo {
|
||||
|
||||
Reference in New Issue
Block a user