(null);
@@ -119,7 +120,7 @@ export function ToolStarter({
workspace.project_id,
workspace.repo_id,
selectedToolTypeId,
- workspace.name,
+ displayName.trim() || undefined,
undefined,
undefined,
undefined,
@@ -140,7 +141,7 @@ export function ToolStarter({
} finally {
setStarting(false);
}
- }, [selectedToolTypeId, selectedProfileId, workspace, onStarted]);
+ }, [selectedToolTypeId, selectedProfileId, displayName, workspace, onStarted]);
return (
@@ -187,6 +188,19 @@ export function ToolStarter({
{toolTypesError && {toolTypesError}}
+ {/* Session Name */}
+
+
+ setDisplayName(e.target.value)}
+ placeholder="My dev environment"
+ disabled={starting}
+ />
+
+
{/* Config Profile */}
{selectedToolTypeId && (
diff --git a/apps/web/src/hooks/use-terminal-page.ts b/apps/web/src/hooks/use-terminal-page.ts
index 10fe534..9974e8c 100644
--- a/apps/web/src/hooks/use-terminal-page.ts
+++ b/apps/web/src/hooks/use-terminal-page.ts
@@ -10,294 +10,301 @@ import type { TerminalSession } from "../api/terminal";
import type { ModifierKey } from "./use-special-keys";
const SESSIONS_TO_INFO = (sessions: TerminalSession[]): TerminalSessionInfo[] =>
- sessions.map((s) => ({
- id: s.id,
- name: s.name,
- status: s.status as TerminalSessionInfo["status"],
- }));
+ sessions.map((s) => ({
+ id: s.id,
+ name: s.name,
+ status: s.status as TerminalSessionInfo["status"],
+ }));
type TerminalStatus =
- | "connecting"
- | "connected"
- | "disconnected"
- | "error"
- | "resetting";
+ | "connecting"
+ | "connected"
+ | "disconnected"
+ | "error"
+ | "resetting";
export const useTerminalPage = () => {
- const { instanceId } = useParams<{ instanceId: string }>();
- const navigate = useNavigate();
- const isMobile = useMobileViewport();
- const [isFullscreen, setIsFullscreen] = useState(false);
- const terminalRefs = useRef>>({});
- const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
+ const { instanceId } = useParams<{ instanceId: string }>();
+ const navigate = useNavigate();
+ const isMobile = useMobileViewport();
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const terminalRefs = useRef>>({});
+ const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
- const [terminalStatuses, setTerminalStatuses] = useState<
- Record
- >({});
- const changeFontSizeRef = useRef<((delta: number) => void) | null>(null);
- const sendDataRef = useRef<((data: string) => void) | null>(null);
- const focusInputRef = useRef<(() => void) | null>(null);
- const [showResetConfirm, setShowResetConfirm] = useState(false);
- const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false);
- const [activeModifier, setActiveModifier] = useState(null);
- const { isOpen: isKeyboardOpen, height: keyboardHeight } = useVirtualKeyboard();
+ const [terminalStatuses, setTerminalStatuses] = useState<
+ Record
+ >({});
+ const changeFontSizeRef = useRef<((delta: number) => void) | null>(null);
+ const sendDataRef = useRef<((data: string) => void) | null>(null);
+ const focusInputRef = useRef<(() => void) | null>(null);
+ const [showResetConfirm, setShowResetConfirm] = useState(false);
+ const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false);
+ const [activeModifier, setActiveModifier] = useState(
+ null,
+ );
+ const { isOpen: isKeyboardOpen, height: keyboardHeight } =
+ useVirtualKeyboard();
- const {
- sessions,
- activeSessionId,
- setActiveSessionId,
- createSession,
- closeSession,
- renameSession,
- resetSession,
- loading,
- error,
- } = useTerminalSessions(instanceId ?? "");
+ const {
+ sessions,
+ activeSessionId,
+ setActiveSessionId,
+ createSession,
+ closeSession,
+ renameSession,
+ resetSession,
+ loading,
+ error,
+ } = useTerminalSessions(instanceId ?? "");
- // Auto-create default session
- useEffect(() => {
- if (!loading && sessions.length === 0 && !error && instanceId) {
- void createSession("Session 1");
- }
- }, [loading, sessions.length, error, instanceId, createSession]);
+ // Auto-create default session
+ useEffect(() => {
+ if (!loading && sessions.length === 0 && !error && instanceId) {
+ void createSession("Session 1");
+ }
+ }, [loading, sessions.length, error, instanceId, createSession]);
- // Update document title based on active terminal session
- useEffect(() => {
- if (!instanceId) {
- document.title = "Terminal — Headquarter";
- return;
- }
- const active = sessions.find((s) => s.id === activeSessionId);
- const name = active?.name ?? `Instance ${instanceId.slice(0, 8)}`;
- document.title = `${name} — Terminal — Headquarter`;
- return () => {
- document.title = "Headquarter";
- };
- }, [instanceId, activeSessionId, sessions]);
+ // Update document title based on active terminal session
+ useEffect(() => {
+ if (!instanceId) {
+ document.title = "Terminal — Headquarter";
+ return;
+ }
+ const active = sessions.find((s) => s.id === activeSessionId);
+ const name = active?.name ?? `Instance ${instanceId.slice(0, 8)}`;
+ document.title = `${name} — Terminal — Headquarter`;
+ return () => {
+ document.title = "Headquarter";
+ };
+ }, [instanceId, activeSessionId, sessions]);
- // Sync refs with sessions
- useEffect(() => {
- for (const session of sessions) {
- if (!terminalRefs.current[session.id]) {
- terminalRefs.current[session.id] = React.createRef();
- }
- }
- const currentIds = new Set(sessions.map((s) => s.id));
- for (const id of Object.keys(terminalRefs.current)) {
- if (!currentIds.has(id)) {
- delete terminalRefs.current[id];
- }
- }
- }, [sessions]);
+ // Sync refs with sessions
+ useEffect(() => {
+ for (const session of sessions) {
+ if (!terminalRefs.current[session.id]) {
+ terminalRefs.current[session.id] = React.createRef();
+ }
+ }
+ const currentIds = new Set(sessions.map((s) => s.id));
+ for (const id of Object.keys(terminalRefs.current)) {
+ if (!currentIds.has(id)) {
+ delete terminalRefs.current[id];
+ }
+ }
+ }, [sessions]);
- // Fit and focus active terminal
- useEffect(() => {
- if (activeSessionId && terminalRefs.current[activeSessionId]) {
- const ref = terminalRefs.current[activeSessionId];
- let raf1 = 0;
- let raf2 = 0;
- raf1 = requestAnimationFrame(() => {
- raf2 = requestAnimationFrame(() => {
- ref.current?.fit();
- ref.current?.focus();
- });
- });
- return () => {
- cancelAnimationFrame(raf1);
- cancelAnimationFrame(raf2);
- };
- }
- }, [activeSessionId]);
+ // Fit and focus active terminal
+ useEffect(() => {
+ if (activeSessionId && terminalRefs.current[activeSessionId]) {
+ const ref = terminalRefs.current[activeSessionId];
+ let raf1 = 0;
+ let raf2 = 0;
+ raf1 = requestAnimationFrame(() => {
+ raf2 = requestAnimationFrame(() => {
+ ref.current?.fit();
+ ref.current?.focus();
+ });
+ });
+ return () => {
+ cancelAnimationFrame(raf1);
+ cancelAnimationFrame(raf2);
+ };
+ }
+ }, [activeSessionId]);
- // Keyboard shortcuts
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
- if (!isAltShift) return;
+ // Keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
+ if (!isAltShift) return;
- switch (e.key.toLowerCase()) {
- case "n":
- e.preventDefault();
- if (sessions.length < 5) {
- void createSession(`Session ${sessions.length + 1}`);
- }
- break;
- case "w":
- e.preventDefault();
- if (activeSessionId && window.confirm("Close this terminal session?")) {
- void closeSession(activeSessionId);
- }
- break;
- case "arrowleft":
- e.preventDefault();
- if (activeSessionId) {
- const idx = sessions.findIndex((s) => s.id === activeSessionId);
- if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
- }
- break;
- case "arrowright":
- e.preventDefault();
- if (activeSessionId) {
- const idx = sessions.findIndex((s) => s.id === activeSessionId);
- if (idx < sessions.length - 1) setActiveSessionId(sessions[idx + 1].id);
- }
- break;
- case "r":
- e.preventDefault();
- if (activeSessionId) void resetSession(activeSessionId);
- break;
- case "f":
- e.preventDefault();
- setIsFullscreen((prev) => !prev);
- break;
- }
- };
+ switch (e.key.toLowerCase()) {
+ case "n":
+ e.preventDefault();
+ if (sessions.length < 5) {
+ void createSession(`Session ${sessions.length + 1}`);
+ }
+ break;
+ case "w":
+ e.preventDefault();
+ if (
+ activeSessionId &&
+ window.confirm("Close this terminal session?")
+ ) {
+ void closeSession(activeSessionId);
+ }
+ break;
+ case "arrowleft":
+ e.preventDefault();
+ if (activeSessionId) {
+ const idx = sessions.findIndex((s) => s.id === activeSessionId);
+ if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
+ }
+ break;
+ case "arrowright":
+ e.preventDefault();
+ if (activeSessionId) {
+ const idx = sessions.findIndex((s) => s.id === activeSessionId);
+ if (idx < sessions.length - 1)
+ setActiveSessionId(sessions[idx + 1].id);
+ }
+ break;
+ case "r":
+ e.preventDefault();
+ if (activeSessionId) void resetSession(activeSessionId);
+ break;
+ case "f":
+ e.preventDefault();
+ setIsFullscreen((prev) => !prev);
+ break;
+ }
+ };
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [
- sessions,
- activeSessionId,
- createSession,
- closeSession,
- resetSession,
- setActiveSessionId,
- ]);
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [
+ sessions,
+ activeSessionId,
+ createSession,
+ closeSession,
+ resetSession,
+ setActiveSessionId,
+ ]);
- // Keep screen awake
- useEffect(() => {
- let wakeLock: WakeLockSentinel | null = null;
- const requestWakeLock = async () => {
- try {
- if ("wakeLock" in navigator) {
- wakeLock = await navigator.wakeLock.request("screen");
- }
- } catch {
- // ignore
- }
- };
- void requestWakeLock();
- const handleVisibilityChange = () => {
- if (document.visibilityState === "visible") void requestWakeLock();
- };
- document.addEventListener("visibilitychange", handleVisibilityChange);
- return () => {
- document.removeEventListener("visibilitychange", handleVisibilityChange);
- wakeLock?.release().catch(() => {});
- };
- }, []);
+ // Keep screen awake
+ useEffect(() => {
+ let wakeLock: WakeLockSentinel | null = null;
+ const requestWakeLock = async () => {
+ try {
+ if ("wakeLock" in navigator) {
+ wakeLock = await navigator.wakeLock.request("screen");
+ }
+ } catch {
+ // ignore
+ }
+ };
+ void requestWakeLock();
+ const handleVisibilityChange = () => {
+ if (document.visibilityState === "visible") void requestWakeLock();
+ };
+ document.addEventListener("visibilitychange", handleVisibilityChange);
+ return () => {
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
+ wakeLock?.release().catch(() => {});
+ };
+ }, []);
- // Lock page scroll on mobile
- useEffect(() => {
- if (!isMobile) return;
- document.documentElement.classList.add("terminal-page-open");
- document.body.classList.add("terminal-page-open");
- return () => {
- document.documentElement.classList.remove("terminal-page-open");
- document.body.classList.remove("terminal-page-open");
- };
- }, [isMobile]);
+ // Lock page scroll on mobile
+ useEffect(() => {
+ if (!isMobile) return;
+ document.documentElement.classList.add("terminal-page-open");
+ document.body.classList.add("terminal-page-open");
+ return () => {
+ document.documentElement.classList.remove("terminal-page-open");
+ document.body.classList.remove("terminal-page-open");
+ };
+ }, [isMobile]);
- const handleFullscreenClick = useCallback(
- (e: React.MouseEvent) => {
- if (!isFullscreen) return;
- const target = e.target as Node;
- const current = e.currentTarget as HTMLElement;
- const content = current.querySelector(".terminal-page-content");
- const header = current.querySelector(".terminal-fullscreen-header");
- if (content?.contains(target) || header?.contains(target)) return;
- setIsFullscreen(false);
- },
- [isFullscreen],
- );
+ const handleFullscreenClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (!isFullscreen) return;
+ const target = e.target as Node;
+ const current = e.currentTarget as HTMLElement;
+ const content = current.querySelector(".terminal-page-content");
+ const header = current.querySelector(".terminal-fullscreen-header");
+ if (content?.contains(target) || header?.contains(target)) return;
+ setIsFullscreen(false);
+ },
+ [isFullscreen],
+ );
- const handleSelect = useCallback(
- (sessionId: string) => setActiveSessionId(sessionId),
- [setActiveSessionId],
- );
+ const handleSelect = useCallback(
+ (sessionId: string) => setActiveSessionId(sessionId),
+ [setActiveSessionId],
+ );
- const handleClose = useCallback(
- async (sessionId: string) => closeSession(sessionId),
- [closeSession],
- );
+ const handleClose = useCallback(
+ async (sessionId: string) => closeSession(sessionId),
+ [closeSession],
+ );
- const handleCreate = useCallback(() => {
- void createSession(`Session ${sessions.length + 1}`);
- }, [createSession, sessions.length]);
+ const handleCreate = useCallback(() => {
+ void createSession(`Session ${sessions.length + 1}`);
+ }, [createSession, sessions.length]);
- const handleRename = useCallback(
- (sessionId: string, newName: string) => {
- void renameSession(sessionId, newName);
- },
- [renameSession],
- );
+ const handleRename = useCallback(
+ (sessionId: string, newName: string) => {
+ void renameSession(sessionId, newName);
+ },
+ [renameSession],
+ );
- const handleTerminalReady = useCallback(
- (
- sendData: (data: string) => void,
- status: TerminalStatus,
- focusInput: () => void,
- changeFontSize: (delta: number) => void,
- ) => {
- setTerminalStatuses((prev) => ({
- ...prev,
- [activeSessionId ?? "default"]: status,
- }));
- sendDataRef.current = sendData;
- focusInputRef.current = focusInput;
- changeFontSizeRef.current = changeFontSize;
- },
- [activeSessionId],
- );
+ const handleTerminalReady = useCallback(
+ (
+ sendData: (data: string) => void,
+ status: TerminalStatus,
+ focusInput: () => void,
+ changeFontSize: (delta: number) => void,
+ ) => {
+ setTerminalStatuses((prev) => ({
+ ...prev,
+ [activeSessionId ?? "default"]: status,
+ }));
+ sendDataRef.current = sendData;
+ focusInputRef.current = focusInput;
+ changeFontSizeRef.current = changeFontSize;
+ },
+ [activeSessionId],
+ );
- const handleFontSizeChange = useCallback((delta: number) => {
- changeFontSizeRef.current?.(delta);
- }, []);
+ const handleFontSizeChange = useCallback((delta: number) => {
+ changeFontSizeRef.current?.(delta);
+ }, []);
- const handleSendKey = useCallback((data: string) => {
- sendDataRef.current?.(data);
- }, []);
+ const handleSendKey = useCallback((data: string) => {
+ sendDataRef.current?.(data);
+ }, []);
- const handleReset = useCallback(() => {
- if (activeSessionId && terminalRefs.current[activeSessionId]) {
- terminalRefs.current[activeSessionId].current?.reset();
- }
- }, [activeSessionId]);
+ const handleReset = useCallback(() => {
+ if (activeSessionId && terminalRefs.current[activeSessionId]) {
+ terminalRefs.current[activeSessionId].current?.reset();
+ }
+ }, [activeSessionId]);
- return {
- instanceId,
- navigate,
- isMobile,
- isFullscreen,
- setIsFullscreen,
- terminalRefs,
- headerAutoHide,
- terminalStatuses,
- sendDataRef,
- focusInputRef,
- changeFontSizeRef,
- showResetConfirm,
- setShowResetConfirm,
- showSpecialKeysPanel,
- setShowSpecialKeysPanel,
- activeModifier,
- setActiveModifier,
- isKeyboardOpen,
- keyboardHeight,
- sessions,
- activeSessionId,
- setActiveSessionId,
- loading,
- error,
- handleFullscreenClick,
- handleSelect,
- handleClose,
- handleCreate,
- handleRename,
- handleTerminalReady,
- handleFontSizeChange,
- handleSendKey,
- handleReset,
- sessionInfos: SESSIONS_TO_INFO(sessions),
- };
+ return {
+ instanceId,
+ navigate,
+ isMobile,
+ isFullscreen,
+ setIsFullscreen,
+ terminalRefs,
+ headerAutoHide,
+ terminalStatuses,
+ sendDataRef,
+ focusInputRef,
+ changeFontSizeRef,
+ showResetConfirm,
+ setShowResetConfirm,
+ showSpecialKeysPanel,
+ setShowSpecialKeysPanel,
+ activeModifier,
+ setActiveModifier,
+ isKeyboardOpen,
+ keyboardHeight,
+ sessions,
+ activeSessionId,
+ setActiveSessionId,
+ loading,
+ error,
+ handleFullscreenClick,
+ handleSelect,
+ handleClose,
+ handleCreate,
+ handleRename,
+ handleTerminalReady,
+ handleFontSizeChange,
+ handleSendKey,
+ handleReset,
+ sessionInfos: SESSIONS_TO_INFO(sessions),
+ };
};
diff --git a/apps/web/src/styles/pages/sessions.css b/apps/web/src/styles/pages/sessions.css
index 11881dc..1d360ee 100644
--- a/apps/web/src/styles/pages/sessions.css
+++ b/apps/web/src/styles/pages/sessions.css
@@ -89,4 +89,3 @@
min-height: 44px;
}
}
-