feat: show ACP sessions optionally in desktop session view (#11594)

This commit is contained in:
Alex Hancock
2026-08-26 18:59:38 +00:00
committed by GitHub
parent 6dff7290e7
commit 6582ebfbb8
19 changed files with 169 additions and 10 deletions
@@ -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: {
+7 -3
View File
@@ -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>
+6
View File
@@ -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..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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": "अधिक सत्र लोड हो रहे हैं..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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": "さらにセッションを読み込み中..."
},
+6
View File
@@ -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": "추가 세션 로드 중..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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": "Загрузка дополнительных сеансов..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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..."
},
+6
View File
@@ -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": "正在加载更多会话…"
},
+6
View File
@@ -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": "正在載入更多工作階段…"
},