diff --git a/packages/ui/src/stores/useTerminalStore.test.ts b/packages/ui/src/stores/useTerminalStore.test.ts index 773f1152..c60f0ffd 100644 --- a/packages/ui/src/stores/useTerminalStore.test.ts +++ b/packages/ui/src/stores/useTerminalStore.test.ts @@ -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']); + }); +}); diff --git a/packages/ui/src/stores/useTerminalStore.ts b/packages/ui/src/stores/useTerminalStore.ts index 6b68d32a..485717ff 100644 --- a/packages/ui/src/stores/useTerminalStore.ts +++ b/packages/ui/src/stores/useTerminalStore.ts @@ -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()( 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) {