feat: multi-session terminal frontend UI + tests (PR 3)

- 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)
This commit is contained in:
2026-05-28 13:35:45 +02:00
parent 0b35ae3bf0
commit 62d1bdc462
26 changed files with 4589 additions and 775 deletions
+54 -54
View File
@@ -1,87 +1,87 @@
import { apiClient } from "./client";
export interface TerminalSession {
id: string;
name: string;
status: string;
has_websockets: boolean;
created_at: string;
last_activity_at: string | null;
id: string;
name: string;
status: string;
has_websockets: boolean;
created_at: string;
last_activity_at: string | null;
}
export interface TerminalSessionListResponse {
sessions: TerminalSession[];
sessions: TerminalSession[];
}
export interface TerminalSessionCreateRequest {
name?: string;
name?: string;
}
export interface TerminalSessionCreateResponse {
id: string;
name: string;
status: string;
created_at: string;
id: string;
name: string;
status: string;
created_at: string;
}
export async function listTerminalSessions(
projectId: string,
repoId: string,
instanceId: string
projectId: string,
repoId: string,
instanceId: string,
): Promise<TerminalSession[]> {
const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions`
);
return response.data.sessions;
const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions`,
);
return response.data.sessions;
}
export async function createTerminalSession(
projectId: string,
repoId: string,
instanceId: string,
name?: string
projectId: string,
repoId: string,
instanceId: string,
name?: string,
): Promise<TerminalSessionCreateResponse> {
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions`,
{ name }
);
return response.data;
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions`,
{ name },
);
return response.data;
}
export async function closeTerminalSession(
projectId: string,
repoId: string,
instanceId: string,
sessionId: string
projectId: string,
repoId: string,
instanceId: string,
sessionId: string,
): Promise<{ status: string; session_id: string }> {
const response = await apiClient.delete(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions/${sessionId}`
);
return response.data;
const response = await apiClient.delete(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions/${sessionId}`,
);
return response.data;
}
export async function resetTerminalSession(
projectId: string,
repoId: string,
instanceId: string,
sessionId: string
projectId: string,
repoId: string,
instanceId: string,
sessionId: string,
): Promise<{ id: string; name: string; status: string }> {
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions/${sessionId}/reset`
);
return response.data;
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions/${sessionId}/reset`,
);
return response.data;
}
export async function renameTerminalSession(
projectId: string,
repoId: string,
instanceId: string,
sessionId: string,
name: string
projectId: string,
repoId: string,
instanceId: string,
sessionId: string,
name: string,
): Promise<{ id: string; name: string }> {
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions/${sessionId}/rename`,
{ name }
);
return response.data;
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/terminal/sessions/${sessionId}/rename`,
{ name },
);
return response.data;
}