Files
Netcatty/domain/hostGroupPathMutations.ts
陈大猫 36e5779d94 perf(terminal): reduce terminal tab-switch and layout jank (#1321)
* 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>
2026-06-09 03:35:03 +08:00

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,
};
}