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
? ''
: entries.map(e =>
- `