diff --git a/docs/PARITY_CHECKLIST.md b/docs/PARITY_CHECKLIST.md index d44b2e1..d4c9f11 100644 --- a/docs/PARITY_CHECKLIST.md +++ b/docs/PARITY_CHECKLIST.md @@ -67,6 +67,28 @@ Closed by the `feat/columns-panel` parity work (2026-08-13): | Constants / tags (save/list/delete) | πŸ”€ Save as constant + picker | `logan_constants` (`/api/constants-*`) | βœ… shared `constantsStore` | | Column Layouts (save/list/delete) | Column Layouts builder / Columns window | `logan_column_layouts` (`/api/column-layout-*`) | βœ… shared `columnLayoutsStore` | +Uniform entity `description` (2026-08-13) β€” every saveable "basic entity" gained an +optional `description?: string` so both operators can record an entity's purpose/why +(naming matches the existing `BaselineRecord.description` / `InvestigationTemplate.description`; +findings already carry `detail`). Human editing = a shared `editEntityDescription()` helper +(prefilled modal; blank clears) reached by right-click on the entity's chip/row, plus a `πŸ“` +marker + the note in the hover tooltip. Agent editing = a `description` param on the entities +that have an MCP create/save tool. ONE field name, both operators (rule 5). + +| Entity | Human sets it | AI sets it | Status | +|--------|---------------|-----------|--------| +| Constant / tag | Right-click in the constants picker | `logan_constants` `description` (`/api/constants-save`) | βœ… both | +| Bookmark | Right-click a bookmark row | `logan_add_bookmark` `description` (`/api/bookmark`, `-update`) | βœ… both | +| Highlight | Right-click a highlight row | `logan_highlight` `description` (`/api/highlight`, `-update`) | βœ… both | +| Column Layout | Right-click a layout chip | `logan_column_layouts` layout.`description` (`/api/column-layout-save`) | βœ… both | +| Search config | Chip context menu β†’ "Edit description" | β€” (no MCP create tool) | βœ… human Β· field carried for AI | +| Search config session | Chip context menu β†’ "Edit description" | β€” (no MCP create tool) | βœ… human Β· field carried for AI | +| Pattern property (trend) | Right-click a property chip | β€” (no MCP create tool) | βœ… human Β· field carried for AI | + +(Search configs / sessions / pattern-properties have no MCP *create* tool today β€” a +pre-existing human-only exemption β€” so their `description` is human-set; the field is +carried on the type so an AI tool added later inherits it for free.) + Still human-only β€” deliberate backlog (add a counterpart or a written exemption when each is next touched): diff --git a/src/main/api-server.ts b/src/main/api-server.ts index ed4b1e3..b52e238 100644 --- a/src/main/api-server.ts +++ b/src/main/api-server.ts @@ -865,7 +865,8 @@ export function startApiServer(ctx: ApiContext): void { const name = String(body.name || '').trim(); const value = String(body.value ?? ''); if (!name || !value) { sendJson(res, { success: false, error: 'name and value are required' }); return; } - saveConstant(name, value); + const description = typeof body.description === 'string' ? body.description : undefined; + saveConstant(name, value, undefined, description); sendJson(res, { success: true, entries: getConstants() }); return; } @@ -910,6 +911,7 @@ export function startApiServer(ctx: ApiContext): void { label: body.label || '', color: body.color || '#ffff00', lineText: body.lineText, + ...(typeof body.description === 'string' ? { description: body.description } : {}), }; const result = ctx.addBookmark(bookmark); sendJson(res, result); @@ -927,6 +929,7 @@ export function startApiServer(ctx: ApiContext): void { includeWhitespace: body.includeWhitespace ?? false, highlightAll: body.highlightAll ?? true, isGlobal: body.isGlobal ?? false, + ...(typeof body.description === 'string' ? { description: body.description } : {}), }; const result = ctx.addHighlight(highlight); sendJson(res, result); @@ -948,6 +951,7 @@ export function startApiServer(ctx: ApiContext): void { ...existing, label: body.label ?? existing.label, color: body.color ?? existing.color, + description: body.description !== undefined ? body.description : existing.description, }; const result = ctx.updateBookmark(updated); sendJson(res, result); @@ -976,6 +980,7 @@ export function startApiServer(ctx: ApiContext): void { pattern: body.pattern ?? existing.pattern, backgroundColor: body.backgroundColor ?? existing.backgroundColor, textColor: body.textColor !== undefined ? body.textColor : existing.textColor, + description: body.description !== undefined ? body.description : existing.description, }; const result = ctx.updateHighlight(updated); sendJson(res, result); diff --git a/src/main/columnLayoutsStore.ts b/src/main/columnLayoutsStore.ts index 94bb362..d04961a 100644 --- a/src/main/columnLayoutsStore.ts +++ b/src/main/columnLayoutsStore.ts @@ -14,6 +14,7 @@ export interface ColumnLayoutSaved { delimiterName?: string; pattern?: { regex: string; flags: string; fields: string[] }; columns: Array<{ index: number; name?: string; visible: boolean }>; + description?: string; // optional human/AI note: what this is for / why it was added } const LAYOUTS_PATH = (): string => path.join(os.homedir(), '.logan', 'column-layouts.json'); diff --git a/src/main/constantsStore.ts b/src/main/constantsStore.ts index 509b8c2..ef43cd7 100644 --- a/src/main/constantsStore.ts +++ b/src/main/constantsStore.ts @@ -13,6 +13,7 @@ export interface ConstantEntry { value: string; createdAt: string; // ISO 8601 updatedAt: string; // ISO 8601 + description?: string; // optional human/AI note: what this is for / why it was added } export interface ConstantsStore { @@ -84,7 +85,7 @@ export class ConstantsStoreImpl { } /** Upsert a named constant. Empty name/value is a no-op. Debounced write. */ - save(name: string, value: string, at: number = Date.now()): void { + save(name: string, value: string, at: number = Date.now(), description?: string): void { try { const trimmedName = (name || '').trim(); if (!trimmedName || !value) return; @@ -94,8 +95,12 @@ export class ConstantsStoreImpl { if (existing) { existing.value = value; existing.updatedAt = iso; + if (description !== undefined) existing.description = description; } else { - store.entries[trimmedName] = { name: trimmedName, value, createdAt: iso, updatedAt: iso }; + store.entries[trimmedName] = { + name: trimmedName, value, createdAt: iso, updatedAt: iso, + ...(description !== undefined ? { description } : {}), + }; } store.updatedAt = iso; this.scheduleWrite(); @@ -150,8 +155,8 @@ function store(): ConstantsStoreImpl { return instance; } -export function saveConstant(name: string, value: string, at?: number): void { - store().save(name, value, at); +export function saveConstant(name: string, value: string, at?: number, description?: string): void { + store().save(name, value, at, description); } export function getConstants(): ConstantEntry[] { diff --git a/src/main/index.ts b/src/main/index.ts index f46aa5b..7653a7e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7806,8 +7806,8 @@ ipcMain.handle(IPC.COMPILE_PATTERN, (_, input: CompileInput) => { // ── Named constants IPC ──────────────────────────────────────────────── // Captured from a selection via the log viewer's "Save as constant…" gesture. // Persistence-only this brick; a viewer/consumer brick lands later. -ipcMain.handle(IPC.CONSTANTS_SAVE, (_, name: string, value: string) => { - saveConstant(name, value); +ipcMain.handle(IPC.CONSTANTS_SAVE, (_, name: string, value: string, description?: string) => { + saveConstant(name, value, undefined, description); return { success: true }; }); diff --git a/src/mcp-server/index.ts b/src/mcp-server/index.ts index 9cdf97b..6bc1873 100644 --- a/src/mcp-server/index.ts +++ b/src/mcp-server/index.ts @@ -369,13 +369,14 @@ server.tool( action: z.enum(['list', 'save', 'delete']).describe('list all Β· save one Β· delete one'), name: z.string().optional().describe('constant name (required for save/delete)'), value: z.string().optional().describe('constant value (required for save)'), + description: z.string().optional().describe('optional note on what this constant is for / why (shown to the human too)'), }, - async ({ action, name, value }) => { + async ({ action, name, value, description }) => { try { const path = action === 'save' ? '/api/constants-save' : action === 'delete' ? '/api/constants-delete' : '/api/constants-list'; - const result = await apiCall('POST', path, { name, value }); + const result = await apiCall('POST', path, { name, value, description }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (err: any) { return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }; @@ -389,7 +390,7 @@ server.tool( 'Manage saved Column Layouts (named column definitions β€” delimiter OR regex/paint pattern + per-column name/visibility) β€” the same store the human Column Layouts builder / Columns window uses. list Β· save Β· delete. (Applying a layout to the human viewer is human-only.)', { action: z.enum(['list', 'save', 'delete']).describe('list all Β· save one Β· delete one'), - layout: z.any().optional().describe('layout object for save: {id, name, method:"delimiter"|"pattern", delimiter?/pattern?, columns:[{index,name?,visible}]}'), + layout: z.any().optional().describe('layout object for save: {id, name, method:"delimiter"|"pattern", delimiter?/pattern?, columns:[{index,name?,visible}], description?}. Include an optional `description` note on what the layout is for.'), id: z.string().optional().describe('layout id (required for delete)'), }, async ({ action, layout, id }) => { @@ -429,10 +430,11 @@ server.tool( lineNumber: z.number().int().min(0).describe('0-based line number to bookmark'), label: z.string().default('').describe('Optional label/note for the bookmark'), color: z.string().default('#ffff00').describe('Bookmark color (hex)'), + description: z.string().optional().describe('optional note on why this line matters / what to check (shown to the human too)'), }, - async ({ lineNumber, label, color }) => { + async ({ lineNumber, label, color, description }) => { try { - const result = await apiCall('POST', '/api/bookmark', { lineNumber, label, color }); + const result = await apiCall('POST', '/api/bookmark', { lineNumber, label, color, description }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (err: any) { return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }; @@ -468,10 +470,11 @@ server.tool( matchCase: z.boolean().default(false).describe('Case-sensitive matching'), backgroundColor: z.string().default('#ffff00').describe('Highlight background color (hex)'), isGlobal: z.boolean().default(false).describe('Apply to all files (true) or current file only (false)'), + description: z.string().optional().describe('optional note on what this highlight is for / why (shown to the human too)'), }, - async ({ pattern, isRegex, matchCase, backgroundColor, isGlobal }) => { + async ({ pattern, isRegex, matchCase, backgroundColor, isGlobal, description }) => { try { - const result = await apiCall('POST', '/api/highlight', { pattern, isRegex, matchCase, backgroundColor, isGlobal }); + const result = await apiCall('POST', '/api/highlight', { pattern, isRegex, matchCase, backgroundColor, isGlobal, description }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (err: any) { return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }; diff --git a/src/preload/index.ts b/src/preload/index.ts index 3f71fe1..5c7c8d3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -971,8 +971,8 @@ const api = { ipcRenderer.invoke(IPC.COMPILE_PATTERN, input), // Named constants (captured from a selection via "Save as constant…") - saveConstant: (name: string, value: string): Promise<{ success: boolean; error?: string }> => - ipcRenderer.invoke(IPC.CONSTANTS_SAVE, name, value), + saveConstant: (name: string, value: string, description?: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke(IPC.CONSTANTS_SAVE, name, value, description), getConstants: (): Promise<{ success: boolean; entries?: any[] }> => ipcRenderer.invoke(IPC.CONSTANTS_GET), deleteConstant: (name: string): Promise<{ success: boolean; removed?: boolean }> => diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 25552f0..4e0b708 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -7305,11 +7305,20 @@ function renderTrendPropChips(): void { for (const prop of trendPatternProperties) { const chip = document.createElement('span'); chip.className = 'trend-prop-chip'; - chip.title = `/${prop.pattern}/${prop.patternFlags || ''} β€” click to chart with the selected cell type`; + chip.title = `/${prop.pattern}/${prop.patternFlags || ''} β€” click to chart with the selected cell type` + descTitleSuffix(prop.description); + // Right-click β†’ add/edit its description (purpose/why). + chip.addEventListener('contextmenu', (e) => { + e.preventDefault(); + void editEntityDescription(prop.description, async (d) => { + prop.description = d || undefined; + await window.api.patternPropSave(prop); + renderTrendPropChips(); + }); + }); const name = document.createElement('button'); name.className = 'trend-prop-name'; - name.textContent = prop.name; + name.textContent = prop.description ? `πŸ“ ${prop.name}` : prop.name; name.addEventListener('click', () => applyTrendProperty(prop)); const del = document.createElement('button'); @@ -12521,7 +12530,8 @@ function renderSearchConfigsChips(): void { const patternText = document.createElement('span'); patternText.className = 'sc-chip-pattern'; patternText.textContent = config.pattern; - patternText.title = config.pattern; + patternText.title = config.pattern + descTitleSuffix(config.description); + if (config.description) patternText.textContent = `πŸ“ ${config.pattern}`; const count = document.createElement('span'); count.className = 'sc-chip-count'; @@ -13775,6 +13785,18 @@ function showSearchConfigContextMenu(e: MouseEvent, config: SearchConfigDef): vo showSearchConfigForm(config.id); }); + const descItem = document.createElement('button'); + descItem.className = 'sc-context-menu-item'; + descItem.textContent = config.description ? 'Edit description…' : 'Add description…'; + descItem.addEventListener('click', () => { + menu.remove(); + void editEntityDescription(config.description, async (d) => { + config.description = d || undefined; + await window.api.searchConfigSave(config); + renderSearchConfigsChips(); + }); + }); + const exportItem = document.createElement('button'); exportItem.className = 'sc-context-menu-item'; exportItem.textContent = 'Export Results'; @@ -13798,6 +13820,7 @@ function showSearchConfigContextMenu(e: MouseEvent, config: SearchConfigDef): vo }); menu.appendChild(editItem); + menu.appendChild(descItem); menu.appendChild(exportItem); menu.appendChild(deleteItem); document.body.appendChild(menu); @@ -14552,13 +14575,14 @@ function renderSearchConfigSessionsUI(): void { chip.className = `sc-session-chip${isActive ? ' active' : ''}`; chip.dataset.id = session.id; const scopeTag = session.isGlobal ? ' [Global]' : ' [Local]'; - chip.title = isActive + 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`; + : `${escapeHtml(session.name)} (${session.configs.length} configs)${scopeTag} β€” click to apply`) + + descTitleSuffix(session.description); const nameSpan = document.createElement('span'); nameSpan.className = 'sc-session-chip-name'; - nameSpan.textContent = session.name; + nameSpan.textContent = session.description ? `πŸ“ ${session.name}` : session.name; const badge = document.createElement('span'); badge.className = 'sc-session-chip-badge'; @@ -14709,6 +14733,18 @@ function showSearchConfigSessionContextMenu(e: MouseEvent, session: SearchConfig } }); + const descItem = document.createElement('button'); + descItem.className = 'sc-context-menu-item'; + descItem.textContent = session.description ? 'Edit description…' : 'Add description…'; + descItem.addEventListener('click', () => { + menu.remove(); + void editEntityDescription(session.description, async (d) => { + session.description = d || undefined; + await window.api.searchConfigSessionSave(session); + renderSearchConfigSessionsUI(); + }); + }); + // Promote/demote scope so an existing session can be made reusable across files // (or pinned back to this file) without re-saving. const scopeItem = document.createElement('button'); @@ -14723,6 +14759,7 @@ function showSearchConfigSessionContextMenu(e: MouseEvent, session: SearchConfig }); menu.appendChild(renameItem); + menu.appendChild(descItem); menu.appendChild(scopeItem); menu.appendChild(deleteItem); document.body.appendChild(menu); @@ -16968,13 +17005,25 @@ function renderColumnLayoutChips(): void { return; } el.innerHTML = columnLayouts.map((l: any) => - `${escapeHtml(l.name)}` + `${l.description ? 'πŸ“ ' : ''}${escapeHtml(l.name)}` ).join(''); el.querySelectorAll('.patcol-chip-name').forEach((b) => b.addEventListener('click', (e) => { const id = ((e.currentTarget as HTMLElement).closest('.patcol-chip') as HTMLElement)?.dataset.id; const layout = columnLayouts.find((l: any) => l.id === id); if (layout) applyColumnLayoutToModal(layout); })); + // Right-click a layout chip β†’ add/edit its description (purpose/why). + el.querySelectorAll('.patcol-chip').forEach((chip) => chip.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const id = (chip as HTMLElement).dataset.id; + const layout = columnLayouts.find((l: any) => l.id === id); + if (!layout) return; + void editEntityDescription(layout.description, async (d) => { + layout.description = d || undefined; + await (window.api as any).columnLayoutSave(layout); + renderColumnLayoutChips(); + }); + })); el.querySelectorAll('.patcol-chip-x').forEach((b) => b.addEventListener('click', async (e) => { e.stopPropagation(); const id = (e.currentTarget as HTMLElement).dataset.id; @@ -17069,7 +17118,7 @@ function applyColumnPattern( // include list. onPick(value, name) fires on choose; each row has a Γ— to delete. async function showConstantsPickerMenu(anchor: HTMLElement, onPick: (value: string, name: string) => void): Promise { document.getElementById('constants-picker-menu')?.remove(); - let entries: Array<{ name: string; value: string }> = []; + let entries: Array<{ name: string; value: string; description?: string }> = []; try { const res = await window.api.getConstants(); entries = (res && res.entries) || []; @@ -17081,8 +17130,8 @@ async function showConstantsPickerMenu(anchor: HTMLElement, onPick: (value: stri menu.innerHTML = entries.length === 0 ? '
No constants yet β€” right-click a selection β†’ β€œπŸ”€ Save as constant”.
' : entries.map(e => - `
` + - `${escapeHtml(e.name)}` + + `
` + + `${e.description ? 'πŸ“ ' : ''}${escapeHtml(e.name)}` + `${escapeHtml(e.value.length > 40 ? e.value.slice(0, 37) + '…' : e.value)}` + `
` ).join(''); @@ -17103,6 +17152,17 @@ async function showConstantsPickerMenu(anchor: HTMLElement, onPick: (value: stri onPick(row.dataset.value || '', row.dataset.name || ''); close(); }); + // Right-click β†’ add/edit this constant's description (purpose/why). + row.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const name = row.dataset.name || ''; + const value = row.dataset.value || ''; + const current = entries.find(en => en.name === name)?.description; + close(); + void editEntityDescription(current, async (d) => { + await window.api.saveConstant(name, value, d); + }); + }); }); menu.querySelectorAll('.constant-del').forEach(btn => { btn.addEventListener('click', async (e) => { @@ -20147,7 +20207,7 @@ function updateBookmarksUI(): void { ? `` : ''; return ` -
+
${colorDot}Line ${b.lineNumber + 1} ${b.lineText ? `${escapeHtml(b.lineText.substring(0, 120))}` : ''} @@ -20184,6 +20244,19 @@ function updateBookmarksUI(): void { const id = (item as HTMLElement).dataset.id!; editBookmarkComment(id); }); + + // Right-click β†’ add/edit its description (purpose/why), distinct from the label. + item.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const id = (item as HTMLElement).dataset.id!; + const bm = state.bookmarks.find((b) => b.id === id); + if (!bm) return; + void editEntityDescription(bm.description, async (d) => { + bm.description = d || undefined; + await window.api.updateBookmark(bm); + updateBookmarksUI(); + }); + }); }); } @@ -20375,11 +20448,11 @@ function updateHighlightsUI(): void { const total = matches?.length ?? 0; const posText = total > 0 ? `${currentIdx + 1}/${total}` : '0/0'; return ` -
+
- ${escapeHtml(h.pattern)} + ${h.description ? 'πŸ“ ' : ''}${escapeHtml(h.pattern)} ${h.isGlobal ? 'G' : 'L'}
@@ -20416,6 +20489,21 @@ function updateHighlightsUI(): void { }); }); + // Right-click a highlight row β†’ add/edit its description (purpose/why). + elements.highlightsList.querySelectorAll('.highlight-item').forEach((item) => { + item.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const id = (item as HTMLElement).dataset.id!; + const hl = state.highlights.find((h) => h.id === id); + if (!hl) return; + void editEntityDescription(hl.description, async (d) => { + hl.description = d || undefined; + await window.api.updateHighlight(hl); + updateHighlightsUI(); + }); + }); + }); + // Add click handlers for toggle global elements.highlightsList.querySelectorAll('.highlight-toggle-global').forEach((btn) => { btn.addEventListener('click', async (e) => { @@ -23808,6 +23896,59 @@ async function applyHighlightGroup(groupId: string): Promise { renderVisibleLines(); } +// ── Uniform "description" editor for saveable entities ───────────────────────── +// Every saveable entity (tag/constant, search pattern, session, column layout, +// bookmark, highlight, trend property) carries an optional `description`. This modal +// PREFILLS the current text (unlike showTextInputModal's placeholder-only input) and +// lets you save an empty string to CLEAR it. Returns the new text, or null if cancelled. +function showDescriptionModal(current: string): Promise { + return new Promise((resolve) => { + const overlay = document.createElement('div'); + overlay.className = 'modal'; + overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;'; + overlay.innerHTML = ` + `; + const input = overlay.querySelector('textarea') as HTMLTextAreaElement; + input.value = current; + const close = (v: string | null) => { overlay.remove(); resolve(v); }; + (overlay.querySelector('[data-action="save"]') as HTMLButtonElement).addEventListener('click', () => close(input.value.trim())); + (overlay.querySelector('[data-action="cancel"]') as HTMLButtonElement).addEventListener('click', () => close(null)); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) close(input.value.trim()); + else if (e.key === 'Escape') close(null); + }); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(null); }); + document.body.appendChild(overlay); + input.focus(); + input.select(); + }); +} + +// Prompt for a new description and persist via `save` (which receives '' to clear). +async function editEntityDescription(current: string | undefined, save: (desc: string) => void | Promise): Promise { + const next = await showDescriptionModal(current || ''); + if (next === null) return; // cancelled + await save(next); + showToast(next ? 'Description saved' : 'Description cleared'); +} + +// Tooltip suffix: append an entity's description to a title string when present. +function descTitleSuffix(description?: string): string { + return description ? `\nπŸ“ ${description}` : ''; +} + function showTextInputModal(title: string, label: string, placeholder: string): Promise { return new Promise((resolve) => { const overlay = document.createElement('div'); diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 4ef399c..01bc504 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -123,6 +123,7 @@ interface Bookmark { color?: string; lineText?: string; createdAt: number; + description?: string; // optional human/AI note: what this is for / why it was added } interface HighlightConfig { @@ -136,6 +137,7 @@ interface HighlightConfig { includeWhitespace: boolean; highlightAll: boolean; // true = all occurrences, false = first only per line isGlobal?: boolean; // true = applies to all files, false = file-specific + description?: string; // optional human/AI note: what this is for / why it was added } interface FolderFile { @@ -204,6 +206,7 @@ interface SearchConfigDef { enabled: boolean; isGlobal: boolean; createdAt: number; + description?: string; // optional human/AI note: what this is for / why it was added } // A reusable, named pattern-property: a regex whose 1st capture group (or whole @@ -216,6 +219,7 @@ interface PatternPropertyDef { patternFlags?: string; unit?: string; createdAt: number; + description?: string; // optional human/AI note: what this is for / why it was added } // A reusable, named search/regex pattern in the Pattern Library. Written once, @@ -270,6 +274,7 @@ interface SearchConfigSessionDef { configs: SearchConfigDef[]; isGlobal: boolean; createdAt: number; + description?: string; // optional human/AI note: what this is for / why it was added } interface BaselineRecord { @@ -816,8 +821,8 @@ interface Api { compilePattern: (input: { mode: 'plain' | 'grok' | 'paint' | 'regex'; text?: string; sample?: string; spans?: Array<{ start: number; end: number; name: string }>; flags?: string; matchCase?: boolean; wholeWord?: boolean; invert?: boolean }) => Promise<{ ok: boolean; source: string; flags: string; error?: string; warnings: string[]; mode: string }>; // Named constants (captured from a selection via "Save as constant…") - saveConstant: (name: string, value: string) => Promise<{ success: boolean; error?: string }>; - getConstants: () => Promise<{ success: boolean; entries?: Array<{ name: string; value: string; createdAt: string; updatedAt: string }> }>; + saveConstant: (name: string, value: string, description?: string) => Promise<{ success: boolean; error?: string }>; + getConstants: () => Promise<{ success: boolean; entries?: Array<{ name: string; value: string; createdAt: string; updatedAt: string; description?: string }> }>; deleteConstant: (name: string) => Promise<{ success: boolean; removed?: boolean }>; // Active scope ("Use filter/search/selection as scope" + breadcrumb) diff --git a/src/shared/types.ts b/src/shared/types.ts index fbb22a1..f382b99 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -44,6 +44,7 @@ export interface Bookmark { label: string; color: string; lineText?: string; + description?: string; // optional human/AI note: what this is for / why it was added } export interface Annotation { @@ -74,6 +75,7 @@ export interface Highlight { includeWhitespace: boolean; highlightAll: boolean; // true = all occurrences, false = first only per line isGlobal?: boolean; // true = applies to all files, false = file-specific + description?: string; // optional human/AI note: what this is for / why it was added } export interface HighlightGroup { @@ -160,6 +162,7 @@ export interface SearchConfig { enabled: boolean; isGlobal: boolean; createdAt: number; + description?: string; // optional human/AI note: what this is for / why it was added } // A reusable, named pattern-property: a regex whose 1st capture group (or the @@ -171,6 +174,7 @@ export interface PatternProperty { patternFlags?: string; unit?: string; createdAt: number; + description?: string; // optional human/AI note: what this is for / why it was added } // A reusable, named search/regex pattern in the Pattern Library. Written once, @@ -308,6 +312,7 @@ export interface SearchConfigSession { configs: SearchConfig[]; isGlobal: boolean; createdAt: number; + description?: string; // optional human/AI note: what this is for / why it was added } // Live connection info returned to renderer diff --git a/src/tests/constantsStore.test.ts b/src/tests/constantsStore.test.ts index 9db82ac..2541544 100644 --- a/src/tests/constantsStore.test.ts +++ b/src/tests/constantsStore.test.ts @@ -67,6 +67,32 @@ describe('ConstantsStore', () => { }); }); + describe('description (optional human/AI note)', () => { + it('stores a description when provided', () => { + store.save('sessionId', 'abc-123', undefined, 'the auth session token seen at the crash'); + expect(store.getAll()[0].description).toBe('the auth session token seen at the crash'); + }); + + it('leaves description undefined when not provided', () => { + store.save('deviceId', 'dev-9'); + expect(store.getAll()[0].description).toBeUndefined(); + }); + + it('can set/replace the description on upsert without dropping it', () => { + store.save('k', 'v1'); + store.save('k', 'v2', undefined, 'why this matters'); + expect(store.getAll()[0].value).toBe('v2'); + expect(store.getAll()[0].description).toBe('why this matters'); + }); + + it('round-trips the description through disk', () => { + store.save('k', 'v', undefined, 'purpose note'); + store.flush(); + const reloaded = new ConstantsStoreImpl(filePath); + expect(reloaded.getAll()[0].description).toBe('purpose note'); + }); + }); + describe('getAll()', () => { it('returns constants sorted by name ascending', () => { store.save('zeta', '1');