human (Feb 3, 2025, 02:08 PM)
Build a tool that turns a link to a GitHub issues thread into markdown I can copy out - the markdown should include the title and body of the issue and the body of every attached comment
Include a copy to clipboard button
Use the GitHub public api and fetch()
assistant (Feb 3, 2025, 02:09 PM)
I'll help create a tool that converts GitHub issue threads to markdown with a copy button.
<!DOCTYPE html>
<html>
<head>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Helvetica, Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
margin-bottom: 24px;
}
.input-group {
margin-bottom: 24px;
}
label {
display: block;
margin-bottom: 8px;
}
input {
width: 100%;
padding: 8px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background: #2ea44f;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #2c974b;
}
button:disabled {
background: #94d3a2;
cursor: not-allowed;
}
.output {
margin-top: 24px;
}
textarea {
width: 100%;
min-height: 300px;
padding: 12px;
font-size: 16px;
font-family: monospace;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
}
.error {
color: #cf222e;
margin-top: 8px;
}
.copy-button {
margin-top: 12px;
}
</style>
</head>
<body>
<h1>Convert GitHub issue to markdown</h1>
<div class="input-group">
<label for="issue-url">GitHub issue URL</label>
<input
type="text"
id="issue-url"
placeholder="https://github.com/owner/repo/issues/123"
>
</div>
<button id="convert">Convert to markdown</button>
<div class="output">
<textarea id="markdown-output" readonly></textarea>
<button class="copy-button" id="copy">Copy to clipboard</button>
</div>
<p class="error" id="error"></p>
<script type="module">
const urlInput = document.getElementById('issue-url')
const convertButton = document.getElementById('convert')
const markdownOutput = document.getElementById('markdown-output')
const copyButton = document.getElementById('copy')
const errorElement = document.getElementById('error')
function parseGitHubUrl(url) {
try {
const urlObj = new URL(url)
const [, owner, repo, , number] = urlObj.pathname.split('/')
return { owner, repo, number }
} catch (e) {
throw new Error('Invalid GitHub URL')
}
}
function convertToMarkdown(issue, comments) {
let md = `# ${issue.title}\n\n`
md += `*Posted by @${issue.user.login}*\n\n`
md += issue.body + '\n\n'
if (comments.length > 0) {
md += '---\n\n'
comments.forEach(comment => {
md += `### Comment by @${comment.user.login}\n\n`
md += comment.body + '\n\n'
md += '---\n\n'
})
}
return md
}
async function fetchIssueAndComments(owner, repo, number) {
const issueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
const commentsUrl = `${issueUrl}/comments`
const [issueRes, commentsRes] = await Promise.all([
fetch(issueUrl),
fetch(commentsUrl)
])
if (!issueRes.ok || !commentsRes.ok) {
throw new Error('Failed to fetch issue data')
}
const issue = await issueRes.json()
const comments = await commentsRes.json()
return { issue, comments }
}
convertButton.addEventListener('click', async () => {
errorElement.textContent = ''
markdownOutput.value = ''
convertButton.disabled = true
try {
const { owner, repo, number } = parseGitHubUrl(urlInput.value)
const { issue, comments } = await fetchIssueAndComments(owner, repo, number)
const markdown = convertToMarkdown(issue, comments)
markdownOutput.value = markdown
} catch (error) {
errorElement.textContent = error.message
} finally {
convertButton.disabled = false
}
})
copyButton.addEventListener('click', () => {
markdownOutput.select()
document.execCommand('copy')
})
</script>
</body>
</html>
I've created a simple web tool that:
- Takes a GitHub issue URL as input
- Fetches the issue data and comments using the GitHub API
- Converts everything to markdown format
- Provides a copy button to easily copy the output
To use it:
- Paste a GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)
- Click "Convert to markdown"
- The formatted markdown will appear in the textarea
- Click "Copy to clipboard" to copy it
The markdown format includes:
- Issue title as a heading
- Original poster's username
- Issue body
- All comments with usernames and separators
Let me know if you'd like any adjustments to the formatting or functionality!
human (Feb 3, 2025, 02:26 PM)
Copy button should change text to "Copied" for a few seconds then change back
assistant (Feb 3, 2025, 02:27 PM)
I'll update the copy button behavior.
Find this:
copyButton.addEventListener('click', () => {
markdownOutput.select()
document.execCommand('copy')
})
Replace with this:
copyButton.addEventListener('click', () => {
markdownOutput.select()
document.execCommand('copy')
const originalText = copyButton.textContent
copyButton.textContent = 'Copied'
copyButton.disabled = true
setTimeout(() => {
copyButton.textContent = originalText
copyButton.disabled = false
}, 2000)
})
I've updated the copy button so that:
- When clicked, it changes text to "Copied"
- The button is temporarily disabled to prevent multiple clicks
- After 2 seconds, it reverts to "Copy to clipboard" and becomes enabled again
The button will now provide better visual feedback when the copy action succeeds. Would you like any other improvements to the interface?
human (Feb 3, 2025, 02:28 PM)
Make sure it handles pagination if there are many comments
assistant (Feb 3, 2025, 02:28 PM)
I'll update the code to handle GitHub's pagination using the Link header.
Find this:
async function fetchIssueAndComments(owner, repo, number) {
const issueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
const commentsUrl = `${issueUrl}/comments`
const [issueRes, commentsRes] = await Promise.all([
fetch(issueUrl),
fetch(commentsUrl)
])
if (!issueRes.ok || !commentsRes.ok) {
throw new Error('Failed to fetch issue data')
}
const issue = await issueRes.json()
const comments = await commentsRes.json()
return { issue, comments }
Replace with this:
async function getAllPages(url) {
let allItems = []
let nextUrl = url
while (nextUrl) {
const response = await fetch(nextUrl)
if (!response.ok) {
throw new Error('Failed to fetch data')
}
const items = await response.json()
allItems = allItems.concat(items)
// Check for pagination in Link header
const link = response.headers.get('Link')
nextUrl = null
if (link) {
const nextLink = link.split(',').find(s => s.includes('rel="next"'))
if (nextLink) {
nextUrl = nextLink.split(';')[0].trim().slice(1, -1)
}
}
}
return allItems
}
async function fetchIssueAndComments(owner, repo, number) {
const issueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
const commentsUrl = `${issueUrl}/comments`
const [issue, comments] = await Promise.all([
fetch(issueUrl).then(res => {
if (!res.ok) throw new Error('Failed to fetch issue')
return res.json()
}),
getAllPages(commentsUrl)
])
return { issue, comments }
I've updated the code to:
-
Add a new
getAllPages
function that:- Follows the GitHub API pagination using the Link header
- Recursively fetches all pages of comments
- Concatenates the results into a single array
-
Modified
fetchIssueAndComments
to:- Use the new pagination-aware function for comments
- Keep the single request for the issue itself (since issues aren't paginated)
- Handle errors more specifically
Now the tool will correctly handle issues with any number of comments. GitHub typically paginates at 30 comments per page, so this ensures we get the complete thread no matter how long it is.
Would you like any other improvements to the functionality?