* perf(terminal): smooth layout drags and faster tab switching Defer xterm refit during split, sidebar, and host-tree drags while keeping pane containers in sync with live layout measurements. Refactor TerminalLayer into focused sections with TabBridge/memo optimizations and add the terminal host tree sidebar. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): keep side panels alive and guard session attach races Prevent terminal boot unmount from leaking backend sessions, keep SFTP/scripts/theme/AI state when switching side tabs, and defer heavy SFTP UI mount so first entry stays responsive. Co-authored-by: Cursor <cursoragent@cursor.com> * perf(terminal): reduce tab switch jank --------- Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
import type { Host, ManagedSource } from '../types';
|
|
|
|
export function groupDisplayName(groupPath: string): string {
|
|
return groupPath.split('/').filter(Boolean).pop() ?? groupPath;
|
|
}
|
|
|
|
export function computeRenamedGroupPath(renameTargetPath: string, nextName: string): string {
|
|
const segments = renameTargetPath.split('/').filter(Boolean);
|
|
const parent = segments.slice(0, -1).join('/');
|
|
return parent ? `${parent}/${nextName}` : nextName;
|
|
}
|
|
|
|
export function allocateUnnamedGroupPath(
|
|
customGroups: string[],
|
|
parentPath: string | null,
|
|
baseName: string,
|
|
): { name: string; path: string } {
|
|
let name = baseName;
|
|
let counter = 2;
|
|
while (true) {
|
|
const path = parentPath ? `${parentPath}/${name}` : name;
|
|
if (!customGroups.includes(path)) {
|
|
return { name, path };
|
|
}
|
|
name = `${baseName} ${counter}`;
|
|
counter += 1;
|
|
}
|
|
}
|
|
|
|
export function ensureAncestorPathsExpanded(
|
|
groupPath: string,
|
|
ensurePathExpanded: (path: string) => void,
|
|
) {
|
|
const segments = groupPath.split('/').filter(Boolean);
|
|
for (let i = 1; i <= segments.length; i++) {
|
|
ensurePathExpanded(segments.slice(0, i).join('/'));
|
|
}
|
|
}
|
|
|
|
export type GroupPathRenameResult =
|
|
| {
|
|
ok: true;
|
|
nextPath: string;
|
|
updatedGroups: string[];
|
|
updatedHosts: Host[];
|
|
updatedManagedSources: ManagedSource[];
|
|
}
|
|
| { ok: false; error: 'required' | 'invalidChars' | 'duplicatePath' | 'unchanged' };
|
|
|
|
export function applyGroupPathRename(params: {
|
|
renameTargetPath: string;
|
|
nextName: string;
|
|
customGroups: string[];
|
|
hosts: Host[];
|
|
managedSources: ManagedSource[];
|
|
}): GroupPathRenameResult {
|
|
const trimmed = params.nextName.trim();
|
|
if (!trimmed) {
|
|
return { ok: false, error: 'required' };
|
|
}
|
|
if (trimmed.includes('/') || trimmed.includes('\\')) {
|
|
return { ok: false, error: 'invalidChars' };
|
|
}
|
|
|
|
const nextPath = computeRenamedGroupPath(params.renameTargetPath, trimmed);
|
|
if (nextPath === params.renameTargetPath) {
|
|
return { ok: false, error: 'unchanged' };
|
|
}
|
|
if (params.customGroups.includes(nextPath)) {
|
|
return { ok: false, error: 'duplicatePath' };
|
|
}
|
|
|
|
const { renameTargetPath, customGroups, hosts, managedSources } = params;
|
|
const updatedGroups = customGroups.map((groupPath) => {
|
|
if (groupPath === renameTargetPath) return nextPath;
|
|
if (groupPath.startsWith(`${renameTargetPath}/`)) {
|
|
return nextPath + groupPath.slice(renameTargetPath.length);
|
|
}
|
|
return groupPath;
|
|
});
|
|
const updatedHosts = hosts.map((host) => {
|
|
const group = host.group || '';
|
|
if (group === renameTargetPath) return { ...host, group: nextPath };
|
|
if (group.startsWith(`${renameTargetPath}/`)) {
|
|
return { ...host, group: nextPath + group.slice(renameTargetPath.length) };
|
|
}
|
|
return host;
|
|
});
|
|
const updatedManagedSources = managedSources.map((source) => {
|
|
if (source.groupName === renameTargetPath) return { ...source, groupName: nextPath };
|
|
if (source.groupName.startsWith(`${renameTargetPath}/`)) {
|
|
return { ...source, groupName: nextPath + source.groupName.slice(renameTargetPath.length) };
|
|
}
|
|
return source;
|
|
});
|
|
|
|
return {
|
|
ok: true,
|
|
nextPath,
|
|
updatedGroups: Array.from(new Set(updatedGroups)),
|
|
updatedHosts,
|
|
updatedManagedSources,
|
|
};
|
|
}
|