Skip to content

Commit 5e96dff

Browse files
committed
feat(toolbar): implement pin/unpin tab functionality with localStorage persistence
- Added a Pin SVG icon to the shared registry in Icons.tsx. - Modified ToolbarTab.tsx to render the Pin icon in place of the close button when pinned. - Handled state management in Toolbar.tsx for pinned files, terminals, and agents using storage helper functions loadItem/saveItem with constant keys. - Preserved pinning order by appending new pins to the end of the arrays and sorting based on array indices. - Excluded pinned tabs from mass-closing actions ('Close All', 'Close Right') and refactored close handlers into component-level helper methods for improved readability. - Added custom styles for tab-pin-button and tab-pinned classes.
1 parent ec1ce58 commit 5e96dff

4 files changed

Lines changed: 234 additions & 36 deletions

File tree

anycode/components/Icons.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,20 @@ export const Icons = {
137137
</g>
138138
</svg>
139139
),
140+
Pin: () => (
141+
<svg
142+
width="11"
143+
height="11"
144+
viewBox="0 0 24 24"
145+
fill="currentColor"
146+
stroke="currentColor"
147+
strokeWidth="2"
148+
strokeLinecap="round"
149+
strokeLinejoin="round"
150+
style={{ transform: 'rotate(45deg)', display: 'block' }}
151+
>
152+
<path d="M12 17v5" strokeWidth="3" />
153+
<path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.89a1 1 0 0 0-.53.76c-.08.56.32 1.08.89 1.08h11.06a1 1 0 0 0 .89-1.08c-.08-.56-.37-.67-.53-.76l-1.78-.89A2 2 0 0 1 15 10.76V6a2 2 0 0 1 2-2v0a2 2 0 0 1-2-2H9a2 2 0 0 1-2 2v0a2 2 0 0 1 2 2z" />
154+
</svg>
155+
),
140156
};

anycode/components/toolbar/Toolbar.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,37 @@
9797
font-size: 16px;
9898
}
9999
}
100+
101+
.tab-pin-button {
102+
background: none;
103+
border: none;
104+
color: var(--theme-tab-foreground, #888888);
105+
cursor: pointer;
106+
padding: 3px;
107+
display: flex;
108+
align-items: center;
109+
justify-content: center;
110+
border-radius: 3px;
111+
opacity: 0.6;
112+
transition: all 0.2s ease;
113+
outline: none;
114+
margin-right: 2px;
115+
margin-left: 2px;
116+
}
117+
118+
.tab-pin-button:hover {
119+
opacity: 1;
120+
color: var(--theme-accent-background, #7ec8ff);
121+
background: rgba(255, 255, 255, 0.08);
122+
}
123+
124+
.tab-pin-button:focus {
125+
outline: none;
126+
border: none;
127+
box-shadow: none;
128+
}
129+
130+
.tab-pinned {
131+
padding-right: 6px;
132+
}
133+

anycode/components/toolbar/Toolbar.tsx

Lines changed: 152 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import type { FileState, Terminal, AcpSession } from '../../types';
2-
import type { WheelEvent } from 'react';
2+
import { WheelEvent, useState, useMemo } from 'react';
3+
import { loadItem, saveItem } from '../../storage';
34
import { TabContextMenu } from './TabContextMenu';
45
import type { TabMenuAction } from './TabContextMenu';
56
import { ToolbarTab } from './ToolbarTab';
67
import { useTabContextMenu } from './useTabContextMenu';
78
import './Toolbar.css';
89

10+
const PINNED_FILES_KEY = 'pinnedFiles';
11+
const PINNED_TERMINALS_KEY = 'pinnedTerminals';
12+
const PINNED_AGENTS_KEY = 'pinnedAgents';
13+
914
interface ToolbarProps {
1015
files: FileState[];
1116
activeFileId: string | null;
@@ -40,7 +45,122 @@ export const Toolbar = ({
4045
onCloseAgent,
4146
}: ToolbarProps) => {
4247
const { closeMenu, menuRef, openMenu, tabMenu } = useTabContextMenu();
43-
48+
49+
const [pinnedFileIds, setPinnedFileIds] = useState<string[]>(() => {
50+
return loadItem<string[]>(PINNED_FILES_KEY) ?? [];
51+
});
52+
const [pinnedTerminalIds, setPinnedTerminalIds] = useState<string[]>(() => {
53+
return loadItem<string[]>(PINNED_TERMINALS_KEY) ?? [];
54+
});
55+
const [pinnedAgentIds, setPinnedAgentIds] = useState<string[]>(() => {
56+
return loadItem<string[]>(PINNED_AGENTS_KEY) ?? [];
57+
});
58+
59+
const togglePinFile = (fileId: string) => {
60+
setPinnedFileIds((prev) => {
61+
const next = prev.includes(fileId)
62+
? prev.filter((id) => id !== fileId)
63+
: [...prev, fileId];
64+
saveItem(PINNED_FILES_KEY, next);
65+
return next;
66+
});
67+
};
68+
69+
const togglePinTerminal = (terminalId: string) => {
70+
setPinnedTerminalIds((prev) => {
71+
const next = prev.includes(terminalId)
72+
? prev.filter((id) => id !== terminalId)
73+
: [...prev, terminalId];
74+
saveItem(PINNED_TERMINALS_KEY, next);
75+
return next;
76+
});
77+
};
78+
79+
const togglePinAgent = (agentId: string) => {
80+
setPinnedAgentIds((prev) => {
81+
const next = prev.includes(agentId)
82+
? prev.filter((id) => id !== agentId)
83+
: [...prev, agentId];
84+
saveItem(PINNED_AGENTS_KEY, next);
85+
return next;
86+
});
87+
};
88+
89+
const sortedFiles = useMemo(() => {
90+
const pinned = pinnedFileIds
91+
.map((id) => files.find((f) => f.id === id))
92+
.filter((f): f is FileState => !!f);
93+
const pinnedSet = new Set(pinnedFileIds);
94+
const unpinned = files.filter((f) => !pinnedSet.has(f.id));
95+
return [...pinned, ...unpinned];
96+
}, [files, pinnedFileIds]);
97+
98+
const sortedTerminals = useMemo(() => {
99+
const pinned = pinnedTerminalIds
100+
.map((id) => terminals.find((t) => t.id === id))
101+
.filter((t): t is Terminal => !!t);
102+
const pinnedSet = new Set(pinnedTerminalIds);
103+
const unpinned = terminals.filter((t) => !pinnedSet.has(t.id));
104+
return [...pinned, ...unpinned];
105+
}, [terminals, pinnedTerminalIds]);
106+
107+
const sortedAgentSessions = useMemo(() => {
108+
const pinned = pinnedAgentIds
109+
.map((id) => agentSessions.find((s) => s.agentId === id))
110+
.filter((s): s is AcpSession => !!s);
111+
const pinnedSet = new Set(pinnedAgentIds);
112+
const unpinned = agentSessions.filter((s) => !pinnedSet.has(s.agentId));
113+
return [...pinned, ...unpinned];
114+
}, [agentSessions, pinnedAgentIds]);
115+
116+
// Mass tab closing helper handlers
117+
const handleCloseRightFiles = (fileId: string) => {
118+
const fileIndexSorted = sortedFiles.findIndex((f) => f.id === fileId);
119+
if (fileIndexSorted < 0) return;
120+
sortedFiles.slice(fileIndexSorted + 1).forEach((f) => {
121+
if (!pinnedFileIds.includes(f.id)) {
122+
onCloseFile(f.id);
123+
}
124+
});
125+
};
126+
127+
const handleCloseAllFiles = () => {
128+
files.forEach((f) => {
129+
if (!pinnedFileIds.includes(f.id)) {
130+
onCloseFile(f.id);
131+
}
132+
});
133+
};
134+
135+
const handleCloseRightTerminals = (terminalId: string) => {
136+
const terminalIndexSorted = sortedTerminals.findIndex((t) => t.id === terminalId);
137+
if (terminalIndexSorted < 0) return;
138+
const rightTerminals = sortedTerminals.slice(terminalIndexSorted + 1);
139+
for (let i = rightTerminals.length - 1; i >= 0; i -= 1) {
140+
const t = rightTerminals[i];
141+
if (!pinnedTerminalIds.includes(t.id)) {
142+
onCloseTerminal(t.id);
143+
}
144+
}
145+
};
146+
147+
const handleCloseAllTerminals = () => {
148+
for (let i = terminals.length - 1; i >= 0; i -= 1) {
149+
const t = terminals[i];
150+
if (!pinnedTerminalIds.includes(t.id)) {
151+
onCloseTerminal(t.id);
152+
}
153+
}
154+
};
155+
156+
const handleCloseAllAgents = () => {
157+
agentSessions.forEach((s) => {
158+
if (!pinnedAgentIds.includes(s.agentId)) {
159+
onCloseAgent(s.agentId);
160+
}
161+
});
162+
};
163+
44164
const handleTabsWheel = (event: WheelEvent<HTMLDivElement>) => {
45165
if (event.deltaY === 0) return;
46166
const tabsElement = event.currentTarget;
@@ -62,24 +182,26 @@ export const Toolbar = ({
62182
case 'file': {
63183
const file = files.find((f) => f.id === tabMenu.targetId);
64184
if (!file) return [];
65-
const fileIndex = files.findIndex((f) => f.id === file.id);
66-
const hasRight = fileIndex >= 0 && files.length > fileIndex + 1;
185+
const isPinned = pinnedFileIds.includes(file.id);
186+
const fileIndexSorted = sortedFiles.findIndex((f) => f.id === file.id);
187+
const hasRight = fileIndexSorted >= 0 && sortedFiles.length > fileIndexSorted + 1;
67188
return [
68-
[{ key: 'copy-path', label: 'Copy path', onClick: () => copyText(file.id) }],
189+
[
190+
{ key: 'copy-path', label: 'Copy path', onClick: () => copyText(file.id) },
191+
{ key: 'pin-file', label: isPinned ? 'Unpin' : 'Pin', onClick: () => togglePinFile(file.id) },
192+
],
69193
[
70194
{ key: 'close', label: 'Close', onClick: () => onCloseFile(file.id) },
71195
{
72196
key: 'close-right',
73197
label: 'Close right',
74198
disabled: !hasRight,
75-
onClick: () => {
76-
files.slice(fileIndex + 1).forEach((f) => onCloseFile(f.id));
77-
},
199+
onClick: () => handleCloseRightFiles(file.id),
78200
},
79201
{
80202
key: 'close-all',
81203
label: 'Close all',
82-
onClick: () => files.forEach((f) => onCloseFile(f.id)),
204+
onClick: handleCloseAllFiles,
83205
},
84206
],
85207
];
@@ -88,37 +210,34 @@ export const Toolbar = ({
88210
const terminalIndex = terminals.findIndex((t) => t.id === tabMenu.targetId);
89211
if (terminalIndex < 0) return [];
90212
const terminal = terminals[terminalIndex];
91-
const hasRight = terminals.length > terminalIndex + 1;
213+
const isPinned = pinnedTerminalIds.includes(terminal.id);
214+
const terminalIndexSorted = sortedTerminals.findIndex((t) => t.id === terminal.id);
215+
const hasRight = sortedTerminals.length > terminalIndexSorted + 1;
92216
return [
93-
[{ key: 'copy-terminal-name', label: 'Copy terminal name', onClick: () => copyText(terminal.name) }],
217+
[
218+
{ key: 'copy-terminal-name', label: 'Copy terminal name', onClick: () => copyText(terminal.name) },
219+
{ key: 'pin-terminal', label: isPinned ? 'Unpin' : 'Pin', onClick: () => togglePinTerminal(terminal.id) },
220+
],
94221
[
95222
{ key: 'close-terminal', label: 'Close', onClick: () => onCloseTerminal(terminal.id) },
96223
{
97224
key: 'close-right-terminals',
98225
label: 'Close right',
99226
disabled: !hasRight,
100-
// Close from the end so indices stay valid
101-
onClick: () => {
102-
for (let i = terminals.length - 1; i > terminalIndex; i -= 1) {
103-
onCloseTerminal(terminals[i].id);
104-
}
105-
},
227+
onClick: () => handleCloseRightTerminals(terminal.id),
106228
},
107229
{
108230
key: 'close-all-terminals',
109231
label: 'Close all',
110-
onClick: () => {
111-
for (let i = terminals.length - 1; i >= 0; i -= 1) {
112-
onCloseTerminal(terminals[i].id);
113-
}
114-
},
232+
onClick: handleCloseAllTerminals,
115233
},
116234
],
117235
];
118236
}
119237
case 'agent': {
120238
const agent = agentSessions.find((s) => s.agentId === tabMenu.targetId);
121239
if (!agent) return [];
240+
const isPinned = pinnedAgentIds.includes(agent.agentId);
122241
return [
123242
[
124243
{ key: 'copy-agent-id', label: 'Copy agent id', onClick: () => copyText(agent.agentId) },
@@ -127,13 +246,14 @@ export const Toolbar = ({
127246
label: 'Copy agent name',
128247
onClick: () => copyText(agent.agentName || agent.agentId),
129248
},
249+
{ key: 'pin-agent', label: isPinned ? 'Unpin' : 'Pin', onClick: () => togglePinAgent(agent.agentId) },
130250
],
131251
[
132252
{ key: 'close-agent', label: 'Close', onClick: () => onCloseAgent(agent.agentId) },
133253
{
134254
key: 'close-all-agents',
135255
label: 'Close all',
136-
onClick: () => agentSessions.forEach((s) => onCloseAgent(s.agentId)),
256+
onClick: handleCloseAllAgents,
137257
},
138258
],
139259
];
@@ -144,34 +264,40 @@ export const Toolbar = ({
144264
return (
145265
<div className="toolbar">
146266
<div className="toolbar-tabs" onWheel={handleTabsWheel}>
147-
{files.map((file) => (
267+
{sortedFiles.map((file) => (
148268
<ToolbarTab
149269
key={file.id}
150270
active={activeFileId === file.id}
151271
label={file.name}
152272
title={file.id}
273+
pinned={pinnedFileIds.includes(file.id)}
274+
onUnpin={() => togglePinFile(file.id)}
153275
onSelect={() => onSelectFile(file.id)}
154276
onClose={() => onCloseFile(file.id)}
155277
onContextMenu={(event) => openMenu(event, 'file', file.id)}
156278
/>
157279
))}
158-
{terminals.map((terminal) => (
280+
{sortedTerminals.map((terminal) => (
159281
<ToolbarTab
160282
key={`toolbar-terminal-${terminal.id}`}
161283
active={activeTerminalId === terminal.id}
162284
label={`term:${terminal.name}`}
163285
variant="terminal"
286+
pinned={pinnedTerminalIds.includes(terminal.id)}
287+
onUnpin={() => togglePinTerminal(terminal.id)}
164288
onSelect={() => onSelectTerminal(terminal.id)}
165289
onClose={() => onCloseTerminal(terminal.id)}
166290
onContextMenu={(event) => openMenu(event, 'terminal', terminal.id)}
167291
/>
168292
))}
169-
{agentSessions.map((session) => (
293+
{sortedAgentSessions.map((session) => (
170294
<ToolbarTab
171295
key={`toolbar-agent-${session.agentId}`}
172296
active={activeAgentId === session.agentId}
173297
label={session.agentName || session.agentId}
174298
variant="agent"
299+
pinned={pinnedAgentIds.includes(session.agentId)}
300+
onUnpin={() => togglePinAgent(session.agentId)}
175301
onSelect={() => onSelectAgent(session.agentId)}
176302
onClose={() => onCloseAgent(session.agentId)}
177303
onContextMenu={(event) => openMenu(event, 'agent', session.agentId)}

0 commit comments

Comments
 (0)