feat: add remark-stringify for markdown processing and update logging in TeamMemberLogsFinder

- Added `remark-stringify` to the project for converting markdown to plain text.
- Updated the text formatting pipeline to include `remark-stringify` for improved markdown handling.
- Commented out performance logging in `TeamMemberLogsFinder` to reduce console output during execution.
- Enhanced the `getUnreadCount` and `getLegacyCutoff` functions in `commentReadStorage` to clarify legacy comment handling logic.
This commit is contained in:
iliya 2026-03-15 20:18:28 +02:00
parent 36e93abd42
commit a685ae3e6c
5 changed files with 45 additions and 21 deletions

View file

@ -147,6 +147,7 @@
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"simple-git": "^3.32.3",
"ssh-config": "^5.0.4",
"ssh2": "^1.17.0",

View file

@ -254,6 +254,9 @@ importers:
remark-parse:
specifier: ^11.0.0
version: 11.0.0
remark-stringify:
specifier: ^11.0.0
version: 11.0.0
simple-git:
specifier: ^3.32.3
version: 3.32.3

View file

@ -412,9 +412,9 @@ export class TeamMemberLogsFinder {
const tDiscovery = performance.now();
if (!discovery) {
console.log(
`[perf] findLogFileRefsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms`
);
// console.log(
// `[perf] findLogFileRefsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms`
// );
return [];
}
@ -581,15 +581,15 @@ export class TeamMemberLogsFinder {
const sortedRefs = [...refs].sort((a, b) => b.sortTime - a.sortTime);
const tTotal = performance.now();
console.log(
`[perf] findLogFileRefsForTask(${taskId}@${teamName}) ` +
`total=${(tTotal - t0).toFixed(0)}ms | ` +
`discovery=${(tDiscovery - t0).toFixed(0)}ms | ` +
`lead=${(tLead - tDiscovery).toFixed(0)}ms | ` +
`scan=${(tScan - tLead).toFixed(0)}ms (${totalFiles} files, ${mentionHits} hits) | ` +
`owner=${(tOwner - tScan).toFixed(0)}ms | ` +
`sessions=${sessionIds.length} | results=${sortedRefs.length}`
);
// console.log(
// `[perf] findLogFileRefsForTask(${taskId}@${teamName}) ` +
// `total=${(tTotal - t0).toFixed(0)}ms | ` +
// `discovery=${(tDiscovery - t0).toFixed(0)}ms | ` +
// `lead=${(tLead - tDiscovery).toFixed(0)}ms | ` +
// `scan=${(tScan - tLead).toFixed(0)}ms (${totalFiles} files, ${mentionHits} hits) | ` +
// `owner=${(tOwner - tScan).toFixed(0)}ms | ` +
// `sessions=${sessionIds.length} | results=${sortedRefs.length}`
// );
return sortedRefs.map(({ filePath, memberName }) => ({ filePath, memberName }));
}

View file

@ -1,14 +1,16 @@
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import stripMarkdownPlugin from 'strip-markdown';
import { unified } from 'unified';
const processor = unified().use(remarkParse).use(stripMarkdownPlugin);
const processor = unified().use(remarkParse).use(stripMarkdownPlugin).use(remarkStringify);
/**
* Strips markdown formatting from text for use in plain-text contexts
* like native OS notifications.
*
* Uses remark ecosystem (strip-markdown plugin) for reliable parsing.
* Pipeline: remarkParse stripMarkdown (transform) remarkStringify (compile to plain text).
*/
export function stripMarkdown(text: string): string {
const result = processor.processSync(text);

View file

@ -162,9 +162,14 @@ export function markAsRead(teamName: string, taskId: string, latestTimestamp: nu
/**
* Count unread comments for a task.
* A comment is unread if:
* 1. Its ID is NOT in the readIds set, AND
* 2. Its timestamp is AFTER the lastUpdated migration marker (for legacy data)
* A comment is unread if its ID is NOT in the readIds set.
*
* Legacy migration: when readIds is empty (data migrated from v1 timestamp
* format), comments created at or before the legacy cutoff are treated as read.
* Once any per-ID tracking starts (readIds non-empty), the cutoff is ignored
* only explicit IDs determine read state. This prevents `lastUpdated`
* (which is refreshed by markCommentsRead on every save for stale-cleanup
* purposes) from accidentally marking ALL comments as read.
*/
export function getUnreadCount(
readState: ReadState,
@ -178,15 +183,20 @@ export function getUnreadCount(
if (!entry) return comments.length;
const readSet = new Set(entry.readIds);
const legacyCutoff = entry.lastUpdated;
// Only use the timestamp cutoff for pure-legacy entries (no per-ID tracking yet).
// Once readIds is non-empty, per-ID tracking is authoritative and the timestamp
// must NOT be used — it gets refreshed to Date.now() on every save.
const legacyCutoff = readSet.size === 0 ? entry.lastUpdated : 0;
let count = 0;
for (const c of comments) {
// If comment has an ID and it's in the read set → read
if (c.id && readSet.has(c.id)) continue;
// If comment was created before/at the legacy cutoff → read (migrated data)
const ts = new Date(c.createdAt).getTime();
if (legacyCutoff > 0 && ts <= legacyCutoff) continue;
// Legacy-only: comment created before/at the migration cutoff → read
if (legacyCutoff > 0) {
const ts = new Date(c.createdAt).getTime();
if (ts <= legacyCutoff) continue;
}
// Otherwise → unread
count++;
}
@ -204,10 +214,18 @@ export function getReadCommentIds(teamName: string, taskId: string): Set<string>
/**
* Get the legacy migration cutoff timestamp for a team/task pair (0 if none).
* Returns non-zero only for pure-legacy entries where readIds is empty.
* Once per-ID tracking has started (readIds non-empty), the cutoff is 0
* because lastUpdated gets refreshed to Date.now() on every save and
* would incorrectly mark all comments as read.
*/
export function getLegacyCutoff(teamName: string, taskId: string): number {
const key = `${teamName}/${taskId}`;
return cache[key]?.lastUpdated ?? 0;
const entry = cache[key];
if (!entry) return 0;
// Only honour the timestamp when no per-ID tracking exists (pure legacy data).
if (entry.readIds.length > 0) return 0;
return entry.lastUpdated;
}
/** @deprecated Use getReadCommentIds() + getLegacyCutoff() instead. */