Skip to content

Commit ed76f8d

Browse files
authored
fix(terminal): renumber auto-named tabs to prevent duplicate labels (#447)
1 parent cc3ca1d commit ed76f8d

2 files changed

Lines changed: 145 additions & 9 deletions

File tree

src/features/terminal/hooks/useTerminalTabs.test.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,95 @@ describe("useTerminalTabs.ensureTerminalWithTitle", () => {
3535
]);
3636
});
3737
});
38+
39+
describe("useTerminalTabs auto-named tabs", () => {
40+
it("renumbers remaining auto-named tabs after closing one", () => {
41+
const { result } = renderHook(() =>
42+
useTerminalTabs({ activeWorkspaceId: "workspace-1" }),
43+
);
44+
45+
let firstId = "";
46+
let secondId = "";
47+
act(() => {
48+
firstId = result.current.createTerminal("workspace-1");
49+
secondId = result.current.createTerminal("workspace-1");
50+
});
51+
52+
act(() => {
53+
result.current.closeTerminal("workspace-1", firstId);
54+
});
55+
56+
expect(result.current.terminals).toEqual([
57+
{ id: secondId, title: "Terminal 1" },
58+
]);
59+
});
60+
61+
it("does not create duplicate auto-named labels after close and create", () => {
62+
const { result } = renderHook(() =>
63+
useTerminalTabs({ activeWorkspaceId: "workspace-1" }),
64+
);
65+
66+
let firstId = "";
67+
let secondId = "";
68+
let thirdId = "";
69+
act(() => {
70+
firstId = result.current.createTerminal("workspace-1");
71+
secondId = result.current.createTerminal("workspace-1");
72+
});
73+
74+
act(() => {
75+
result.current.closeTerminal("workspace-1", firstId);
76+
});
77+
78+
act(() => {
79+
thirdId = result.current.createTerminal("workspace-1");
80+
});
81+
82+
expect(result.current.terminals).toEqual([
83+
{ id: secondId, title: "Terminal 1" },
84+
{ id: thirdId, title: "Terminal 2" },
85+
]);
86+
});
87+
88+
it("keeps custom titles while numbering auto-named tabs independently", () => {
89+
const { result } = renderHook(() =>
90+
useTerminalTabs({ activeWorkspaceId: "workspace-1" }),
91+
);
92+
93+
let firstAutoId = "";
94+
let secondAutoId = "";
95+
act(() => {
96+
result.current.ensureTerminalWithTitle("workspace-1", "launch", "Launch");
97+
firstAutoId = result.current.createTerminal("workspace-1");
98+
secondAutoId = result.current.createTerminal("workspace-1");
99+
});
100+
101+
expect(result.current.terminals).toEqual([
102+
{ id: "launch", title: "Launch" },
103+
{ id: firstAutoId, title: "Terminal 1" },
104+
{ id: secondAutoId, title: "Terminal 2" },
105+
]);
106+
});
107+
108+
it("converts an auto-named tab to custom and renumbers remaining auto tabs", () => {
109+
const { result } = renderHook(() =>
110+
useTerminalTabs({ activeWorkspaceId: "workspace-1" }),
111+
);
112+
113+
let firstAutoId = "";
114+
let secondAutoId = "";
115+
act(() => {
116+
firstAutoId = result.current.createTerminal("workspace-1");
117+
secondAutoId = result.current.createTerminal("workspace-1");
118+
});
119+
120+
act(() => {
121+
result.current.ensureTerminalWithTitle("workspace-1", firstAutoId, "Launch");
122+
});
123+
124+
expect(result.current.terminals).toEqual([
125+
{ id: firstAutoId, title: "Launch" },
126+
{ id: secondAutoId, title: "Terminal 1" },
127+
]);
128+
});
129+
});

src/features/terminal/hooks/useTerminalTabs.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export type TerminalTab = {
55
title: string;
66
};
77

8+
type TerminalTabRecord = TerminalTab & {
9+
autoNamed: boolean;
10+
};
11+
812
type UseTerminalTabsOptions = {
913
activeWorkspaceId: string | null;
1014
onCloseTerminal?: (workspaceId: string, terminalId: string) => void;
@@ -17,12 +21,33 @@ function createTerminalId() {
1721
return `terminal-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1822
}
1923

24+
function renumberAutoNamedTabs(tabs: TerminalTabRecord[]): TerminalTabRecord[] {
25+
let autoNamedIndex = 1;
26+
let changed = false;
27+
const nextTabs = tabs.map((tab) => {
28+
if (!tab.autoNamed) {
29+
return tab;
30+
}
31+
const nextTitle = `Terminal ${autoNamedIndex}`;
32+
autoNamedIndex += 1;
33+
if (tab.title === nextTitle) {
34+
return tab;
35+
}
36+
changed = true;
37+
return {
38+
...tab,
39+
title: nextTitle,
40+
};
41+
});
42+
return changed ? nextTabs : tabs;
43+
}
44+
2045
export function useTerminalTabs({
2146
activeWorkspaceId,
2247
onCloseTerminal,
2348
}: UseTerminalTabsOptions) {
2449
const [tabsByWorkspace, setTabsByWorkspace] = useState<
25-
Record<string, TerminalTab[]>
50+
Record<string, TerminalTabRecord[]>
2651
>({});
2752
const [activeTerminalIdByWorkspace, setActiveTerminalIdByWorkspace] = useState<
2853
Record<string, string | null>
@@ -32,10 +57,13 @@ export function useTerminalTabs({
3257
const id = createTerminalId();
3358
setTabsByWorkspace((prev) => {
3459
const existing = prev[workspaceId] ?? [];
35-
const title = `Terminal ${existing.length + 1}`;
60+
const nextTabs = renumberAutoNamedTabs([
61+
...existing,
62+
{ id, title: "", autoNamed: true },
63+
]);
3664
return {
3765
...prev,
38-
[workspaceId]: [...existing, { id, title }],
66+
[workspaceId]: nextTabs,
3967
};
4068
});
4169
setActiveTerminalIdByWorkspace((prev) => ({ ...prev, [workspaceId]: id }));
@@ -48,17 +76,28 @@ export function useTerminalTabs({
4876
const existing = prev[workspaceId] ?? [];
4977
const index = existing.findIndex((tab) => tab.id === terminalId);
5078
if (index === -1) {
79+
const nextTabs = renumberAutoNamedTabs([
80+
...existing,
81+
{ id: terminalId, title, autoNamed: false },
82+
]);
5183
return {
5284
...prev,
53-
[workspaceId]: [...existing, { id: terminalId, title }],
85+
[workspaceId]: nextTabs,
5486
};
5587
}
56-
if (existing[index].title === title) {
88+
if (!existing[index].autoNamed && existing[index].title === title) {
5789
return prev;
5890
}
5991
const nextTabs = existing.slice();
60-
nextTabs[index] = { ...existing[index], title };
61-
return { ...prev, [workspaceId]: nextTabs };
92+
nextTabs[index] = {
93+
...existing[index],
94+
title,
95+
autoNamed: false,
96+
};
97+
return {
98+
...prev,
99+
[workspaceId]: renumberAutoNamedTabs(nextTabs),
100+
};
62101
});
63102
setActiveTerminalIdByWorkspace((prev) => ({ ...prev, [workspaceId]: terminalId }));
64103
return terminalId;
@@ -70,7 +109,9 @@ export function useTerminalTabs({
70109
(workspaceId: string, terminalId: string) => {
71110
setTabsByWorkspace((prev) => {
72111
const existing = prev[workspaceId] ?? [];
73-
const nextTabs = existing.filter((tab) => tab.id !== terminalId);
112+
const nextTabs = renumberAutoNamedTabs(
113+
existing.filter((tab) => tab.id !== terminalId),
114+
);
74115
setActiveTerminalIdByWorkspace((prevActive) => {
75116
const active = prevActive[workspaceId];
76117
if (active !== terminalId) {
@@ -113,7 +154,10 @@ export function useTerminalTabs({
113154
if (!activeWorkspaceId) {
114155
return [];
115156
}
116-
return tabsByWorkspace[activeWorkspaceId] ?? [];
157+
return (tabsByWorkspace[activeWorkspaceId] ?? []).map(({ id, title }) => ({
158+
id,
159+
title,
160+
}));
117161
}, [activeWorkspaceId, tabsByWorkspace]);
118162

119163
const activeTerminalId = useMemo(() => {

0 commit comments

Comments
 (0)