import type { KnownTerminalSession } from "@t3tools/client-runtime"; import { DEFAULT_TERMINAL_ID, type ProjectScript } from "@t3tools/contracts"; import { nextTerminalId, resolveTerminalSessionLabel } from "@t3tools/shared/terminalLabels"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; export { getTerminalLabel, nextTerminalId, resolveTerminalSessionLabel, } from "@t3tools/shared/terminalLabels"; export interface TerminalMenuSession { readonly terminalId: string; readonly cwd: string | null; readonly status: "starting" | "running" | "exited" | "error" | "closed"; readonly hasRunningSubprocess: boolean; /** Server-authoritative title with the same fallback rules as web. */ readonly displayLabel: string; readonly updatedAt: string | null; } const terminalMenuSessionOrder = Order.make((left, right) => { const comparison = left.terminalId.localeCompare(right.terminalId, undefined, { numeric: true }); if (comparison === 0) { return 0; } return comparison < 0 ? -1 : 1; }); export function basename(input: string | null): string | null { if (!input) { return null; } const normalized = input.replace(/\/+$/, ""); if (normalized.length === 0) { return "/"; } const segments = normalized.split("/"); return segments[segments.length - 1] ?? normalized; } export function getTerminalStatusLabel(input: { readonly status: TerminalMenuSession["status"]; readonly hasRunningSubprocess?: boolean; }): string { if (input.status === "running") { return input.hasRunningSubprocess ? "Task running" : "Ready"; } if (input.status === "starting") { return "Starting"; } if (input.status === "exited") { return "Exited"; } if (input.status === "error") { return "Error"; } return "Not started"; } /** * Picks an id for "open another shell". Counts the terminal screen already mounted * (`activeRouteTerminalId`) as occupied so an empty session list on the primary route * still advances to `term-2` instead of `replace`-navigating to the same `default` tab. */ export function nextOpenTerminalId(input: { readonly listedTerminalIds: ReadonlyArray; readonly activeRouteTerminalId?: string | null; }): string { const listed = input.listedTerminalIds.filter((id) => id.trim().length > 0); const routeId = input.activeRouteTerminalId?.trim() ? input.activeRouteTerminalId : null; if (!routeId || listed.includes(routeId)) { return nextTerminalId(listed); } return nextTerminalId([...listed, routeId]); } export function buildTerminalMenuSessions(input: { readonly knownSessions: ReadonlyArray; readonly workspaceRoot: string | null; readonly currentSession?: TerminalMenuSession | null; }): ReadonlyArray { const sessionsById = new Map(); for (const session of input.knownSessions) { if ( session.state.status !== "running" && session.state.status !== "starting" && session.target.terminalId !== input.currentSession?.terminalId ) { continue; } sessionsById.set(session.target.terminalId, { terminalId: session.target.terminalId, cwd: session.state.summary?.cwd ?? input.workspaceRoot, status: session.state.status, hasRunningSubprocess: session.state.hasRunningSubprocess, displayLabel: resolveTerminalSessionLabel(session.target.terminalId, session.state.summary), updatedAt: session.state.updatedAt, }); } if (input.currentSession && !sessionsById.has(input.currentSession.terminalId)) { sessionsById.set(input.currentSession.terminalId, input.currentSession); } return Arr.sort(sessionsById.values(), terminalMenuSessionOrder); } export function resolveProjectScriptTerminalId(input: { readonly existingTerminalIds: ReadonlyArray; readonly hasRunningTerminal: boolean; }): string { if (!input.hasRunningTerminal) { return DEFAULT_TERMINAL_ID; } return nextTerminalId(input.existingTerminalIds); } export function projectScriptMenuLabel(script: ProjectScript): string { return script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name; } export function projectScriptMenuIcon(icon: ProjectScript["icon"]) { if (icon === "test") return "flask"; if (icon === "lint") return "checklist"; if (icon === "configure") return "wrench.and.screwdriver"; if (icon === "build") return "hammer"; if (icon === "debug") return "ladybug"; return "play"; }