62d1bdc462
- Add TerminalSessionTabs component with status dots, rename, close, max-5 limit - Add 7 component tests for tab rendering, selection, close, rename - TerminalComponent: sessionId prop, forwardRef with fit() method - TerminalPage: multi-session orchestration, tab switching, auto-create default - Fullscreen mode: Alt+Shift+F toggle, auto-hide tabs, Esc exit - Keyboard shortcuts: Alt+Shift+N/W/ArrowLeft/ArrowRight/R - Add CSS for tabs, fullscreen, mobile responsive - Update useTerminalSessions hook for session CRUD - terminal_manager.py: lookup by internal session_id fallback Quality gates: tsc --noEmit clean, vitest (7/7 new tests passed), pytest (182 passed)
159 lines
3.9 KiB
TypeScript
159 lines
3.9 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import {
|
|
listTerminalSessions,
|
|
createTerminalSession,
|
|
closeTerminalSession,
|
|
resetTerminalSession,
|
|
renameTerminalSession,
|
|
type TerminalSession,
|
|
} from "../api/terminal";
|
|
|
|
export interface UseTerminalSessionsResult {
|
|
sessions: TerminalSession[];
|
|
activeSessionId: string | null;
|
|
setActiveSessionId: (id: string) => void;
|
|
createSession: (name?: string) => Promise<TerminalSession | null>;
|
|
closeSession: (sessionId: string) => Promise<void>;
|
|
renameSession: (sessionId: string, name: string) => Promise<void>;
|
|
resetSession: (sessionId: string) => Promise<void>;
|
|
loading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
export function useTerminalSessions(
|
|
projectId: string,
|
|
repoId: string,
|
|
instanceId: string,
|
|
): UseTerminalSessionsResult {
|
|
const [sessions, setSessions] = useState<TerminalSession[]>([]);
|
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadSessions = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const sess = await listTerminalSessions(projectId, repoId, instanceId);
|
|
setSessions(sess);
|
|
if (sess.length > 0 && !activeSessionId) {
|
|
setActiveSessionId(sess[0].id);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to load sessions");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [projectId, repoId, instanceId, activeSessionId]);
|
|
|
|
const createSession = useCallback(
|
|
async (name?: string) => {
|
|
setError(null);
|
|
try {
|
|
const newSession = await createTerminalSession(
|
|
projectId,
|
|
repoId,
|
|
instanceId,
|
|
name,
|
|
);
|
|
const session: TerminalSession = {
|
|
id: newSession.id,
|
|
name: newSession.name,
|
|
status: newSession.status,
|
|
has_websockets: false,
|
|
created_at: newSession.created_at,
|
|
last_activity_at: null,
|
|
};
|
|
setSessions((prev) => [...prev, session]);
|
|
setActiveSessionId(session.id);
|
|
return session;
|
|
} catch (err) {
|
|
const msg =
|
|
err instanceof Error ? err.message : "Failed to create session";
|
|
setError(msg);
|
|
return null;
|
|
}
|
|
},
|
|
[projectId, repoId, instanceId],
|
|
);
|
|
|
|
const closeSession = useCallback(
|
|
async (sessionId: string) => {
|
|
setError(null);
|
|
try {
|
|
await closeTerminalSession(projectId, repoId, instanceId, sessionId);
|
|
setSessions((prev) => {
|
|
const filtered = prev.filter((s) => s.id !== sessionId);
|
|
if (activeSessionId === sessionId && filtered.length > 0) {
|
|
setActiveSessionId(filtered[0].id);
|
|
} else if (filtered.length === 0) {
|
|
setActiveSessionId(null);
|
|
}
|
|
return filtered;
|
|
});
|
|
} catch (err) {
|
|
setError(
|
|
err instanceof Error ? err.message : "Failed to close session",
|
|
);
|
|
}
|
|
},
|
|
[projectId, repoId, instanceId, activeSessionId],
|
|
);
|
|
|
|
const renameSession = useCallback(
|
|
async (sessionId: string, name: string) => {
|
|
setError(null);
|
|
try {
|
|
await renameTerminalSession(
|
|
projectId,
|
|
repoId,
|
|
instanceId,
|
|
sessionId,
|
|
name,
|
|
);
|
|
setSessions((prev) =>
|
|
prev.map((s) => (s.id === sessionId ? { ...s, name } : s)),
|
|
);
|
|
} catch (err) {
|
|
setError(
|
|
err instanceof Error ? err.message : "Failed to rename session",
|
|
);
|
|
}
|
|
},
|
|
[projectId, repoId, instanceId],
|
|
);
|
|
|
|
const resetSession = useCallback(
|
|
async (sessionId: string) => {
|
|
setError(null);
|
|
try {
|
|
await resetTerminalSession(projectId, repoId, instanceId, sessionId);
|
|
// Refetch to get updated session info
|
|
await loadSessions();
|
|
} catch (err) {
|
|
setError(
|
|
err instanceof Error ? err.message : "Failed to reset session",
|
|
);
|
|
}
|
|
},
|
|
[projectId, repoId, instanceId, loadSessions],
|
|
);
|
|
|
|
// Initial load
|
|
useEffect(() => {
|
|
void loadSessions();
|
|
}, [loadSessions]);
|
|
|
|
return {
|
|
sessions,
|
|
activeSessionId,
|
|
setActiveSessionId,
|
|
createSession,
|
|
closeSession,
|
|
renameSession,
|
|
resetSession,
|
|
loading,
|
|
error,
|
|
};
|
|
}
|