feat: show ACP sessions optionally in desktop session view (#11594)
This commit is contained in:
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getAcpClient } from '../acpConnection';
|
||||
import {
|
||||
acpGetSessionListItem,
|
||||
acpListSessions,
|
||||
acpLoadSession,
|
||||
acpNewSession,
|
||||
sessionInfoToSession,
|
||||
@@ -44,6 +45,29 @@ describe('ACP sessions', () => {
|
||||
expect(session.name).toBe('');
|
||||
});
|
||||
|
||||
it('only requests acp session types when explicitly included', async () => {
|
||||
const client = {
|
||||
connection: {
|
||||
agent: {
|
||||
request: vi.fn().mockResolvedValue({ sessions: [] }),
|
||||
},
|
||||
},
|
||||
};
|
||||
vi.mocked(getAcpClient).mockResolvedValue(
|
||||
client as unknown as Awaited<ReturnType<typeof getAcpClient>>
|
||||
);
|
||||
|
||||
await acpListSessions();
|
||||
expect(client.connection.agent.request).toHaveBeenLastCalledWith(methods.agent.session.list, {
|
||||
_meta: { types: ['user', 'scheduled'] },
|
||||
});
|
||||
|
||||
await acpListSessions(undefined, { includeAcp: true });
|
||||
expect(client.connection.agent.request).toHaveBeenLastCalledWith(methods.agent.session.list, {
|
||||
_meta: { types: ['user', 'scheduled', 'acp'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns session info refreshed after loading the ACP session', async () => {
|
||||
const loadedSessionInfo = sessionInfo({
|
||||
_meta: {
|
||||
|
||||
@@ -135,21 +135,25 @@ function sessionInfoToListItem(s: SessionInfo): SessionListItem {
|
||||
|
||||
export interface SessionListFilter {
|
||||
keyword?: string;
|
||||
includeAcp: boolean;
|
||||
}
|
||||
|
||||
const SESSION_LIST_TYPES = ['user', 'scheduled'] as const;
|
||||
const SESSION_LIST_TYPES_WITH_ACP = [...SESSION_LIST_TYPES, 'acp'] as const;
|
||||
|
||||
export async function acpListSessions(
|
||||
cursor?: string | null,
|
||||
filter?: SessionListFilter
|
||||
filter: SessionListFilter = { includeAcp: false }
|
||||
): Promise<SessionListPage> {
|
||||
const client = await getAcpClient();
|
||||
const request: ListSessionsRequest = {};
|
||||
if (cursor) {
|
||||
request.cursor = cursor;
|
||||
}
|
||||
const meta: Record<string, unknown> = { types: SESSION_LIST_TYPES };
|
||||
const keyword = filter?.keyword?.trim();
|
||||
const meta: Record<string, unknown> = {
|
||||
types: filter.includeAcp ? SESSION_LIST_TYPES_WITH_ACP : SESSION_LIST_TYPES,
|
||||
};
|
||||
const keyword = filter.keyword?.trim();
|
||||
if (keyword) {
|
||||
meta.query = keyword;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Switch } from '../ui/switch';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||
import { SearchView } from '../conversation/SearchView';
|
||||
@@ -174,8 +175,15 @@ const i18n = defineMessages({
|
||||
id: 'sessions.scheduledJobsCount',
|
||||
defaultMessage: '{count} {count, plural, one {job} other {jobs}}',
|
||||
},
|
||||
includeAcpSessions: {
|
||||
id: 'sessions.includeAcp',
|
||||
defaultMessage: 'Include ACP sessions',
|
||||
},
|
||||
acpBadge: { id: 'sessions.acpBadge', defaultMessage: 'ACP' },
|
||||
});
|
||||
|
||||
const INCLUDE_ACP_SESSIONS_KEY = 'sessions_include_acp';
|
||||
|
||||
interface EditSessionModalProps {
|
||||
session: SessionListItem | null;
|
||||
isOpen: boolean;
|
||||
@@ -343,6 +351,12 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
const [sharingSessionId, setSharingSessionId] = useState<string | null>(null);
|
||||
const [nostrEnabled, setNostrEnabled] = useState(true);
|
||||
|
||||
const [includeAcpSessions, setIncludeAcpSessions] = useState(
|
||||
() => localStorage.getItem(INCLUDE_ACP_SESSIONS_KEY) === 'true'
|
||||
);
|
||||
const includeAcpSessionsRef = useRef(includeAcpSessions);
|
||||
includeAcpSessionsRef.current = includeAcpSessions;
|
||||
|
||||
// Search state for debouncing
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 300); // 300ms debounce
|
||||
@@ -384,13 +398,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
}, [debouncedSearchTerm, memoizedAllDateGroups.length]);
|
||||
|
||||
const loadRemainingSessionPages = useCallback(
|
||||
async (initialCursor: string, loadId: number, keyword?: string) => {
|
||||
async (initialCursor: string, loadId: number, keyword: string, includeAcp: boolean) => {
|
||||
let cursor: string | null = initialCursor;
|
||||
setIsPrefetchingSessions(true);
|
||||
|
||||
try {
|
||||
while (cursor && loadGenerationRef.current === loadId) {
|
||||
const resp = await acpListSessions(cursor, { keyword });
|
||||
const resp = await acpListSessions(cursor, { keyword, includeAcp });
|
||||
if (loadGenerationRef.current !== loadId) return;
|
||||
|
||||
cursor = resp.nextCursor;
|
||||
@@ -413,7 +427,10 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
);
|
||||
|
||||
const loadSessions = useCallback(
|
||||
async (keyword: string = debouncedSearchTermRef.current) => {
|
||||
async (
|
||||
keyword: string = debouncedSearchTermRef.current,
|
||||
includeAcp: boolean = includeAcpSessionsRef.current
|
||||
) => {
|
||||
const loadId = loadGenerationRef.current + 1;
|
||||
loadGenerationRef.current = loadId;
|
||||
// Only show the skeleton on the first load; subsequent loads (e.g. typing a
|
||||
@@ -427,7 +444,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
setShowContent(false);
|
||||
}
|
||||
try {
|
||||
const resp = await acpListSessions(undefined, { keyword });
|
||||
const resp = await acpListSessions(undefined, { keyword, includeAcp });
|
||||
if (loadGenerationRef.current !== loadId) return;
|
||||
hasLoadedRef.current = true;
|
||||
|
||||
@@ -436,7 +453,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
});
|
||||
|
||||
if (resp.nextCursor) {
|
||||
void loadRemainingSessionPages(resp.nextCursor, loadId, keyword);
|
||||
void loadRemainingSessionPages(resp.nextCursor, loadId, keyword, includeAcp);
|
||||
}
|
||||
} catch (err) {
|
||||
if (loadGenerationRef.current !== loadId) return;
|
||||
@@ -468,12 +485,12 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions(debouncedSearchTerm);
|
||||
loadSessions(debouncedSearchTerm, includeAcpSessions);
|
||||
return () => {
|
||||
// Bump the generation so any in-flight load for the previous keyword is discarded.
|
||||
loadGenerationRef.current += 1;
|
||||
};
|
||||
}, [loadSessions, debouncedSearchTerm]);
|
||||
}, [loadSessions, debouncedSearchTerm, includeAcpSessions]);
|
||||
|
||||
// Hide Nostr sharing when explicitly disabled via env var (restricted/enterprise bundles)
|
||||
useEffect(() => {
|
||||
@@ -536,6 +553,11 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
setSearchTerm(term);
|
||||
}, []);
|
||||
|
||||
const handleIncludeAcpSessionsChange = useCallback((checked: boolean) => {
|
||||
localStorage.setItem(INCLUDE_ACP_SESSIONS_KEY, String(checked));
|
||||
setIncludeAcpSessions(checked);
|
||||
}, []);
|
||||
|
||||
// Handle modal close
|
||||
const handleModalClose = useCallback(() => {
|
||||
setShowEditModal(false);
|
||||
@@ -827,6 +849,11 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-base break-words line-clamp-2 w-full mb-1">{displayName}</h3>
|
||||
{session.sessionType === 'acp' && (
|
||||
<span className="text-xs text-text-tertiary bg-background-secondary px-2 py-0.5 rounded-full">
|
||||
{intl.formatMessage(i18n.acpBadge)}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="flex items-center text-text-secondary text-xs">
|
||||
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
@@ -1115,6 +1142,14 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
|
||||
<p className="text-sm text-text-secondary mb-4">
|
||||
{intl.formatMessage(i18n.chatHistoryDesc, { shortcut: getSearchShortcutText() })}
|
||||
</p>
|
||||
<label className="flex items-center gap-2 text-sm text-text-secondary mb-4 cursor-pointer w-fit">
|
||||
<Switch
|
||||
variant="mono"
|
||||
checked={includeAcpSessions}
|
||||
onCheckedChange={handleIncludeAcpSessionsChange}
|
||||
/>
|
||||
{intl.formatMessage(i18n.includeAcpSessions)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3783,6 +3783,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Streaming"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Sitzung löschen"
|
||||
},
|
||||
@@ -3861,6 +3864,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Wird importiert..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "ACP-Sitzungen einschließen"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Weitere Sitzungen werden geladen..."
|
||||
},
|
||||
|
||||
@@ -3830,6 +3830,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Streaming"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Delete session"
|
||||
},
|
||||
@@ -3908,6 +3911,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Importing..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Include ACP sessions"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Loading more sessions..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Streaming"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Eliminar sesión"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Importando..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Incluir sesiones ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Cargando más sesiones..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Diffusion en cours"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Supprimer la session"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Importation..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Inclure les sessions ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Chargement de sessions supplémentaires..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "स्ट्रीमिंग"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "सत्र हटाएँ"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "आयात किया जा रहा है..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "ACP सत्र शामिल करें"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "अधिक सत्र लोड हो रहे हैं..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Streaming"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Hapus sesi"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Mengimpor..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Sertakan sesi ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Memuat lebih banyak sesi..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Streaming"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Elimina sessione"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Importazione in corso..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Includi sessioni ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Caricamento di altre sessioni..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "ストリーミング中"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "セッションを削除"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "インポート中..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "ACP セッションを含める"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "さらにセッションを読み込み中..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "스트리밍"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "세션 삭제"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "가져오는 중..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "ACP 세션 포함"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "추가 세션 로드 중..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Penstriman"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Padam sesi"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Mengimport..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Sertakan sesi ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Memuatkan lebih banyak sesi..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Em transmissão"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Eliminar sessão"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "A importar..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Incluir sessões ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "A carregar mais sessões..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Потоковая передача"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Удалить сеанс"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Импорт..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Включить сессии ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Загрузка дополнительных сеансов..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Akış"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Oturumu sil"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "İçe aktarılıyor..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "ACP oturumlarını dahil et"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Daha fazla oturum yükleniyor..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "Đang truyền phát"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "Xóa phiên"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "Đang nhập..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "Bao gồm phiên ACP"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "Đang tải thêm phiên..."
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "流式传输中"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "删除会话"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "正在导入..."
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "包含 ACP 会话"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "正在加载更多会话…"
|
||||
},
|
||||
|
||||
@@ -3784,6 +3784,9 @@
|
||||
"sessionIndicators.streaming": {
|
||||
"defaultMessage": "串流中"
|
||||
},
|
||||
"sessions.acpBadge": {
|
||||
"defaultMessage": "ACP"
|
||||
},
|
||||
"sessions.action.delete": {
|
||||
"defaultMessage": "刪除工作階段"
|
||||
},
|
||||
@@ -3862,6 +3865,9 @@
|
||||
"sessions.importing": {
|
||||
"defaultMessage": "正在匯入…"
|
||||
},
|
||||
"sessions.includeAcp": {
|
||||
"defaultMessage": "包含 ACP 工作階段"
|
||||
},
|
||||
"sessions.loadingMore": {
|
||||
"defaultMessage": "正在載入更多工作階段…"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user