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
1 change: 1 addition & 0 deletions src/@types/vscode.proposed.chatParticipantAdditions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ declare module 'vscode' {
isComplete?: boolean;
toolSpecificData?: ChatTerminalToolInvocationData;
fromSubAgent?: boolean;
presentation?: 'hidden' | 'hiddenAfterComplete' | undefined;

constructor(toolName: string, toolCallId: string, isError?: boolean);
}
Expand Down
5 changes: 5 additions & 0 deletions src/@types/vscode.proposed.chatSessionsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
146 changes: 144 additions & 2 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2877,15 +2877,157 @@ const ownedByMe: AsyncPredicate<GitHubRepository> = async repo => {
export const byRemoteName = (name: string): Predicate<GitHubRepository> => ({ 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 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 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];

// Preserve blank lines
if (line.trim() === '') {
result.push(line);
i++;
inListContext = false; // Reset list context on blank line
continue;
}

// 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 (plain text)
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 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;
}

// Join this line with a space
joinedLine += ' ' + nextLine;
i++;
}

result.push(joinedLine);
}

return result.join('\n');
}

export const titleAndBodyFrom = async (promise: Promise<string | undefined>): Promise<{ title: string; body: string } | undefined> => {
const message = await promise;
if (!message) {
return;
}
const idxLineBreak = message.indexOf('\n');
const hasBody = idxLineBreak !== -1;
const rawBody = hasBody ? message.slice(idxLineBreak + 1).trim() : '';
return {
title: idxLineBreak === -1 ? message : message.substr(0, idxLineBreak),
title: hasBody ? message.slice(0, idxLineBreak) : message,

body: idxLineBreak === -1 ? '' : message.slice(idxLineBreak + 1).trim(),
body: unwrapCommitMessageBody(rawBody),
};
};
128 changes: 128 additions & 0 deletions src/test/github/folderRepositoryManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,132 @@ 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.');
});

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.');
});
});