feat(ui): expose desktop session actions in header
Add a dedicated desktop header menu for the active session while keeping recent-session switching available when the sidebar is closed. Match inline rename behavior with the sidebar and expose rename, copy ID, share, export, archive, and delete actions with localized feedback. Automatically copy newly created share links, keep share/unshare state synchronized across live and global stores, and normalize stale upstream unshare responses so the UI immediately reflects successful unsharing. Require Markdown exports to load every available message page before formatting the conversation. Abort incomplete root exports, retain explicit child-session skip warnings, and guard complete-history pagination against failures and cursor cycles.
This commit is contained in:
@@ -831,6 +831,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
handleCancelEdit,
|
||||
handleShareSession,
|
||||
handleCopyShareUrl,
|
||||
handleCopySessionId,
|
||||
handleUnshareSession,
|
||||
handleDeleteSession,
|
||||
confirmDeleteSession,
|
||||
@@ -907,6 +908,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
const stableHandleCancelEdit = useStableRenderCallback(handleCancelEdit);
|
||||
const stableHandleShareSession = useStableRenderCallback(handleShareSession);
|
||||
const stableHandleCopyShareUrl = useStableRenderCallback(handleCopyShareUrl);
|
||||
const stableHandleCopySessionId = useStableRenderCallback(handleCopySessionId);
|
||||
const stableHandleUnshareSession = useStableRenderCallback(handleUnshareSession);
|
||||
const stableHandleDeleteSession = useStableRenderCallback(handleDeleteSession);
|
||||
const stableCreateFolderAndStartRename = useStableRenderCallback(createFolderAndStartRename);
|
||||
@@ -1560,6 +1562,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
||||
handleShareSession={stableHandleShareSession}
|
||||
copiedSessionId={copiedSessionId}
|
||||
handleCopyShareUrl={stableHandleCopyShareUrl}
|
||||
handleCopySessionId={stableHandleCopySessionId}
|
||||
handleUnshareSession={stableHandleUnshareSession}
|
||||
openSidebarMenuKey={openSidebarMenuKey}
|
||||
setOpenSidebarMenuKey={setOpenSidebarMenuKey}
|
||||
|
||||
@@ -80,6 +80,7 @@ type Props = {
|
||||
handleShareSession: (session: Session) => void;
|
||||
copiedSessionId: string | null;
|
||||
handleCopyShareUrl: (url: string, sessionId: string) => void;
|
||||
handleCopySessionId: (sessionId: string) => void;
|
||||
handleUnshareSession: (sessionId: string) => void;
|
||||
openSidebarMenuKey: string | null;
|
||||
setOpenSidebarMenuKey: (key: string | null) => void;
|
||||
@@ -274,6 +275,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
handleShareSession,
|
||||
copiedSessionId,
|
||||
handleCopyShareUrl,
|
||||
handleCopySessionId,
|
||||
handleUnshareSession,
|
||||
openSidebarMenuKey,
|
||||
setOpenSidebarMenuKey,
|
||||
@@ -478,7 +480,8 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
let skipped = 0;
|
||||
for (const child of children) {
|
||||
try {
|
||||
await sync.ensureSessionRenderable(child.session.id, false, sessionDirectory ?? undefined);
|
||||
if (!sessionDirectory) throw new Error('Session directory is required for export');
|
||||
await sync.loadCompleteHistory(child.session.id, sessionDirectory);
|
||||
const childRecords = buildSessionMessageRecordsSnapshot(directoryStore.getState(), child.session.id).list;
|
||||
const childTitle = child.session.title || t('sessions.sidebar.session.export.untitledSubagent');
|
||||
const childAgent = (child.session as Session & { agent?: string }).agent;
|
||||
@@ -510,7 +513,12 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
await sync.ensureSessionRenderable(session.id, false, sessionDirectory);
|
||||
try {
|
||||
await sync.loadCompleteHistory(session.id, sessionDirectory);
|
||||
} catch {
|
||||
toast.error(t('sessions.sidebar.session.export.failedLoadHistory'));
|
||||
return;
|
||||
}
|
||||
|
||||
const records = buildSessionMessageRecordsSnapshot(directoryStore.getState(), session.id).list;
|
||||
if (records.length === 0) {
|
||||
@@ -900,6 +908,10 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
<Icon name="pencil-ai" className="mr-1 h-4 w-4" />
|
||||
{t('sessions.sidebar.session.menu.rename')}
|
||||
</Item>
|
||||
<Item onClick={() => handleCopySessionId(session.id)} className="[&>svg]:mr-1">
|
||||
<Icon name="file-copy" className="mr-1 h-4 w-4" />
|
||||
{t('sessions.sidebar.session.menu.copyId')}
|
||||
</Item>
|
||||
<Item onClick={() => sessionDirectory && togglePinnedSession({ directory: sessionDirectory, sessionId: session.id })} className="[&>svg]:mr-1">
|
||||
{isPinnedSession ? <Icon name="unpin" className="mr-1 h-4 w-4" /> : <Icon name="pushpin" className="mr-1 h-4 w-4" />}
|
||||
{isPinnedSession ? t('sessions.sidebar.session.menu.unpin') : t('sessions.sidebar.session.menu.pin')}
|
||||
@@ -1585,6 +1597,7 @@ const areSessionNodeItemPropsEqual = (prev: Props, next: Props): boolean => {
|
||||
&& prev.togglePinnedSession === next.togglePinnedSession
|
||||
&& prev.handleShareSession === next.handleShareSession
|
||||
&& prev.handleCopyShareUrl === next.handleCopyShareUrl
|
||||
&& prev.handleCopySessionId === next.handleCopySessionId
|
||||
&& prev.handleUnshareSession === next.handleUnshareSession
|
||||
&& prev.setOpenSidebarMenuKey === next.setOpenSidebarMenuKey
|
||||
&& prev.getFoldersForScope === next.getFoldersForScope
|
||||
|
||||
@@ -117,36 +117,52 @@ export const useSessionActions = (args: Args) => {
|
||||
args.setEditTitle('');
|
||||
}, [args]);
|
||||
|
||||
const copyShareUrl = React.useCallback(async (url: string, sessionId: string): Promise<boolean> => {
|
||||
try {
|
||||
const result = await copyTextToClipboard(url);
|
||||
if (!result.ok) return false;
|
||||
setCopiedSessionId(sessionId);
|
||||
if (copyTimeout.current) clearTimeout(copyTimeout.current);
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
setCopiedSessionId(null);
|
||||
copyTimeout.current = null;
|
||||
}, 2000);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleShareSession = React.useCallback(async (session: Session) => {
|
||||
const result = await args.shareSession(session.id);
|
||||
if (result && result.share?.url) {
|
||||
toast.success(t('sessions.sidebar.session.share.successTitle'), {
|
||||
description: t('sessions.sidebar.session.share.successDescription'),
|
||||
});
|
||||
} else {
|
||||
if (!result?.share?.url) {
|
||||
toast.error(t('sessions.sidebar.session.share.error'));
|
||||
return;
|
||||
}
|
||||
}, [args, t]);
|
||||
const copied = await copyShareUrl(result.share.url, session.id);
|
||||
toast[copied ? 'success' : 'warning'](t('sessions.sidebar.session.share.successTitle'), {
|
||||
description: t(copied
|
||||
? 'sessions.sidebar.session.share.successDescription'
|
||||
: 'sessions.sidebar.session.share.copyUrlError'),
|
||||
});
|
||||
}, [args, copyShareUrl, t]);
|
||||
|
||||
const handleCopyShareUrl = React.useCallback((url: string, sessionId: string) => {
|
||||
void copyTextToClipboard(url)
|
||||
void copyShareUrl(url, sessionId).then((copied) => {
|
||||
if (!copied) toast.error(t('sessions.sidebar.session.share.copyUrlError'));
|
||||
});
|
||||
}, [copyShareUrl, t]);
|
||||
|
||||
const handleCopySessionId = React.useCallback((sessionId: string) => {
|
||||
void copyTextToClipboard(sessionId)
|
||||
.then((result) => {
|
||||
if (!result.ok) {
|
||||
toast.error(t('sessions.sidebar.session.share.copyUrlError'));
|
||||
if (result.ok) {
|
||||
toast.success(t('sessions.sidebar.session.copyId.success'));
|
||||
return;
|
||||
}
|
||||
setCopiedSessionId(sessionId);
|
||||
if (copyTimeout.current) {
|
||||
clearTimeout(copyTimeout.current);
|
||||
}
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
setCopiedSessionId(null);
|
||||
copyTimeout.current = null;
|
||||
}, 2000);
|
||||
toast.error(t('sessions.sidebar.session.copyId.error'));
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(t('sessions.sidebar.session.share.copyUrlError'));
|
||||
});
|
||||
.catch(() => toast.error(t('sessions.sidebar.session.copyId.error')));
|
||||
}, [t]);
|
||||
|
||||
const handleUnshareSession = React.useCallback(async (sessionId: string) => {
|
||||
@@ -278,6 +294,7 @@ export const useSessionActions = (args: Args) => {
|
||||
handleCancelEdit,
|
||||
handleShareSession,
|
||||
handleCopyShareUrl,
|
||||
handleCopySessionId,
|
||||
handleUnshareSession,
|
||||
handleDeleteSession,
|
||||
confirmDeleteSession,
|
||||
|
||||
Reference in New Issue
Block a user