feat: redesign settings pages to match canonical flat UI patterns (#493)
* refactor(settings): new IA shell + projects section + skills catalog discoverability * chore(settings): split providers list by scope; show user before project * fix: navigation flow in mobile Settings * feat: redesign settings pages to use modern elevated surface patterns * feat: replace helper text with tooltips in settings * ui: redesign update dialog and fix external link routing - Restructures UpdateDialog to focus on changelog readability with a wider max-w-4xl canvas - Highlights @username contributor mentions with theme primary color - Strips excessive vertical padding and right-aligns compact action buttons - Disables streamdown's internal link safety dialog in favor of direct Tauri shell routing * feat: refactor Git identities into dedicated Git settings page * feat: unify sidebar background styling across VS Code and web/mobile * fix: adjust button styling and layout for mobile settings pages * feat: add MCP settings page and sidebar * feat: hide models in provider view (thanks to @nguyenngothuong) * feat: add "Add new provider" option to model selector dropdown * fix: local evroc logo + provider dropdown icons * fix: increase width of provider menu * fix: dark theme background color for better contrast * feat: update @opencode-ai/sdk dependency to v1.2.10 * fix: restore session sorting to only use updated time * fix: added settings for sessions deletion dialog * fix: adjust padding on settings pages for better layout * fix: standardize select dropdown height across UI * fix: agent selector UI and notification settings * fix: remove redundant helper text from settings pages * fix: update UI layout for description fields * fix: remove border-none and shadow-none from textarea classes * fix: enable context menu on sidebar items * feat: refactor UI controls and layout patterns across settings pages * fix: use headerless blocks when page title already provides context * fix: remove subtask option from command settings * fix: refactor mcp page settings * fix: reduce spacing in skills configuration pages * feat: refactor voice settings * feat: refactor settings sidebar sections
This commit is contained in:
committed by
GitHub
parent
d2d39c48ac
commit
d2358c2c03
@@ -74,7 +74,7 @@ export function GitHubIssuePickerDialog({
|
||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
const setSidebarSection = useUIStore((state) => state.setSidebarSection);
|
||||
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||
|
||||
const projectDirectory = activeProject?.path ?? null;
|
||||
@@ -181,9 +181,9 @@ export function GitHubIssuePickerDialog({
|
||||
const repoUrl = result?.repo?.url ?? null;
|
||||
|
||||
const openGitHubSettings = React.useCallback(() => {
|
||||
setSidebarSection('settings');
|
||||
setSettingsPage('github');
|
||||
setSettingsDialogOpen(true);
|
||||
}, [setSettingsDialogOpen, setSidebarSection]);
|
||||
}, [setSettingsDialogOpen, setSettingsPage]);
|
||||
|
||||
const filtered = React.useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
|
||||
@@ -110,7 +110,7 @@ export function GitHubPullRequestPickerDialog({
|
||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
const setSidebarSection = useUIStore((state) => state.setSidebarSection);
|
||||
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||
|
||||
const projectDirectory = activeProject?.path ?? null;
|
||||
@@ -300,9 +300,9 @@ export function GitHubPullRequestPickerDialog({
|
||||
const repoUrl = result?.repo?.url ?? null;
|
||||
|
||||
const openGitHubSettings = React.useCallback(() => {
|
||||
setSidebarSection('settings');
|
||||
setSettingsPage('github');
|
||||
setSettingsDialogOpen(true);
|
||||
}, [setSettingsDialogOpen, setSidebarSection]);
|
||||
}, [setSettingsDialogOpen, setSettingsPage]);
|
||||
|
||||
const filtered = React.useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
|
||||
@@ -21,6 +21,7 @@ import { removeProjectWorktree } from '@/lib/worktrees/worktreeManager';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
|
||||
import { isDesktopLocalOriginActive, isTauriShell } from '@/lib/desktop';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
@@ -64,6 +65,8 @@ export const SessionDialogs: React.FC = () => {
|
||||
loadSessions,
|
||||
getWorktreeMetadata,
|
||||
} = useSessionStore();
|
||||
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
||||
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
|
||||
const { currentDirectory, homeDirectory, isHomeReady } = useDirectoryStore();
|
||||
const { projects, addProject, activeProjectId } = useProjectsStore();
|
||||
const { requestAccess, startAccessing } = useFileSystemAccess();
|
||||
@@ -194,11 +197,52 @@ export const SessionDialogs: React.FC = () => {
|
||||
setDirtyWorktreePaths(new Set());
|
||||
}, []);
|
||||
|
||||
const deleteSessionsWithoutDialog = React.useCallback(async (payload: { sessions: Session[]; dateLabel?: string }) => {
|
||||
if (payload.sessions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.sessions.length === 1) {
|
||||
const target = payload.sessions[0];
|
||||
const success = await deleteSession(target.id);
|
||||
if (success) {
|
||||
toast.success('Session deleted');
|
||||
} else {
|
||||
toast.error('Failed to delete session');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = payload.sessions.map((session) => session.id);
|
||||
const { deletedIds, failedIds } = await deleteSessions(ids);
|
||||
|
||||
if (deletedIds.length > 0) {
|
||||
const successDescription = failedIds.length > 0
|
||||
? `${failedIds.length} session${failedIds.length === 1 ? '' : 's'} could not be deleted.`
|
||||
: payload.dateLabel
|
||||
? `Removed all sessions from ${payload.dateLabel}.`
|
||||
: undefined;
|
||||
toast.success(`Deleted ${deletedIds.length} session${deletedIds.length === 1 ? '' : 's'}`, {
|
||||
description: renderToastDescription(successDescription),
|
||||
});
|
||||
}
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
toast.error(`Failed to delete ${failedIds.length} session${failedIds.length === 1 ? '' : 's'}`, {
|
||||
description: renderToastDescription('Please try again in a moment.'),
|
||||
});
|
||||
}
|
||||
}, [deleteSession, deleteSessions]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return sessionEvents.onDeleteRequest((payload) => {
|
||||
if (!showDeletionDialog && (payload.mode ?? 'session') === 'session') {
|
||||
void deleteSessionsWithoutDialog(payload);
|
||||
return;
|
||||
}
|
||||
openDeleteDialog(payload);
|
||||
});
|
||||
}, [openDeleteDialog]);
|
||||
}, [openDeleteDialog, showDeletionDialog, deleteSessionsWithoutDialog]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return sessionEvents.onDirectoryRequest(() => {
|
||||
@@ -634,18 +678,29 @@ export const SessionDialogs: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="ghost" onClick={closeDeleteDialog} disabled={isProcessingDelete}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleConfirmDelete} disabled={isProcessingDelete}>
|
||||
{isProcessingDelete
|
||||
? 'Deleting…'
|
||||
: deleteDialog?.sessions.length === 1
|
||||
? 'Delete session'
|
||||
: 'Delete sessions'}
|
||||
</Button>
|
||||
</>
|
||||
<div className="flex w-full items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDeletionDialog(!showDeletionDialog)}
|
||||
className="inline-flex items-center gap-1.5 typography-meta text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
|
||||
aria-pressed={!showDeletionDialog}
|
||||
>
|
||||
{!showDeletionDialog ? <RiCheckboxLine className="size-4 text-primary" /> : <RiCheckboxBlankLine className="size-4" />}
|
||||
Never ask
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" onClick={closeDeleteDialog} disabled={isProcessingDelete}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleConfirmDelete} disabled={isProcessingDelete}>
|
||||
{isProcessingDelete
|
||||
? 'Deleting…'
|
||||
: deleteDialog?.sessions.length === 1
|
||||
? 'Delete session'
|
||||
: 'Delete sessions'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const deleteDialogTitle = isWorktreeDelete
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
RiAddLine,
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiCheckboxBlankLine,
|
||||
RiCheckboxLine,
|
||||
RiCheckLine,
|
||||
RiCloseLine,
|
||||
RiDeleteBinLine,
|
||||
@@ -194,25 +196,14 @@ const getSessionCreatedAt = (session: Session): number => {
|
||||
return toFiniteNumber(session.time?.created) ?? 0;
|
||||
};
|
||||
|
||||
const getSessionUpdatedAt = (session: Session, attentionStates?: Map<string, { lastUserMessageAt: number | null; lastStatusChangeAt: number }>): number => {
|
||||
const baseUpdated = toFiniteNumber(session.time?.updated) ?? 0;
|
||||
if (!attentionStates) return baseUpdated;
|
||||
|
||||
const attention = attentionStates.get(session.id);
|
||||
if (!attention) return baseUpdated;
|
||||
|
||||
return Math.max(
|
||||
baseUpdated,
|
||||
attention.lastUserMessageAt ?? 0,
|
||||
attention.lastStatusChangeAt ?? 0
|
||||
);
|
||||
const getSessionUpdatedAt = (session: Session): number => {
|
||||
return toFiniteNumber(session.time?.updated) ?? toFiniteNumber(session.time?.created) ?? 0;
|
||||
};
|
||||
|
||||
const compareSessionsByPinnedAndTime = (
|
||||
a: Session,
|
||||
b: Session,
|
||||
pinnedSessionIds: Set<string>,
|
||||
attentionStates?: Map<string, { lastUserMessageAt: number | null; lastStatusChangeAt: number }>
|
||||
pinnedSessionIds: Set<string>
|
||||
): number => {
|
||||
const aPinned = pinnedSessionIds.has(a.id);
|
||||
const bPinned = pinnedSessionIds.has(b.id);
|
||||
@@ -224,7 +215,7 @@ const compareSessionsByPinnedAndTime = (
|
||||
return getSessionCreatedAt(b) - getSessionCreatedAt(a);
|
||||
}
|
||||
|
||||
return getSessionUpdatedAt(b, attentionStates) - getSessionUpdatedAt(a, attentionStates);
|
||||
return getSessionUpdatedAt(b) - getSessionUpdatedAt(a);
|
||||
};
|
||||
|
||||
const centerDragOverlayUnderPointer: Modifier = ({ transform, activeNodeRect, activatorEvent }) => {
|
||||
@@ -883,6 +874,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
||||
const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher);
|
||||
const notifyOnSubtasks = useUIStore((state) => state.notifyOnSubtasks);
|
||||
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
||||
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
|
||||
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
|
||||
|
||||
// Session Folders store
|
||||
@@ -1026,8 +1019,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}, []);
|
||||
|
||||
const sortedSessions = React.useMemo(() => {
|
||||
return [...sessions].sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds, sessionAttentionStates));
|
||||
}, [sessions, pinnedSessionIds, sessionAttentionStates]);
|
||||
return [...sessions].sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds));
|
||||
}, [sessions, pinnedSessionIds]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -1081,9 +1074,9 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
collection.push(session);
|
||||
map.set(parentID, collection);
|
||||
});
|
||||
map.forEach((list) => list.sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds, sessionAttentionStates)));
|
||||
map.forEach((list) => list.sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds)));
|
||||
return map;
|
||||
}, [sortedSessions, pinnedSessionIds, sessionAttentionStates]);
|
||||
}, [sortedSessions, pinnedSessionIds]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const directories = new Set<string>();
|
||||
@@ -1314,27 +1307,19 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const deleteSession = useSessionStore((state) => state.deleteSession);
|
||||
const deleteSessions = useSessionStore((state) => state.deleteSessions);
|
||||
|
||||
const handleDeleteSession = React.useCallback(
|
||||
(session: Session) => {
|
||||
const executeDeleteSession = React.useCallback(
|
||||
async (session: Session) => {
|
||||
const descendants = collectDescendants(session.id);
|
||||
setDeleteSessionConfirm({ session, descendantCount: descendants.length });
|
||||
},
|
||||
[collectDescendants],
|
||||
);
|
||||
|
||||
const confirmDeleteSession = React.useCallback(async () => {
|
||||
if (!deleteSessionConfirm) return;
|
||||
const { session } = deleteSessionConfirm;
|
||||
setDeleteSessionConfirm(null);
|
||||
const descendants = collectDescendants(session.id);
|
||||
if (descendants.length === 0) {
|
||||
const success = await deleteSession(session.id);
|
||||
if (success) {
|
||||
toast.success('Session deleted');
|
||||
} else {
|
||||
toast.error('Failed to delete session');
|
||||
if (descendants.length === 0) {
|
||||
const success = await deleteSession(session.id);
|
||||
if (success) {
|
||||
toast.success('Session deleted');
|
||||
} else {
|
||||
toast.error('Failed to delete session');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
const ids = [session.id, ...descendants.map((s) => s.id)];
|
||||
const { deletedIds, failedIds } = await deleteSessions(ids);
|
||||
if (deletedIds.length > 0) {
|
||||
@@ -1343,8 +1328,28 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
if (failedIds.length > 0) {
|
||||
toast.error(`Failed to delete ${failedIds.length} session${failedIds.length === 1 ? '' : 's'}`);
|
||||
}
|
||||
}
|
||||
}, [deleteSessionConfirm, collectDescendants, deleteSession, deleteSessions]);
|
||||
},
|
||||
[collectDescendants, deleteSession, deleteSessions],
|
||||
);
|
||||
|
||||
const handleDeleteSession = React.useCallback(
|
||||
(session: Session) => {
|
||||
const descendants = collectDescendants(session.id);
|
||||
if (!showDeletionDialog) {
|
||||
void executeDeleteSession(session);
|
||||
return;
|
||||
}
|
||||
setDeleteSessionConfirm({ session, descendantCount: descendants.length });
|
||||
},
|
||||
[collectDescendants, showDeletionDialog, executeDeleteSession],
|
||||
);
|
||||
|
||||
const confirmDeleteSession = React.useCallback(async () => {
|
||||
if (!deleteSessionConfirm) return;
|
||||
const { session } = deleteSessionConfirm;
|
||||
setDeleteSessionConfirm(null);
|
||||
await executeDeleteSession(session);
|
||||
}, [deleteSessionConfirm, executeDeleteSession]);
|
||||
|
||||
const confirmDeleteFolder = React.useCallback(() => {
|
||||
if (!deleteFolderConfirm) return;
|
||||
@@ -1396,6 +1401,24 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
});
|
||||
}, [safeStorage]);
|
||||
|
||||
const createFolderAndStartRename = React.useCallback(
|
||||
(scopeKey: string, parentId?: string | null) => {
|
||||
if (!scopeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parentId && collapsedFolderIds.has(parentId)) {
|
||||
toggleFolderCollapse(parentId);
|
||||
}
|
||||
|
||||
const newFolder = createFolder(scopeKey, 'New folder', parentId);
|
||||
setRenamingFolderId(newFolder.id);
|
||||
setRenameFolderDraft(newFolder.name);
|
||||
return newFolder;
|
||||
},
|
||||
[collapsedFolderIds, toggleFolderCollapse, createFolder],
|
||||
);
|
||||
|
||||
const buildNode = React.useCallback(
|
||||
(session: Session): SessionNode => {
|
||||
const children = childrenMap.get(session.id) ?? [];
|
||||
@@ -1418,7 +1441,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
projectIsRepo: boolean,
|
||||
) => {
|
||||
const normalizedProjectRoot = normalizePath(projectRoot ?? null);
|
||||
const sortedProjectSessions = [...projectSessions].sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds, sessionAttentionStates));
|
||||
const sortedProjectSessions = [...projectSessions].sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds));
|
||||
|
||||
const sessionMap = new Map(sortedProjectSessions.map((session) => [session.id, session]));
|
||||
const childrenMap = new Map<string, Session[]>();
|
||||
@@ -1431,7 +1454,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
collection.push(session);
|
||||
childrenMap.set(parentID, collection);
|
||||
});
|
||||
childrenMap.forEach((list) => list.sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds, sessionAttentionStates)));
|
||||
childrenMap.forEach((list) => list.sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds)));
|
||||
|
||||
// Build worktree lookup map
|
||||
const worktreeByPath = new Map<string, WorktreeMetadata>();
|
||||
@@ -1564,7 +1587,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
|
||||
return groups;
|
||||
},
|
||||
[homeDirectory, worktreeMetadata, pinnedSessionIds, gitDirectories, sessionAttentionStates]
|
||||
[homeDirectory, worktreeMetadata, pinnedSessionIds, gitDirectories]
|
||||
);
|
||||
|
||||
const toggleGroupSessionLimit = React.useCallback((groupId: string) => {
|
||||
@@ -2406,9 +2429,12 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const newFolder = createFolder(sessionDirectory, 'New folder');
|
||||
addSessionToFolder(sessionDirectory, newFolder.id, session.id);
|
||||
}}
|
||||
const newFolder = createFolderAndStartRename(sessionDirectory);
|
||||
if (!newFolder) {
|
||||
return;
|
||||
}
|
||||
addSessionToFolder(sessionDirectory, newFolder.id, session.id);
|
||||
}}
|
||||
>
|
||||
<RiAddLine className="mr-1 h-4 w-4" />
|
||||
New folder...
|
||||
@@ -2480,7 +2506,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
getSessionFolderId,
|
||||
addSessionToFolder,
|
||||
removeSessionFromFolder,
|
||||
createFolder,
|
||||
createFolderAndStartRename,
|
||||
notifyOnSubtasks,
|
||||
foldersMap, // trigger re-render when folder data changes (getFoldersForScope is a stable fn selector)
|
||||
],
|
||||
@@ -2504,7 +2530,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const nodes = folder.sessionIds
|
||||
.map((sid) => group.sessions.find((node) => node.session.id === sid))
|
||||
.filter((n): n is SessionNode => Boolean(n))
|
||||
.sort((a, b) => compareSessionsByPinnedAndTime(a.session, b.session, pinnedSessionIds, sessionAttentionStates));
|
||||
.sort((a, b) => compareSessionsByPinnedAndTime(a.session, b.session, pinnedSessionIds));
|
||||
return { folder, nodes };
|
||||
});
|
||||
|
||||
@@ -2561,6 +2587,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (!folderScopeKey) return;
|
||||
if (!showDeletionDialog) {
|
||||
deleteFolder(folderScopeKey, folder.id);
|
||||
return;
|
||||
}
|
||||
// Count affected sub-folders and sessions for the confirm dialog
|
||||
const subFolderCount = allFoldersForGroup.filter(({ folder: f }) => f.parentId === folder.id).length;
|
||||
const sessionCount = nodes.length;
|
||||
@@ -2604,10 +2634,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}
|
||||
openNewSessionDraft({ directoryOverride: group.directory, targetFolderId: folder.id });
|
||||
}}
|
||||
onNewSubFolder={depth === 0 ? () => {
|
||||
if (!folderScopeKey) return;
|
||||
createFolder(folderScopeKey, 'New folder', folder.id);
|
||||
} : undefined}
|
||||
onNewSubFolder={depth === 0 ? () => {
|
||||
if (!folderScopeKey) return;
|
||||
createFolderAndStartRename(folderScopeKey, folder.id);
|
||||
} : undefined}
|
||||
/>
|
||||
)}
|
||||
</DroppableFolderWrapper>
|
||||
@@ -2750,27 +2780,12 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label={`New session or folder in ${group.label}`}
|
||||
>
|
||||
<RiAddLine className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={4}>
|
||||
<p>New session or folder</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="min-w-[160px]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (projectId && projectId !== activeProjectId) {
|
||||
setActiveProject(projectId);
|
||||
}
|
||||
@@ -2780,22 +2795,16 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}
|
||||
openNewSessionDraft({ directoryOverride: group.directory });
|
||||
}}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label={`New session in ${group.label}`}
|
||||
>
|
||||
<RiAddLine className="mr-1.5 h-4 w-4" />
|
||||
New session
|
||||
</DropdownMenuItem>
|
||||
{folderScopeKey ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
createFolder(folderScopeKey, 'New folder');
|
||||
}}
|
||||
>
|
||||
<RiFolderAddLine className="mr-1.5 h-4 w-4" />
|
||||
New folder
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<RiAddLine className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={4}>
|
||||
<p>New session</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -2856,14 +2865,14 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
getFoldersForScope,
|
||||
collapsedFolderIds,
|
||||
toggleFolderCollapse,
|
||||
createFolder,
|
||||
createFolderAndStartRename,
|
||||
renameFolder,
|
||||
deleteFolder,
|
||||
showDeletionDialog,
|
||||
addSessionToFolder,
|
||||
renamingFolderId,
|
||||
renameFolderDraft,
|
||||
pinnedSessionIds,
|
||||
sessionAttentionStates,
|
||||
foldersMap, // trigger re-render when folder data changes (getFoldersForScope is a stable fn selector)
|
||||
]
|
||||
);
|
||||
@@ -3396,21 +3405,32 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
: `"${deleteSessionConfirm?.session.title || 'Untitled Session'}" will be permanently deleted.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="w-full sm:items-center sm:justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteSessionConfirm(null)}
|
||||
className="inline-flex h-8 items-center justify-center rounded-md border border-border px-3 typography-ui-label text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
onClick={() => setShowDeletionDialog(!showDeletionDialog)}
|
||||
className="inline-flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
|
||||
aria-pressed={!showDeletionDialog}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void confirmDeleteSession()}
|
||||
className="inline-flex h-8 items-center justify-center rounded-md bg-destructive px-3 typography-ui-label text-destructive-foreground hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/50"
|
||||
>
|
||||
Delete
|
||||
{!showDeletionDialog ? <RiCheckboxLine className="h-4 w-4 text-primary" /> : <RiCheckboxBlankLine className="h-4 w-4" />}
|
||||
Never ask
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteSessionConfirm(null)}
|
||||
className="inline-flex h-8 items-center justify-center rounded-md border border-border px-3 typography-ui-label text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void confirmDeleteSession()}
|
||||
className="inline-flex h-8 items-center justify-center rounded-md bg-destructive px-3 typography-ui-label text-destructive-foreground hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/50"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user