From a7e68b166bfe4e10ba5466fbb4441f0c08f8b812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zge=20G=C3=BCner?= Date: Thu, 13 Aug 2026 11:56:33 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20make=20saved=20search-config=20sess?= =?UTF-8?q?ion=20chips=20toggle=20(select=20=E2=86=94=20deselect)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a session chip now toggles it. Clicking the active session again deselects it: its loaded search configs are removed and the results overview is cleared (zero enabled configs → the batch runner already clears + repaints). Previously a chip could only be applied, never un-applied, so there was no way to clear a session's configs without deleting them one by one. - toggleSearchConfigSession(): apply if not active, deselect if active. - deselectSearchConfigSession(): drop the session's configs + results, activeSessionId = null, re-render, re-run batch (clears display). - Active chip shows a ✕ badge + "click to deselect" tooltip; inactive chips keep the config-count badge + "click to apply" tooltip. - CSS: active ✕ badge bolded and brightens on hover (deselect cue). tsc 0 · 663 tests green · bundle built. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/renderer.ts | 36 ++++++++++++++++++++++++++++++++---- src/renderer/styles.css | 6 ++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index c9b420e..16c9a67 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -14500,10 +14500,14 @@ function renderSearchConfigSessionsUI(): void { fragment.appendChild(saveBtn); for (const session of searchConfigSessions) { + const isActive = session.id === activeSessionId; 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'; @@ -14511,12 +14515,13 @@ function renderSearchConfigSessionsUI(): void { 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); @@ -14563,6 +14568,29 @@ async function saveCurrentAsSearchConfigSession(): Promise { } } +// Clicking a session chip toggles it: apply if it isn't the active one, deselect if it is. +async function toggleSearchConfigSession(sessionId: string): Promise { + if (activeSessionId === sessionId) { + await deselectSearchConfigSession(); + } else { + await applySearchConfigSession(sessionId); + } +} + +// Deselect the active session: remove its loaded search configs and clear their results. +async function deselectSearchConfigSession(): Promise { + for (const config of [...state.searchConfigs]) { + await window.api.searchConfigDelete(config.id); + } + state.searchConfigs = []; + state.searchConfigResults.clear(); + activeSessionId = null; + renderSearchConfigsChips(); + renderSearchConfigSessionsUI(); + // Zero enabled configs → runSearchConfigsBatchOnce clears the results overview + repaints. + await runSearchConfigsBatch(true); +} + async function applySearchConfigSession(sessionId: string): Promise { const session = searchConfigSessions.find(s => s.id === sessionId); if (!session) return; diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 3e8ad89..d50bc79 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -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) ─── */ From d3a334088e9f704a2cd44aa2452d5987f8fd079a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zge=20G=C3=BCner?= Date: Thu, 13 Aug 2026 12:15:33 +0200 Subject: [PATCH 2/2] feat: make search-config sessions multi-select (union) with per-session deselect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the session toggle: sessions are no longer mutually exclusive. Selecting a session now APPENDS its search configs to the working set instead of replacing everything, so several sessions can be active at once (their patterns union). Clicking an active chip deselects just that session — removing only the configs it contributed, leaving the other sessions' (and any manual) configs untouched. - activeSessionId (single) → activeSessionIds Set + sessionConfigIds Map (sessionId → the config ids it added) for precise deselect. - selectSearchConfigSession() appends; deselectSearchConfigSession(id) removes that session's configs; toggle picks by membership. - Saving current configs as a session marks it active + attributes the live configs to it (deselectable like the rest). - Deleting an active session pulls its configs out of the working set. - loadSearchConfigs() resets the selection tracking (it's scoped to a file's working set; configs reload per-file). tsc 0 · 663 tests green · bundle built. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/renderer.ts | 82 +++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 16c9a67..0428683 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -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(); +const sessionConfigIds = new Map(); // Terminal - tabbed multi-session interface TerminalTab { @@ -13778,6 +13783,10 @@ async function loadSearchConfigs(): Promise { } 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 @@ -14500,7 +14509,7 @@ function renderSearchConfigSessionsUI(): void { fragment.appendChild(saveBtn); for (const session of searchConfigSessions) { - const isActive = session.id === activeSessionId; + const isActive = activeSessionIds.has(session.id); const chip = document.createElement('span'); chip.className = `sc-session-chip${isActive ? ' active' : ''}`; chip.dataset.id = session.id; @@ -14563,62 +14572,66 @@ async function saveCurrentAsSearchConfigSession(): Promise { 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(); } } -// Clicking a session chip toggles it: apply if it isn't the active one, deselect if it is. +// 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 { - if (activeSessionId === sessionId) { - await deselectSearchConfigSession(); + if (activeSessionIds.has(sessionId)) { + await deselectSearchConfigSession(sessionId); } else { - await applySearchConfigSession(sessionId); + await selectSearchConfigSession(sessionId); } } -// Deselect the active session: remove its loaded search configs and clear their results. -async function deselectSearchConfigSession(): Promise { - for (const config of [...state.searchConfigs]) { - await window.api.searchConfigDelete(config.id); - } - state.searchConfigs = []; - state.searchConfigResults.clear(); - activeSessionId = null; - renderSearchConfigsChips(); - renderSearchConfigSessionsUI(); - // Zero enabled configs → runSearchConfigsBatchOnce clears the results overview + repaints. - await runSearchConfigsBatch(true); -} - -async function applySearchConfigSession(sessionId: string): Promise { +// Select a session: append its configs to the current working set (does NOT clear others). +async function selectSearchConfigSession(sessionId: string): Promise { + 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 { + 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()); @@ -14651,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(); } });