From c43b739b03af654132305ae869aecf471684abbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:47:41 +0000 Subject: [PATCH 1/7] Initial plan From 7777100d260d8e3c4f7d4930c65f3dc34671f8a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:51:38 +0000 Subject: [PATCH 2/7] Initial plan for commit message line unwrapping Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/@types/vscode.proposed.chatParticipantAdditions.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index 71520fa1ec..aa7001a3d2 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -105,6 +105,7 @@ declare module 'vscode' { isComplete?: boolean; toolSpecificData?: ChatTerminalToolInvocationData; fromSubAgent?: boolean; + presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; constructor(toolName: string, toolCallId: string, isError?: boolean); } From 21ff4a39553602d1571f1920d0a59ff9b6ee6f3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:55:10 +0000 Subject: [PATCH 3/7] Implement commit message line unwrapping for PR creation - Add unwrapCommitMessageBody function to join wrapped lines - Preserve blank lines, list items, quotes, and indented content - Add comprehensive tests for various unwrapping scenarios - Matches GitHub's behavior when converting commit messages to PR descriptions Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 70 +++++++++++++++++- .../github/folderRepositoryManager.test.ts | 72 +++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 9dd9002c84..efff8fed32 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2877,15 +2877,83 @@ const ownedByMe: AsyncPredicate = async repo => { export const byRemoteName = (name: string): Predicate => ({ remote: { remoteName } }) => remoteName === name; +/** + * Unwraps lines that were wrapped for conventional commit message formatting (typically at 72 characters). + * Similar to GitHub's behavior when converting commit messages to PR descriptions. + * + * Rules: + * - Preserves blank lines as paragraph breaks + * - Preserves lines that start with special characters (list items, quotes, indentation) + * - Joins consecutive lines that appear to be wrapped mid-sentence + */ +function unwrapCommitMessageBody(body: string): string { + if (!body) { + return body; + } + + const lines = body.split('\n'); + const result: string[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Preserve blank lines + if (line.trim() === '') { + result.push(line); + i++; + continue; + } + + // Check if this line should NOT be joined with the next + // Lines that start with special formatting characters should be preserved + const shouldPreserveLine = /^[\s*\-+>]|^\d+\./.test(line); + + if (shouldPreserveLine) { + result.push(line); + i++; + continue; + } + + // Start accumulating lines that should be joined + let joinedLine = line; + i++; + + // Keep joining lines until we hit a blank line or a line that shouldn't be joined + while (i < lines.length) { + const nextLine = lines[i]; + + // Stop at blank lines + if (nextLine.trim() === '') { + break; + } + + // Stop at lines that start with special formatting + if (/^[\s*\-+>]|^\d+\./.test(nextLine)) { + break; + } + + // Join this line with a space + joinedLine += ' ' + nextLine; + i++; + } + + result.push(joinedLine); + } + + return result.join('\n'); +} + export const titleAndBodyFrom = async (promise: Promise): Promise<{ title: string; body: string } | undefined> => { const message = await promise; if (!message) { return; } const idxLineBreak = message.indexOf('\n'); + const rawBody = idxLineBreak === -1 ? '' : message.slice(idxLineBreak + 1).trim(); return { title: idxLineBreak === -1 ? message : message.substr(0, idxLineBreak), - body: idxLineBreak === -1 ? '' : message.slice(idxLineBreak + 1).trim(), + body: unwrapCommitMessageBody(rawBody), }; }; diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index ad1cbff0f6..d17b46e780 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -94,4 +94,76 @@ describe('titleAndBodyFrom', function () { assert.strictEqual(result?.title, 'title'); assert.strictEqual(result?.body, ''); }); + + it('unwraps wrapped lines in body', async function () { + const message = Promise.resolve('title\n\nThis is a long line that has been wrapped at 72 characters\nto fit the conventional commit message format.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'This is a long line that has been wrapped at 72 characters to fit the conventional commit message format.'); + }); + + it('preserves blank lines as paragraph breaks', async function () { + const message = Promise.resolve('title\n\nFirst paragraph that is wrapped\nacross multiple lines.\n\nSecond paragraph that is also wrapped\nacross multiple lines.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'First paragraph that is wrapped across multiple lines.\n\nSecond paragraph that is also wrapped across multiple lines.'); + }); + + it('preserves list items', async function () { + const message = Promise.resolve('title\n\n- First item\n- Second item\n- Third item'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '- First item\n- Second item\n- Third item'); + }); + + it('preserves numbered list items', async function () { + const message = Promise.resolve('title\n\n1. First item\n2. Second item\n3. Third item'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '1. First item\n2. Second item\n3. Third item'); + }); + + it('preserves indented lines', async function () { + const message = Promise.resolve('title\n\nNormal paragraph.\n\n Indented code block\n More code'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'Normal paragraph.\n\n Indented code block\n More code'); + }); + + it('unwraps but preserves asterisk list items', async function () { + const message = Promise.resolve('title\n\n* First item\n* Second item'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '* First item\n* Second item'); + }); + + it('handles mixed content with wrapped paragraphs and lists', async function () { + const message = Promise.resolve('title\n\nThis is a paragraph that has been wrapped\nat 72 characters.\n\n- Item 1\n- Item 2\n\nAnother wrapped paragraph\nthat continues here.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'This is a paragraph that has been wrapped at 72 characters.\n\n- Item 1\n- Item 2\n\nAnother wrapped paragraph that continues here.'); + }); + + it('preserves lines with special characters at the start', async function () { + const message = Promise.resolve('title\n\n> Quote line 1\n> Quote line 2'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '> Quote line 1\n> Quote line 2'); + }); + + it('handles wrapped lines with punctuation', async function () { + const message = Promise.resolve('title\n\nThis is a sentence.\nThis is another sentence on a new line.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'This is a sentence. This is another sentence on a new line.'); + }); }); From 13255620353d29828d621dd99fcd7eee67d2e29a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:57:42 +0000 Subject: [PATCH 4/7] Address code review feedback - Extract regex pattern to named constant PRESERVE_LINE_PATTERN - Replace deprecated substr() with slice() - Improve regex to match only spaces/tabs explicitly instead of \s - Add detailed comments explaining the pattern Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index efff8fed32..60e2af3b67 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2891,6 +2891,12 @@ function unwrapCommitMessageBody(body: string): string { return body; } + // Pattern to detect lines that should be preserved (not joined): + // - Lines starting with whitespace (indented/code blocks) + // - Lines starting with list markers (*, -, +, >) + // - Lines starting with numbered list items (e.g., "1. ") + const PRESERVE_LINE_PATTERN = /^[ \t*\-+>]|^\d+\./; + const lines = body.split('\n'); const result: string[] = []; let i = 0; @@ -2907,7 +2913,7 @@ function unwrapCommitMessageBody(body: string): string { // Check if this line should NOT be joined with the next // Lines that start with special formatting characters should be preserved - const shouldPreserveLine = /^[\s*\-+>]|^\d+\./.test(line); + const shouldPreserveLine = PRESERVE_LINE_PATTERN.test(line); if (shouldPreserveLine) { result.push(line); @@ -2929,7 +2935,7 @@ function unwrapCommitMessageBody(body: string): string { } // Stop at lines that start with special formatting - if (/^[\s*\-+>]|^\d+\./.test(nextLine)) { + if (PRESERVE_LINE_PATTERN.test(nextLine)) { break; } @@ -2952,7 +2958,7 @@ export const titleAndBodyFrom = async (promise: Promise): Pr const idxLineBreak = message.indexOf('\n'); const rawBody = idxLineBreak === -1 ? '' : message.slice(idxLineBreak + 1).trim(); return { - title: idxLineBreak === -1 ? message : message.substr(0, idxLineBreak), + title: idxLineBreak === -1 ? message : message.slice(0, idxLineBreak), body: unwrapCommitMessageBody(rawBody), }; From 36871604e5201fc7cfffeaf82b7cbcfeaea143f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:00:58 +0000 Subject: [PATCH 5/7] Refine code based on additional review feedback - Place hyphen at end of character class for clarity - Extract hasBody condition to avoid duplication Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 60e2af3b67..ac52c834c9 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2895,7 +2895,7 @@ function unwrapCommitMessageBody(body: string): string { // - Lines starting with whitespace (indented/code blocks) // - Lines starting with list markers (*, -, +, >) // - Lines starting with numbered list items (e.g., "1. ") - const PRESERVE_LINE_PATTERN = /^[ \t*\-+>]|^\d+\./; + const PRESERVE_LINE_PATTERN = /^[ \t*+>\-]|^\d+\./; const lines = body.split('\n'); const result: string[] = []; @@ -2956,9 +2956,10 @@ export const titleAndBodyFrom = async (promise: Promise): Pr return; } const idxLineBreak = message.indexOf('\n'); - const rawBody = idxLineBreak === -1 ? '' : message.slice(idxLineBreak + 1).trim(); + const hasBody = idxLineBreak !== -1; + const rawBody = hasBody ? message.slice(idxLineBreak + 1).trim() : ''; return { - title: idxLineBreak === -1 ? message : message.slice(0, idxLineBreak), + title: hasBody ? message.slice(0, idxLineBreak) : message, body: unwrapCommitMessageBody(rawBody), }; From 44eb4e64842aa85f30bb5d381b127b4cbd8fa0a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:20:24 +0000 Subject: [PATCH 6/7] Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/@types/vscode.proposed.chatSessionsProvider.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index bd4e624430..772fc387b9 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -95,6 +95,11 @@ declare module 'vscode' { */ description?: string | MarkdownString; + /** + * An optional badge that provides additional context about the chat session. + */ + badge?: string | MarkdownString; + /** * An optional status indicating the current state of the session. */ From 1072b95f73af6e96e9f2780293da97b1ec1ffddc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:49:13 +0000 Subject: [PATCH 7/7] Add support for fenced code blocks and fix indented code detection - Support fenced code blocks with ``` markers - Fix indented code detection to distinguish between: - Actual code blocks (4+ spaces, not in list context) - List item continuations (2+ spaces in list context) - Nested list items (detected by list markers with indentation) - Track list context state to handle list continuations correctly - Add comprehensive tests for all new scenarios Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 93 ++++++++++++++++--- .../github/folderRepositoryManager.test.ts | 56 +++++++++++ 2 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index ac52c834c9..2b0135f05e 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2883,23 +2883,29 @@ export const byRemoteName = (name: string): Predicate => ({ re * * Rules: * - Preserves blank lines as paragraph breaks - * - Preserves lines that start with special characters (list items, quotes, indentation) - * - Joins consecutive lines that appear to be wrapped mid-sentence + * - Preserves fenced code blocks (```) + * - Preserves list items (-, *, +, numbered) + * - Preserves blockquotes (>) + * - Preserves indented code blocks (4+ spaces at start, when not in a list context) + * - Joins consecutive plain text lines that appear to be wrapped mid-sentence */ function unwrapCommitMessageBody(body: string): string { if (!body) { return body; } - // Pattern to detect lines that should be preserved (not joined): - // - Lines starting with whitespace (indented/code blocks) - // - Lines starting with list markers (*, -, +, >) - // - Lines starting with numbered list items (e.g., "1. ") - const PRESERVE_LINE_PATTERN = /^[ \t*+>\-]|^\d+\./; + // Pattern to detect list item markers at the start of a line + const LIST_ITEM_PATTERN = /^[ \t]*([*+\-]|\d+\.)\s/; + // Pattern to detect blockquote markers + const BLOCKQUOTE_PATTERN = /^[ \t]*>/; + // Pattern to detect fenced code block markers + const FENCE_PATTERN = /^[ \t]*```/; const lines = body.split('\n'); const result: string[] = []; let i = 0; + let inFencedBlock = false; + let inListContext = false; while (i < lines.length) { const line = lines[i]; @@ -2908,20 +2914,58 @@ function unwrapCommitMessageBody(body: string): string { if (line.trim() === '') { result.push(line); i++; + inListContext = false; // Reset list context on blank line continue; } - // Check if this line should NOT be joined with the next - // Lines that start with special formatting characters should be preserved - const shouldPreserveLine = PRESERVE_LINE_PATTERN.test(line); + // Check for fenced code block markers + if (FENCE_PATTERN.test(line)) { + inFencedBlock = !inFencedBlock; + result.push(line); + i++; + continue; + } + + // Preserve everything inside fenced code blocks + if (inFencedBlock) { + result.push(line); + i++; + continue; + } + + // Check if this line is a list item + const isListItem = LIST_ITEM_PATTERN.test(line); + + // Check if this line is a blockquote + const isBlockquote = BLOCKQUOTE_PATTERN.test(line); + + // Check if this line is indented (4+ spaces) but NOT a list continuation + // List continuations have leading spaces but we're in list context + const leadingSpaces = line.match(/^[ \t]*/)?.[0].length || 0; + const isIndentedCode = leadingSpaces >= 4 && !inListContext; + + // Determine if this line should be preserved (not joined) + const shouldPreserveLine = isListItem || isBlockquote || isIndentedCode; if (shouldPreserveLine) { + result.push(line); + i++; + // If this is a list item, we're now in list context + if (isListItem) { + inListContext = true; + } + continue; + } + + // If we have leading spaces but we're in a list context, this is a list continuation + // We should preserve it to maintain list formatting + if (inListContext && leadingSpaces >= 2) { result.push(line); i++; continue; } - // Start accumulating lines that should be joined + // Start accumulating lines that should be joined (plain text) let joinedLine = line; i++; @@ -2934,8 +2978,31 @@ function unwrapCommitMessageBody(body: string): string { break; } - // Stop at lines that start with special formatting - if (PRESERVE_LINE_PATTERN.test(nextLine)) { + // Stop at fenced code blocks + if (FENCE_PATTERN.test(nextLine)) { + break; + } + + // Stop at list items + if (LIST_ITEM_PATTERN.test(nextLine)) { + break; + } + + // Stop at blockquotes + if (BLOCKQUOTE_PATTERN.test(nextLine)) { + break; + } + + // Check if next line is indented code (4+ spaces, not in list context) + const nextLeadingSpaces = nextLine.match(/^[ \t]*/)?.[0].length || 0; + const nextIsIndentedCode = nextLeadingSpaces >= 4 && !inListContext; + + if (nextIsIndentedCode) { + break; + } + + // If in list context and next line is indented, it's a list continuation + if (inListContext && nextLeadingSpaces >= 2) { break; } diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index d17b46e780..574a69a32e 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -166,4 +166,60 @@ describe('titleAndBodyFrom', function () { assert.strictEqual(result?.title, 'title'); assert.strictEqual(result?.body, 'This is a sentence. This is another sentence on a new line.'); }); + + it('preserves fenced code blocks', async function () { + const message = Promise.resolve('title\n\nSome text before.\n\n```\ncode line 1\ncode line 2\n```\n\nSome text after.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'Some text before.\n\n```\ncode line 1\ncode line 2\n```\n\nSome text after.'); + }); + + it('preserves fenced code blocks with language', async function () { + const message = Promise.resolve('title\n\nSome text.\n\n```javascript\nconst x = 1;\nconst y = 2;\n```\n\nMore text.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'Some text.\n\n```javascript\nconst x = 1;\nconst y = 2;\n```\n\nMore text.'); + }); + + it('preserves nested list items with proper indentation', async function () { + const message = Promise.resolve('title\n\n- Item 1\n - Nested item 1.1\n - Nested item 1.2\n- Item 2'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '- Item 1\n - Nested item 1.1\n - Nested item 1.2\n- Item 2'); + }); + + it('preserves list item continuations', async function () { + const message = Promise.resolve('title\n\n- This is a list item that is long\n and continues on the next line\n- Second item'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '- This is a list item that is long\n and continues on the next line\n- Second item'); + }); + + it('preserves indented code blocks but not list continuations', async function () { + const message = Promise.resolve('title\n\nRegular paragraph.\n\n This is code\n More code\n\nAnother paragraph.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'Regular paragraph.\n\n This is code\n More code\n\nAnother paragraph.'); + }); + + it('unwraps regular text but preserves list item continuations', async function () { + const message = Promise.resolve('title\n\nThis is wrapped text\nthat should be joined.\n\n- List item with\n continuation\n- Another item'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'This is wrapped text that should be joined.\n\n- List item with\n continuation\n- Another item'); + }); + + it('handles complex nested lists with wrapped paragraphs', async function () { + const message = Promise.resolve('title\n\nWrapped paragraph\nacross lines.\n\n- Item 1\n - Nested item\n More nested content\n- Item 2\n\nAnother wrapped paragraph\nhere.'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, 'Wrapped paragraph across lines.\n\n- Item 1\n - Nested item\n More nested content\n- Item 2\n\nAnother wrapped paragraph here.'); + }); });