Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 61 additions & 19 deletions src/renderer/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,12 @@ let activeHighlightGroupId: string | null = null;

// Search config sessions
let searchConfigSessions: SearchConfigSessionDef[] = [];
let activeSessionId: string | null = null;
// Search-config sessions are multi-select. activeSessionIds = which sessions are currently
// applied; sessionConfigIds maps each active session → the config ids it contributed (so a
// single session can be deselected without disturbing the others). Scoped to the current
// file's working set — reset when configs reload (loadSearchConfigs).
const activeSessionIds = new Set<string>();
const sessionConfigIds = new Map<string, string[]>();

// Terminal - tabbed multi-session
interface TerminalTab {
Expand Down Expand Up @@ -13778,6 +13783,10 @@ async function loadSearchConfigs(): Promise<void> {
}
state.searchConfigResults.clear();
searchConfigScanSignatures.clear();
// Session selection is scoped to a file's working set; the configs just reloaded, so any
// prior session→config attribution is stale. Reset it (chips fall back to inactive).
activeSessionIds.clear();
sessionConfigIds.clear();
renderSearchConfigsChips();

// Auto-run batch if any enabled configs
Expand Down Expand Up @@ -14500,23 +14509,28 @@ function renderSearchConfigSessionsUI(): void {
fragment.appendChild(saveBtn);

for (const session of searchConfigSessions) {
const isActive = activeSessionIds.has(session.id);
const chip = document.createElement('span');
chip.className = `sc-session-chip${session.id === activeSessionId ? ' active' : ''}`;
chip.className = `sc-session-chip${isActive ? ' active' : ''}`;
chip.dataset.id = session.id;
chip.title = `${escapeHtml(session.name)} (${session.configs.length} configs)${session.isGlobal ? ' [Global]' : ' [Local]'}`;
const scopeTag = session.isGlobal ? ' [Global]' : ' [Local]';
chip.title = isActive
? `${escapeHtml(session.name)} — applied. Click to deselect (clear its search configs).`
: `${escapeHtml(session.name)} (${session.configs.length} configs)${scopeTag} — click to apply`;

const nameSpan = document.createElement('span');
nameSpan.className = 'sc-session-chip-name';
nameSpan.textContent = session.name;

const badge = document.createElement('span');
badge.className = 'sc-session-chip-badge';
badge.textContent = String(session.configs.length);
// Active chip shows a ✕ hint that clicking again deselects; otherwise the config count.
badge.textContent = isActive ? '✕' : String(session.configs.length);

chip.appendChild(nameSpan);
chip.appendChild(badge);

chip.addEventListener('click', () => applySearchConfigSession(session.id));
chip.addEventListener('click', () => toggleSearchConfigSession(session.id));
chip.addEventListener('contextmenu', (e) => {
e.preventDefault();
showSearchConfigSessionContextMenu(e as MouseEvent, session);
Expand Down Expand Up @@ -14558,39 +14572,66 @@ async function saveCurrentAsSearchConfigSession(): Promise<void> {
const result = await window.api.searchConfigSessionSave(session);
if (result.success) {
searchConfigSessions.push(session);
activeSessionId = session.id;
// The configs it was saved from are the ones live right now — mark it applied and
// attribute them so it can be deselected like any other active session.
activeSessionIds.add(session.id);
sessionConfigIds.set(session.id, state.searchConfigs.map(c => c.id));
renderSearchConfigSessionsUI();
}
}

async function applySearchConfigSession(sessionId: string): Promise<void> {
// Sessions are multi-select: clicking a chip toggles just that session. Selecting is
// ADDITIVE (its configs join the working set); deselecting removes only that session's
// configs. sessionConfigIds tracks which config ids each active session contributed, so
// deselect can pull exactly its own configs and leave the others (and manual ones) alone.
async function toggleSearchConfigSession(sessionId: string): Promise<void> {
if (activeSessionIds.has(sessionId)) {
await deselectSearchConfigSession(sessionId);
} else {
await selectSearchConfigSession(sessionId);
}
}

// Select a session: append its configs to the current working set (does NOT clear others).
async function selectSearchConfigSession(sessionId: string): Promise<void> {
if (activeSessionIds.has(sessionId)) return; // already applied
const session = searchConfigSessions.find(s => s.id === sessionId);
if (!session) return;

// Clear current search configs
for (const config of [...state.searchConfigs]) {
await window.api.searchConfigDelete(config.id);
}
state.searchConfigs = [];
state.searchConfigResults.clear();

// Load session's configs
const addedIds: string[] = [];
for (const config of session.configs) {
const newConfig = { ...config, id: `sc-${Date.now()}-${Math.random().toString(36).substring(2, 7)}` };
await window.api.searchConfigSave(newConfig);
state.searchConfigs.push(newConfig);
addedIds.push(newConfig.id);
}
sessionConfigIds.set(sessionId, addedIds);
activeSessionIds.add(sessionId);

activeSessionId = sessionId;
renderSearchConfigsChips();
renderSearchConfigSessionsUI();

// Run batch search
if (state.searchConfigs.some(c => c.enabled)) {
await runSearchConfigsBatch(true);
}
}

// Deselect one session: remove only the configs it contributed and re-run the batch.
async function deselectSearchConfigSession(sessionId: string): Promise<void> {
const ids = new Set(sessionConfigIds.get(sessionId) || []);
for (const id of ids) {
await window.api.searchConfigDelete(id);
state.searchConfigResults.delete(id);
}
state.searchConfigs = state.searchConfigs.filter(c => !ids.has(c.id));
sessionConfigIds.delete(sessionId);
activeSessionIds.delete(sessionId);

renderSearchConfigsChips();
renderSearchConfigSessionsUI();
// If that was the last enabled config, runSearchConfigsBatchOnce clears the overview.
await runSearchConfigsBatch(true);
}

function showSearchConfigSessionContextMenu(e: MouseEvent, session: SearchConfigSessionDef): void {
document.querySelectorAll('.sc-context-menu').forEach(el => el.remove());

Expand Down Expand Up @@ -14623,8 +14664,9 @@ function showSearchConfigSessionContextMenu(e: MouseEvent, session: SearchConfig
if (!confirm(`Delete session "${session.name}"?`)) return;
const result = await window.api.searchConfigSessionDelete(session.id, session.isGlobal);
if (result.success) {
// If it was applied, pull its configs out of the working set first.
if (activeSessionIds.has(session.id)) await deselectSearchConfigSession(session.id);
searchConfigSessions = searchConfigSessions.filter(s => s.id !== session.id);
if (activeSessionId === session.id) activeSessionId = null;
renderSearchConfigSessionsUI();
}
});
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5465,6 +5465,12 @@ kbd {
.sc-session-chip.active .sc-session-chip-badge {
background: rgba(255, 255, 255, 0.25);
color: #ffffff;
font-weight: 700;
}

/* Active chip's ✕ badge = deselect affordance; brighten it on hover. */
.sc-session-chip.active:hover .sc-session-chip-badge {
background: rgba(255, 255, 255, 0.45);
}

/* ─── Search Configs content (inside bottom panel tab) ─── */
Expand Down
Loading