Add copy buttons to code blocks and tool input and output
This commit is contained in:
Vendored
+17
@@ -26,6 +26,23 @@ export function replace(el, ...children) {
|
|||||||
el.replaceChildren();
|
el.replaceChildren();
|
||||||
append(el, ...children);
|
append(el, ...children);
|
||||||
}
|
}
|
||||||
|
export function copyable(pre) {
|
||||||
|
const button = h("button", { type: "button", class: "copy", title: "Copy to the clipboard" }, "copy");
|
||||||
|
button.onclick = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(pre.textContent ?? "");
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
void showAlert(errorText(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.textContent = "copied";
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = "copy";
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
return h("div", { class: "copyable" }, pre, button);
|
||||||
|
}
|
||||||
export function ago(iso) {
|
export function ago(iso) {
|
||||||
const seconds = Math.round((Date.now() - Date.parse(iso)) / 1000);
|
const seconds = Math.round((Date.now() - Date.parse(iso)) / 1000);
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { h } from "./dom.js";
|
import { copyable, h } from "./dom.js";
|
||||||
import { diagram } from "./mermaid.js";
|
import { diagram } from "./mermaid.js";
|
||||||
import { taskStyle } from "./taskcolor.js";
|
import { taskStyle } from "./taskcolor.js";
|
||||||
const choiceMarker = /^(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$/;
|
const choiceMarker = /^(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$/;
|
||||||
@@ -37,7 +37,7 @@ function blocks(lines, opts) {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
out.push(f[2].toLowerCase() === "mermaid" ? diagram(body.join("\n")) : h("pre", { class: "md-code" }, h("code", {}, body.join("\n"))));
|
out.push(f[2].toLowerCase() === "mermaid" ? diagram(body.join("\n")) : copyable(h("pre", { class: "md-code" }, h("code", {}, body.join("\n")))));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (detailsClose.test(line)) {
|
if (detailsClose.test(line)) {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { get, post } from "./api.js";
|
import { get, post } from "./api.js";
|
||||||
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||||
import { markdown } from "./markdown.js";
|
import { markdown } from "./markdown.js";
|
||||||
import { previewURL } from "./preview.js";
|
import { previewURL } from "./preview.js";
|
||||||
import { reviewCounts } from "./reviews.js";
|
import { reviewCounts } from "./reviews.js";
|
||||||
@@ -717,7 +717,7 @@ function subagentTag(parent, ctx) {
|
|||||||
function renderToolCall(p, ctx) {
|
function renderToolCall(p, ctx) {
|
||||||
const result = h("div", { class: "tool-result pending" }, "running…");
|
const result = h("div", { class: "tool-result pending" }, "running…");
|
||||||
ctx.tools.set(String(p["id"]), result);
|
ctx.tools.set(String(p["id"]), result);
|
||||||
return h("details", { class: "tool" }, h("summary", {}, subagentTag(parentOf(p), ctx), h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root()))), h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2)), result);
|
return h("details", { class: "tool" }, h("summary", {}, subagentTag(parentOf(p), ctx), h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root()))), copyable(h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2))), result);
|
||||||
}
|
}
|
||||||
function isSubagentCall(p) {
|
function isSubagentCall(p) {
|
||||||
return p["name"] === "Agent" || p["name"] === "Task";
|
return p["name"] === "Agent" || p["name"] === "Task";
|
||||||
@@ -833,7 +833,7 @@ function renderEvent(ev, ctx) {
|
|||||||
return h("details", { class: "thinking" }, h("summary", {}, subagentTag(parentOf(p), ctx), "thinking"), markdown(String(p["text"]), "text md", md));
|
return h("details", { class: "thinking" }, h("summary", {}, subagentTag(parentOf(p), ctx), "thinking"), markdown(String(p["text"]), "text md", md));
|
||||||
case "agent.tool_result": {
|
case "agent.tool_result": {
|
||||||
const target = ctx.tools.get(String(p["tool_use_id"]));
|
const target = ctx.tools.get(String(p["tool_use_id"]));
|
||||||
const body = h("pre", {}, resultText(p["content"]));
|
const body = copyable(h("pre", {}, resultText(p["content"])));
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return h("div", { class: "tool-result orphan" }, body);
|
return h("div", { class: "tool-result orphan" }, body);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -39,6 +39,23 @@ export function replace(el: Element, ...children: Child[]): void {
|
|||||||
append(el, ...children);
|
append(el, ...children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copyable(pre: HTMLPreElement): HTMLElement {
|
||||||
|
const button = h("button", { type: "button", class: "copy", title: "Copy to the clipboard" }, "copy");
|
||||||
|
button.onclick = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(pre.textContent ?? "");
|
||||||
|
} catch (err) {
|
||||||
|
void showAlert(errorText(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.textContent = "copied";
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = "copy";
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
return h("div", { class: "copyable" }, pre, button);
|
||||||
|
}
|
||||||
|
|
||||||
export function ago(iso: string): string {
|
export function ago(iso: string): string {
|
||||||
const seconds = Math.round((Date.now() - Date.parse(iso)) / 1000);
|
const seconds = Math.round((Date.now() - Date.parse(iso)) / 1000);
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { h } from "./dom.js";
|
import { copyable, h } from "./dom.js";
|
||||||
import { diagram } from "./mermaid.js";
|
import { diagram } from "./mermaid.js";
|
||||||
import { taskStyle } from "./taskcolor.js";
|
import { taskStyle } from "./taskcolor.js";
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ function blocks(lines: string[], opts: Options): Block[] {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
out.push(f[2]!.toLowerCase() === "mermaid" ? diagram(body.join("\n")) : h("pre", { class: "md-code" }, h("code", {}, body.join("\n"))));
|
out.push(f[2]!.toLowerCase() === "mermaid" ? diagram(body.join("\n")) : copyable(h("pre", { class: "md-code" }, h("code", {}, body.join("\n")))));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { get, post, type AgentSession, type ByreEvent, type Participant, type Schemas, type Snapshot } from "./api.js";
|
import { get, post, type AgentSession, type ByreEvent, type Participant, type Schemas, type Snapshot } from "./api.js";
|
||||||
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||||
import { markdown } from "./markdown.js";
|
import { markdown } from "./markdown.js";
|
||||||
import { previewURL } from "./preview.js";
|
import { previewURL } from "./preview.js";
|
||||||
import { reviewCounts } from "./reviews.js";
|
import { reviewCounts } from "./reviews.js";
|
||||||
@@ -837,7 +837,7 @@ function renderToolCall(p: Record<string, unknown>, ctx: Ctx): HTMLElement {
|
|||||||
"details",
|
"details",
|
||||||
{ class: "tool" },
|
{ class: "tool" },
|
||||||
h("summary", {}, subagentTag(parentOf(p), ctx), h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root()))),
|
h("summary", {}, subagentTag(parentOf(p), ctx), h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root()))),
|
||||||
h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2)),
|
copyable(h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2))),
|
||||||
result,
|
result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -982,7 +982,7 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
|||||||
|
|
||||||
case "agent.tool_result": {
|
case "agent.tool_result": {
|
||||||
const target = ctx.tools.get(String(p["tool_use_id"]));
|
const target = ctx.tools.get(String(p["tool_use_id"]));
|
||||||
const body = h("pre", {}, resultText(p["content"]));
|
const body = copyable(h("pre", {}, resultText(p["content"])));
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return h("div", { class: "tool-result orphan" }, body);
|
return h("div", { class: "tool-result orphan" }, body);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-1
@@ -625,6 +625,7 @@ button.compact.danger {
|
|||||||
.md ol,
|
.md ol,
|
||||||
.md blockquote,
|
.md blockquote,
|
||||||
.md pre,
|
.md pre,
|
||||||
|
.md .copyable,
|
||||||
.md .md-table {
|
.md .md-table {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
@@ -670,6 +671,7 @@ button.compact.danger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md pre.md-code {
|
.md pre.md-code {
|
||||||
|
margin: 0;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -822,11 +824,42 @@ button.compact.danger {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin: 6px 0;
|
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool .copyable,
|
||||||
|
.tool-result .copyable {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyable {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyable .copy {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--raised);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyable:hover .copy,
|
||||||
|
.copyable .copy:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.copyable .copy {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tool-result.pending {
|
.tool-result.pending {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|||||||
Reference in New Issue
Block a user