Skip to content

Commit 4dfc95c

Browse files
committed
feat: implement smart title search and inline comments separation
- Add hybrid title search that attempts exact match first, then falls back to partial matching using CQL - Implement new inline comments tool (conf_ls_inline_comments) for filtering inline-only comments - Add comprehensive error handling for CQL compatibility issues (space.id vs space.key) - Transform search results to proper pages format for seamless integration - Maintain backward compatibility for exact title matches - Update tool descriptions to reflect SMART MATCHING capabilities BREAKING CHANGE: Title search behavior now includes automatic partial matching fallback
1 parent 7f81648 commit 4dfc95c

8 files changed

+725
-4
lines changed

src/controllers/atlassian.comments.controller.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '../utils/pagination.util.js';
1212
import { ControllerResponse } from '../types/common.types.js';
1313
import { formatCommentsList } from './atlassian.comments.formatter.js';
14+
import { formatInlineCommentsList } from './atlassian.inline-comments.formatter.js';
1415
import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
1516
import { adfToMarkdown } from '../utils/adf.util.js';
1617
import {
@@ -49,6 +50,41 @@ interface ListPageCommentsOptions {
4950
bodyFormat?: 'storage' | 'view' | 'atlas_doc_format';
5051
}
5152

53+
/**
54+
* Interface for list inline comments options
55+
*/
56+
interface ListInlineCommentsOptions {
57+
/**
58+
* The ID of the page to get inline comments for
59+
*/
60+
pageId: string;
61+
62+
/**
63+
* Include resolved inline comments
64+
*/
65+
includeResolved?: boolean;
66+
67+
/**
68+
* Sort order for inline comments
69+
*/
70+
sortBy?: 'created' | 'position';
71+
72+
/**
73+
* Maximum number of results to return
74+
*/
75+
limit?: number;
76+
77+
/**
78+
* Starting point for pagination
79+
*/
80+
start?: number;
81+
82+
/**
83+
* Body format (storage, view, atlas_doc_format)
84+
*/
85+
bodyFormat?: 'storage' | 'view' | 'atlas_doc_format';
86+
}
87+
5288
/**
5389
* Extended interface for a comment with converted markdown content
5490
*/
@@ -206,7 +242,184 @@ async function listPageComments(
206242
}
207243
}
208244

245+
/**
246+
* List inline comments only for a specific Confluence page
247+
*
248+
* @param options - Options for listing inline comments
249+
* @returns Controller response with formatted inline comments and pagination info
250+
*/
251+
async function listInlineComments(
252+
options: ListInlineCommentsOptions,
253+
): Promise<ControllerResponse> {
254+
const methodLogger = logger.forMethod('listInlineComments');
255+
try {
256+
// Apply defaults and prepare service parameters
257+
const {
258+
pageId,
259+
includeResolved = false,
260+
sortBy = 'position',
261+
limit = DEFAULT_PAGE_SIZE,
262+
start = 0,
263+
bodyFormat = 'atlas_doc_format',
264+
} = options;
265+
266+
methodLogger.debug('Listing inline comments for page', {
267+
pageId,
268+
includeResolved,
269+
sortBy,
270+
limit,
271+
start,
272+
bodyFormat,
273+
});
274+
275+
// Get all comments first with a higher limit to ensure we capture inline comments
276+
// since we'll filter them locally
277+
const allCommentsData = await atlassianCommentsService.listPageComments(
278+
{
279+
pageId,
280+
limit: 250, // Get more comments to filter inline ones
281+
start: 0, // Always start from beginning for inline filtering
282+
bodyFormat,
283+
},
284+
);
285+
286+
methodLogger.debug('Retrieved all comments for filtering', {
287+
totalComments: allCommentsData.results.length,
288+
pageId,
289+
});
290+
291+
// Filter for inline comments only
292+
const inlineCommentsRaw = allCommentsData.results.filter((comment) => {
293+
const isInline = comment.extensions?.location === 'inline';
294+
295+
// Apply resolved filter if needed
296+
if (!includeResolved && comment.status !== 'current') {
297+
return false;
298+
}
299+
300+
return isInline;
301+
});
302+
303+
methodLogger.debug('Filtered inline comments', {
304+
inlineCount: inlineCommentsRaw.length,
305+
totalCount: allCommentsData.results.length,
306+
includeResolved,
307+
});
308+
309+
// Convert ADF content to Markdown and extract highlighted text for inline comments
310+
const convertedComments: CommentWithContext[] = inlineCommentsRaw.map(
311+
(comment) => {
312+
let markdownBody =
313+
'*Content format not supported or unavailable*';
314+
315+
// Convert comment body from ADF to Markdown
316+
if (comment.body?.atlas_doc_format?.value) {
317+
try {
318+
markdownBody = adfToMarkdown(
319+
comment.body.atlas_doc_format.value,
320+
);
321+
methodLogger.debug(
322+
`Successfully converted ADF to Markdown for inline comment ${comment.id}`,
323+
);
324+
} catch (conversionError) {
325+
methodLogger.error(
326+
`ADF conversion failed for inline comment ${comment.id}`,
327+
conversionError,
328+
);
329+
// Keep default error message
330+
}
331+
} else {
332+
methodLogger.warn(
333+
`No ADF content available for inline comment ${comment.id}`,
334+
);
335+
}
336+
337+
// Extract the highlighted text for inline comments
338+
let highlightedText: string | undefined = undefined;
339+
if (comment.extensions?.inlineProperties) {
340+
// Safely access inlineProperties fields with type checking
341+
const props = comment.extensions
342+
.inlineProperties as InlineProperties;
343+
344+
// Try different properties that might contain the highlighted text
345+
highlightedText =
346+
props.originalSelection || props.textContext;
347+
348+
// If not found in standard properties, check for custom properties
349+
if (!highlightedText && 'selectionText' in props) {
350+
highlightedText = String(props.selectionText || '');
351+
}
352+
353+
if (highlightedText) {
354+
methodLogger.debug(
355+
`Found highlighted text for inline comment ${comment.id}: ${highlightedText.substring(0, 50)}${highlightedText.length > 50 ? '...' : ''}`,
356+
);
357+
} else {
358+
methodLogger.warn(
359+
`No highlighted text found for inline comment ${comment.id}`,
360+
);
361+
}
362+
}
363+
364+
// Return comment with added context
365+
return {
366+
...comment,
367+
convertedMarkdownBody: markdownBody,
368+
highlightedText,
369+
};
370+
},
371+
);
372+
373+
// Sort inline comments by requested order
374+
if (sortBy === 'position') {
375+
convertedComments.sort((a, b) => {
376+
// Sort by marker position or container ID if available
377+
const aPos = a.extensions?.inlineProperties?.markerRef || a.id;
378+
const bPos = b.extensions?.inlineProperties?.markerRef || b.id;
379+
return String(aPos).localeCompare(String(bPos));
380+
});
381+
} else if (sortBy === 'created') {
382+
// Sort by ID as a proxy for creation order (newer IDs = later created)
383+
convertedComments.sort((a, b) => a.id.localeCompare(b.id));
384+
}
385+
386+
// Apply pagination after filtering and sorting
387+
const paginatedComments = convertedComments.slice(start, start + limit);
388+
389+
methodLogger.debug('Applied pagination to inline comments', {
390+
totalInline: convertedComments.length,
391+
start,
392+
limit,
393+
returned: paginatedComments.length,
394+
});
395+
396+
// Format the inline comments for display
397+
const baseUrl = allCommentsData._links?.base || '';
398+
const formattedContent = formatInlineCommentsList(
399+
paginatedComments,
400+
pageId,
401+
baseUrl,
402+
convertedComments.length,
403+
start,
404+
limit,
405+
);
406+
407+
return {
408+
content: formattedContent,
409+
};
410+
} catch (error) {
411+
// Handle errors
412+
throw handleControllerError(error, {
413+
entityType: 'InlineComment',
414+
operation: 'list',
415+
source: 'controllers/atlassian.comments.controller.ts@listInlineComments',
416+
additionalInfo: { pageId: options.pageId },
417+
});
418+
}
419+
}
420+
209421
// Export controller functions
210422
export const atlassianCommentsController = {
211423
listPageComments,
424+
listInlineComments,
212425
};

0 commit comments

Comments
 (0)