fix(terminal): keep default terminal tab names unique after closing tabs (#2718) (#2731)

This commit is contained in:
Serhii Dziupin
2026-08-07 09:09:32 +03:00
committed by GitHub
parent bc380e6e1b
commit 3fc136c95e
2 changed files with 57 additions and 2 deletions
@@ -123,3 +123,38 @@ describe('terminal state reconciliation', () => {
expect(useTerminalStore.getState().buffers.size).toBe(0);
});
});
describe('default terminal tab labels', () => {
afterEach(() => useTerminalStore.getState().clearAll());
const labels = () =>
useTerminalStore.getState().getDirectoryState('/repo')!.tabs.map((tab) => tab.label);
// Regression for https://github.com/openchamber/openchamber/issues/2718
test('does not reuse the number of a closed tab', () => {
const first = setup();
useTerminalStore.getState().createTab('/repo');
expect(labels()).toEqual(['Terminal', 'Terminal 2']);
useTerminalStore.getState().closeTab('/repo', first);
useTerminalStore.getState().createTab('/repo');
expect(labels()).toEqual(['Terminal 2', 'Terminal 3']);
});
test('numbers past a user-renamed "Terminal N" label instead of duplicating it', () => {
const first = setup();
useTerminalStore.getState().setTabLabel('/repo', first, 'Terminal 5');
useTerminalStore.getState().createTab('/repo');
expect(labels()).toEqual(['Terminal 5', 'Terminal 6']);
});
test('ignores custom labels and starts over at "Terminal" when no default-labeled tabs remain', () => {
const first = setup();
useTerminalStore.getState().setTabLabel('/repo', first, 'build');
useTerminalStore.getState().createTab('/repo');
expect(labels()).toEqual(['build', 'Terminal']);
});
});
+22 -2
View File
@@ -157,6 +157,27 @@ function normalizeDirectory(dir: string): string {
return normalized;
}
const DEFAULT_TAB_LABEL_PATTERN = /^Terminal(?: (\d+))?$/;
/**
* Default labels must stay unique among the directory's open tabs even after
* closes (#2718), so number from the highest existing "Terminal N" suffix
* instead of the live tab count. Labels are persisted with the tabs, so the
* derivation also survives reloads without a dedicated counter. User-renamed
* labels only participate when they match the default pattern; they are never
* rewritten.
*/
const nextDefaultTabLabel = (tabs: readonly TerminalTab[]): string => {
let highest = 0;
for (const tab of tabs) {
const match = DEFAULT_TAB_LABEL_PATTERN.exec(tab.label);
if (!match) continue;
const value = match[1] ? Number.parseInt(match[1], 10) : 1;
if (Number.isSafeInteger(value)) highest = Math.max(highest, value);
}
return highest === 0 ? 'Terminal' : `Terminal ${highest + 1}`;
};
const createEmptyTab = (id: string, label: string): TerminalTab => ({
id,
terminalSessionId: null,
@@ -295,8 +316,7 @@ export const useTerminalStore = create<TerminalStore>()(
const existing = newSessions.get(key);
const nextTabId = state.nextTabId + 1;
const labelIndex = (existing?.tabs.length ?? 0) + 1;
const label = `Terminal ${labelIndex}`;
const label = nextDefaultTabLabel(existing?.tabs ?? []);
const tab = createEmptyTab(tabId, label);
if (!existing) {