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 .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/src/js/13-agent-handoff.js @redpanda-data/documentation
2 changes: 2 additions & 0 deletions .github/workflows/validate-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci
- name: Test page actions
run: npm run test:markdown-dropdown
- name: Cache generated files
uses: actions/cache@v4
with:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"vinyl-source-stream": "^2.0.0"
},
"scripts": {
"test:markdown-dropdown": "node --test tests/markdown-dropdown/*.test.js",
"test:headless": "node tests/bloblang-playground/test-runner.js",
"test:playground": "npm run build:wasm && npm run test:headless",
"test:interactive": "node tests/bloblang-interactive/test-runner.js",
Expand Down
1 change: 1 addition & 0 deletions preview-src/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
:!table-caption:
:page-pagination:
:page-has-markdown:
:page-agent-handoff:

{description}

Expand Down
7 changes: 7 additions & 0 deletions src/helpers/has-agent-handoff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Show the agent handoff action only when the page opts in.
*/

module.exports = ({ data: { root } }) => {
return root.page?.attributes?.['agent-handoff'] !== undefined
}
130 changes: 130 additions & 0 deletions src/js/13-agent-handoff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
;(function (root, factory) {
const agentHandoff = factory()

if (typeof module === 'object' && module.exports) {
module.exports = agentHandoff
} else {
root.RedpandaDocsAgentHandoff = agentHandoff
}
})(typeof window === 'undefined' ? this : window, function () {
const headingPattern = /^(#{1,6})[ \t]+(.+?)\s*$/
const fencePattern = /^[ \t]*(`{3,}|~{3,})/

function normalizedAnchors (sectionAnchor) {
const rawAnchor = sectionAnchor.replace(/^#/, '')
let decodedAnchor

try {
decodedAnchor = decodeURIComponent(rawAnchor)
} catch (error) {
decodedAnchor = rawAnchor
}

const generatedMarkdownAnchor = decodedAnchor.replace(/^_/, '').replace(/_/g, '-')
return generatedMarkdownAnchor === decodedAnchor
? [decodedAnchor]
: [decodedAnchor, generatedMarkdownAnchor]
}

function extractMarkdownSection (markdown, sectionAnchor) {
const fullPage = markdown.trim()
if (!sectionAnchor) return fullPage

const anchorMarkers = normalizedAnchors(sectionAnchor).map((anchor) => `(#${anchor})`)
const lines = fullPage.split('\n')
const headingLines = computeHeadingLines(lines)
const startIndex = lines.findIndex(
(line, index) => headingLines[index] && anchorMarkers.some((marker) => line.includes(marker))
)
if (startIndex === -1) return fullPage

const sectionLevel = lines[startIndex].match(headingPattern)[1].length
const endIndex = lines.findIndex((line, index) => {
if (index <= startIndex) return false

if (!headingLines[index]) return false
return line.match(headingPattern)[1].length <= sectionLevel
})

return lines.slice(startIndex, endIndex === -1 ? undefined : endIndex).join('\n').trim()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function computeHeadingLines (lines) {
let fenceMarker = ''

return lines.map((line) => {
const fence = line.match(fencePattern)
if (fence) {
const marker = fence[1][0]
if (!fenceMarker) fenceMarker = marker
else if (marker === fenceMarker) fenceMarker = ''
return false
}

return !fenceMarker && headingPattern.test(line)
})
}

function buildAgentHandoffPrompt ({
componentName = '',
docsOrigin,
markdown,
markdownUrl,
pageTitle,
pageUrl,
sectionAnchor = '',
sectionTitle = '',
}) {
const context = extractMarkdownSection(markdown, sectionAnchor)
const componentExport = componentName
? new URL(`/${encodeURIComponent(componentName)}-full.txt`, docsOrigin).href
: null
const scope = [
`- Documentation page: ${pageTitle}`,
sectionTitle ? `- Current section: ${sectionTitle}` : null,
`- Page: ${pageUrl}`,
`- Markdown source: ${markdownUrl}`,
].filter(Boolean)
const sources = [
`- Documentation index: ${new URL('/llms.txt', docsOrigin).href}`,
componentExport ? `- Component documentation export: ${componentExport}` : null,
`- Documentation MCP server: ${new URL('/mcp', docsOrigin).href}`,
].filter(Boolean)
const instructions = [
"1. Read the current project's agent and contributor instructions before changing anything.",
'2. Inspect the project and identify where this documentation applies. Do not invent Redpanda commands, fields, or behavior.',
'3. If applicable, implement the smallest reversible change and preserve unrelated behavior. If not applicable, explain why and stop.',
'4. You may edit and test local files. Before destructive operations, external mutations, or changes to a live ' +
'Redpanda environment, show the plan or diff and get my confirmation. Never expose credentials or secrets.',
"5. Run the project's relevant checks and any documented Redpanda validation or diff command.",
'6. Summarize the changes, verification, remaining manual steps, and any missing or conflicting documentation.',
]

return `# Apply this Redpanda documentation

Work in the current project. Determine whether this guidance applies, then make the smallest safe update that keeps the project aligned with the current Redpanda pattern.

## Scope

${scope.join('\n')}

## Authoritative Redpanda context

${sources.join('\n')}

## Instructions

${instructions.join('\n')}

## Documentation context

--- BEGIN CURRENT DOCUMENTATION ---
${context}
--- END CURRENT DOCUMENTATION ---`
}

return {
buildAgentHandoffPrompt,
extractMarkdownSection,
}
})
69 changes: 69 additions & 0 deletions src/js/14-markdown-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
const menu = dropdown.querySelector('.markdown-dropdown-menu')
const items = dropdown.querySelectorAll('.markdown-dropdown-item')
const markdownUrl = dropdown.dataset.markdownUrl
const componentName = dropdown.dataset.componentName

if (!toggle || !menu || !markdownUrl) {
return
Expand All @@ -60,6 +61,14 @@
setTimeout(function () {
setOpen(false)
}, 2500)
} else if (action === 'copy-agent') {
handleCopyAgent(markdownUrl, componentName, item).then(function (didCopy) {
if (didCopy) {
setTimeout(function () {
setOpen(false)
}, 2500)
}
})
} else if (action === 'view') {
handleView(markdownUrl)
setOpen(false)
Expand Down Expand Up @@ -155,6 +164,66 @@
)
}

function flashCopyStatus (button, message) {
const status = button.querySelector('[data-agent-handoff-status]')
if (status) status.textContent = message

button.classList.add('clicked')
// Force reflow so the animation can restart.
button.offsetHeight // eslint-disable-line no-unused-expressions
button.classList.remove('clicked')
}

/**
* Copy a complete agent handoff with the current documentation section inline.
*/
function handleCopyAgent (markdownUrl, componentName, button) {
const buildAgentHandoffPrompt = window.RedpandaDocsAgentHandoff?.buildAgentHandoffPrompt
if (typeof buildAgentHandoffPrompt !== 'function') {
console.error('Could not copy agent handoff: prompt builder is unavailable.')
flashCopyStatus(button, 'Could not copy. Try again.')
return Promise.resolve(false)
}

return window
.fetch(markdownUrl)
.then(function (response) {
if (!response.ok) throw new Error(`Failed to fetch documentation (${response.status})`)
return response.text()
})
.then(function (markdown) {
const pageUrl = window.location.href
const absoluteMarkdownUrl = new URL(markdownUrl, window.location.origin).href
const pageTitle = document.querySelector('h1.page')?.textContent?.trim() || document.title
const sectionAnchor = window.location.hash
const sectionId = sectionAnchor.replace(/^#/, '')
const sectionTitle = document.getElementById(sectionId)?.textContent?.trim() || ''
const prompt = buildAgentHandoffPrompt({
componentName,
docsOrigin: window.location.origin,
markdown,
markdownUrl: absoluteMarkdownUrl,
pageTitle,
pageUrl,
sectionAnchor,
sectionTitle,
})

return window.navigator.clipboard.writeText(prompt)
})
.then(
function () {
flashCopyStatus(button, 'Copied!')
return true
},
function (error) {
console.error('Could not copy agent handoff:', error)
flashCopyStatus(button, 'Could not copy. Try again.')
return false
}
)
}

/**
* Handle view in new tab
*/
Expand Down
26 changes: 25 additions & 1 deletion src/partials/markdown-dropdown.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{{#if (has-markdown)}}
<div class="markdown-dropdown" data-markdown-url="{{markdown-url}}">
<div
class="markdown-dropdown"
data-component-name="{{page.component.name}}"
data-markdown-url="{{markdown-url}}"
>
<button
class="markdown-dropdown-toggle"
aria-haspopup="true"
Expand Down Expand Up @@ -33,6 +37,26 @@
<span class="markdown-copy-toast">Copied!</span>
</button>

{{#if (has-agent-handoff)}}
<button
class="markdown-dropdown-item"
data-action="copy-agent"
role="menuitem"
type="button"
>
<svg aria-hidden="true" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path d="M2.5 3.5h11v9h-11z" stroke="currentColor"/>
<path d="m5 6 2 2-2 2M8.5 10h2.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Copy agent handoff</span>
<span
class="markdown-copy-toast"
data-agent-handoff-status
role="status"
></span>
</button>
{{/if}}

<button
class="markdown-dropdown-item"
data-action="view"
Expand Down
Loading
Loading