Skip to content
Open
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
36 changes: 20 additions & 16 deletions inst/js/app/btw_app.css
Original file line number Diff line number Diff line change
Expand Up @@ -144,42 +144,46 @@ pre:has(.code-action-wrapper) {
cursor: pointer;
}

.code-action-wrapper .code-copy-button {
.code-action-wrapper .code-copy-button,
.code-action-wrapper .btw-block-copy-btn {
position: static;
top: auto;
right: auto;
margin-top: 0;
margin-right: 0;
opacity: 1;
width: auto;
height: auto;
}

.code-action-wrapper > button > .bi::after {
content: "";
display: block;
width: 1rem;
height: 1rem;
background-color: var(--bs-body-color, #222);
}

/* @vscode/codicons: copy, insert-at-cursor, new-file, terminal (MIT) */
.code-copy-button > .bi::after {
.code-copy-button > .bi::after,
.btw-block-copy-btn > .bi::after {
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M3 5V12.73C2.4 12.38 2 11.74 2 11V5C2 2.79 3.79 1 6 1H9C9.74 1 10.38 1.4 10.73 2H6C4.35 2 3 3.35 3 5ZM11 15H6C4.897 15 4 14.103 4 13V5C4 3.897 4.897 3 6 3H11C12.103 3 13 3.897 13 5V13C13 14.103 12.103 15 11 15ZM12 5C12 4.448 11.552 4 11 4H6C5.448 4 5 4.448 5 5V13C5 13.552 5.448 14 6 14H11C11.552 14 12 13.552 12 13V5Z"/></svg>');
}

.btw-block-copy-btn-checked > .bi::after {
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>');
background-color: var(--bs-success, #198754);
}

.insert-at-cursor-button > .bi::after {
content: "";
display: block;
height: 1rem;
width: 1rem;
background-color: var(--bs-body-color, #222);
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M11 6H14C14.551 6 15 5.551 15 5V2C15 1.449 14.551 1 14 1H11C10.449 1 10 1.449 10 2V5C10 5.551 10.449 6 11 6ZM11 5V2H14V5H11Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M11 14H14C14.551 14 15 13.551 15 13V10C15 9.449 14.551 9 14 9H11C10.449 9 10 9.449 10 10V13C10 13.551 10.449 14 11 14ZM11 13V10H14V13H11Z"/><path d="M7.854 5.14602L9.854 7.14602L9.855 7.14502C10.05 7.34002 10.05 7.65702 9.855 7.85202L7.855 9.85202C7.757 9.94902 7.629 9.99802 7.501 9.99802C7.373 9.99802 7.245 9.95002 7.147 9.85202C6.952 9.65702 6.952 9.34002 7.147 9.14502L8.293 7.99902H4V8.99902C4 9.55002 3.551 9.99902 3 9.99902H1C0.724 9.99902 0.5 9.77502 0.5 9.49902C0.5 9.22302 0.724 8.99902 1 8.99902H3V5.99902H1C0.724 5.99902 0.5 5.77502 0.5 5.49902C0.5 5.22302 0.724 4.99902 1 4.99902H3C3.551 4.99902 4 5.44802 4 5.99902V6.99902H8.293L7.147 5.85302C6.952 5.65802 6.952 5.34102 7.147 5.14602C7.342 4.95102 7.659 4.95102 7.854 5.14602Z"/></svg>');
}

.insert-new-file-button > .bi::after {
content: "";
display: block;
height: 1rem;
width: 1rem;
background-color: var(--bs-body-color, #222);
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M5 14C4.448 14 4 13.552 4 13V3C4 2.448 4.448 2 5 2H8V4.5C8 5.328 8.672 6 9.5 6H12V6.025C12.344 6.056 12.677 6.121 13 6.213V5.414C13 5.016 12.842 4.635 12.561 4.353L9.647 1.439C9.366 1.158 8.984 1 8.586 1H5C3.895 1 3 1.895 3 3V13C3 14.105 3.895 15 5 15H7.261C7.008 14.693 6.791 14.357 6.607 14H5ZM9 2.207L11.793 5H9.5C9.224 5 9 4.776 9 4.5V2.207ZM11.5 7C9.015 7 7 9.015 7 11.5C7 13.985 9.015 16 11.5 16C13.985 16 16 13.985 16 11.5C16 9.015 13.985 7 11.5 7ZM14 12H12V14C12 14.276 11.776 14.5 11.5 14.5C11.224 14.5 11 14.276 11 14V12H9C8.724 12 8.5 11.776 8.5 11.5C8.5 11.224 8.724 11 9 11H11V9C11 8.724 11.224 8.5 11.5 8.5C11.776 8.5 12 8.724 12 9V11H14C14.276 11 14.5 11.224 14.5 11.5C14.5 11.776 14.276 12 14 12Z"/></svg>');
}

.insert-console-button > .bi::after {
content: "";
display: block;
height: 1rem;
width: 1rem;
background-color: var(--bs-body-color, #222);
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M18.75 1.5H5.25C3.1815 1.5 1.5 3.183 1.5 5.25V18.75C1.5 20.8185 3.1815 22.5 5.25 22.5H18.75C20.8185 22.5 22.5 20.8185 22.5 18.75V5.25C22.5 3.183 20.8185 1.5 18.75 1.5ZM21 18.75C21 19.9905 19.9905 21 18.75 21H5.25C4.0095 21 3 19.9905 3 18.75V5.25C3 4.0095 4.0095 3 5.25 3H18.75C19.9905 3 21 4.0095 21 5.25V18.75ZM10.281 13.281L5.781 17.781C5.634 17.928 5.442 18 5.25 18C5.058 18 4.866 17.9265 4.719 17.781C4.4265 17.4885 4.4265 17.013 4.719 16.7205L8.688 12.7515L4.719 8.7825C4.4265 8.49 4.4265 8.0145 4.719 7.722C5.0115 7.4295 5.487 7.4295 5.7795 7.722L10.2795 12.222C10.572 12.5145 10.572 12.99 10.2795 13.2825L10.281 13.281ZM19.5 17.25C19.5 17.664 19.164 18 18.75 18H11.25C10.836 18 10.5 17.664 10.5 17.25C10.5 16.836 10.836 16.5 11.25 16.5H18.75C19.164 16.5 19.5 16.836 19.5 17.25Z"/></svg>');
}
37 changes: 33 additions & 4 deletions inst/js/app/btw_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,21 @@ const inIframe = window.self !== window.top
const inIDE = document.querySelector(".btw-in-ide") !== null

if (inIframe && inIDE) {
// Matches both old Lit (shiny-markdown-stream) and new React (.shiny-chat-message-content)
const STREAM_SELECTOR = "shiny-markdown-stream, .shiny-chat-message-content"

const stopObserving = observeShinyMarkdownStream((streamEl) => {
enhanceCodeActions(streamEl)
})

// New React shinychat: btw-run-r-result dispatches this event after rendering
// its per-block copy buttons. Add IDE action buttons to source code blocks only.
document.addEventListener("btw-run-r-rendered", (e) => {
enhanceBtwCodeActions(e.target)
})

function isMarkdownStream(el) {
return el.matches("shiny-markdown-stream")
return el.matches(STREAM_SELECTOR)
}

function canEnhance(el) {
Expand All @@ -107,6 +116,11 @@ if (inIframe && inIDE) {
function attachObserver(el) {
if (!isMarkdownStream(el) || observers.has(el)) return

// Enhance any content already in the element. In React shinychat the
// element arrives fully populated in one commit, so the per-element
// observer below would never fire for that initial content.
callback(el)

let timeoutId = null

const observer = new MutationObserver(() => {
Expand All @@ -123,7 +137,7 @@ if (inIframe && inIDE) {
observers.set(el, observer)
}

document.querySelectorAll("shiny-markdown-stream").forEach(attachObserver)
document.querySelectorAll(STREAM_SELECTOR).forEach(attachObserver)

const rootObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Expand All @@ -135,7 +149,7 @@ if (inIframe && inIDE) {
}

node
.querySelectorAll?.("shiny-markdown-stream")
.querySelectorAll?.(STREAM_SELECTOR)
.forEach(attachObserver)
})
})
Expand All @@ -159,8 +173,23 @@ if (inIframe && inIDE) {
if (!pre || !canEnhance(pre)) return

const wrapper = ensureWrapper(pre)
moveCopyButton(wrapper, copyButton)
installActionButtons(wrapper, pre)
moveCopyButton(wrapper, copyButton)
})
}

function enhanceBtwCodeActions(result) {
result.querySelectorAll(".btw-block-copy-btn").forEach((copyButton) => {
const pre = copyButton.closest("pre")
if (!pre) return

const wrapper = ensureWrapper(pre)

// Install IDE action buttons first, then copy button so it appears last
if (pre.closest(".btw-output-source")) {
installActionButtons(wrapper, pre)
}
moveCopyButton(wrapper, copyButton)
})
}

Expand Down
53 changes: 51 additions & 2 deletions inst/js/run-r/btw-run-r.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ btw-run-r-result .card-body {
border: none;
border-radius: 0;
overflow-x: auto;
position: relative;
}

.btw-run-output pre:not(.btw-output-source) {
Expand All @@ -41,11 +42,59 @@ btw-run-r-result .card-body {
background-color: var(--bs-black);
}

.btw-run-output > :not(.btw-output-source) .code-copy-button {
/* TODO: Figure out how to disable markdown-stream code copy button */
.btw-run-output .code-copy-button {
/* Hide shinychat's pipeline copy button — btw adds its own via addBlockCopyButtons() */
display: none;
}

/* Per-block copy buttons */
.btw-block-copy-btn {
position: absolute;
top: 0.25rem;
right: 0.25rem;
width: 1.5rem;
height: 1.5rem;
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--bs-body-color);
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
}

.btw-run-output pre:hover .btw-block-copy-btn {
opacity: 0.6;
}

.btw-block-copy-btn:hover {
opacity: 1 !important;
}

.btw-block-copy-btn:focus {
outline: 2px solid var(--bs-primary);
outline-offset: 2px;
opacity: 1;
}

.btw-block-copy-btn > .bi::after {
content: "";
display: block;
width: 1rem;
height: 1rem;
background-color: var(--bs-body-color, #222);
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M3 5V12.73C2.4 12.38 2 11.74 2 11V5C2 2.79 3.79 1 6 1H9C9.74 1 10.38 1.4 10.73 2H6C4.35 2 3 3.35 3 5ZM11 15H6C4.897 15 4 14.103 4 13V5C4 3.897 4.897 3 6 3H11C12.103 3 13 3.897 13 5V13C13 14.103 12.103 15 11 15ZM12 5C12 4.448 11.552 4 11 4H6C5.448 4 5 4.448 5 5V13C5 13.552 5.448 14 6 14H11C11.552 14 12 13.552 12 13V5Z"/></svg>');
}

.btw-block-copy-btn-checked > .bi::after {
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>');
background-color: var(--bs-success, #198754);
}

.btw-run-output pre code {
font-family: var(--bs-font-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
white-space: pre;
Expand Down
39 changes: 39 additions & 0 deletions inst/js/run-r/btw-run-r.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ class BtwRunRResult extends HTMLElement {
}
}

this.addBlockCopyButtons()

// Allow clicking anywhere on the header to toggle, except on action buttons
const header = this.querySelector(".card-header")
if (header) {
Expand All @@ -348,6 +350,43 @@ class BtwRunRResult extends HTMLElement {
}
}

/**
* Add per-block copy buttons to each pre element in the output container.
* Idempotent: skips any pre that already has a button (safe to call after re-render).
*/
addBlockCopyButtons() {
const outputContainer = this.querySelector(".btw-run-output")
if (!outputContainer) return

outputContainer.querySelectorAll("pre").forEach((pre) => {
if (pre.querySelector(".btw-block-copy-btn")) return

const btn = document.createElement("button")
btn.className = "btw-block-copy-btn"
btn.setAttribute("aria-label", "Copy to clipboard")
btn.innerHTML = '<i class="bi" aria-hidden="true"></i>'

btn.addEventListener("click", async (e) => {
e.stopPropagation()
const code = pre.querySelector("code")
const text = code?.textContent ?? pre.textContent ?? ""
try {
await copyToClipboard(text)
btn.classList.add("btw-block-copy-btn-checked")
setTimeout(() => {
btn.classList.remove("btw-block-copy-btn-checked")
}, 1500)
} catch (err) {
console.error("Failed to copy:", err)
}
})

pre.appendChild(btn)
})

this.dispatchEvent(new CustomEvent("btw-run-r-rendered", { bubbles: true }))
}

/**
* Escape a string for use in an HTML attribute
* @param {string} str
Expand Down
Loading