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
22 changes: 22 additions & 0 deletions docs/PARITY_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
7 changes: 6 additions & 1 deletion src/main/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/main/columnLayoutsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
13 changes: 9 additions & 4 deletions src/main/constantsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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[] {
Expand Down
4 changes: 2 additions & 2 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
});

Expand Down
17 changes: 10 additions & 7 deletions src/mcp-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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 };
Expand Down
4 changes: 2 additions & 2 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> =>
Expand Down
Loading
Loading