Reviewed feature request issue for copyToClipboard export.
UnderstandExported copyToClipboard function in codebase.
EditUpdated changelog with new export entry.
EditAdded appropriate label to the issue.
EditCommitted and pushed changes to repository.
VerifyYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/issues/926
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
The user wants me to review a GitHub PR. Let me start by fetching the PR details.
>gh pr view 926 --repo badlogic/pi-mono --json title,body,comments,commits,files,author,state,labels,baseRefName,headRefNameGraphQL: Could not resolve to a PullRequest with the number of 926. (repository.pullRequest)
Command exited with code 1>gh pr diff 926 --repo badlogic/pi-monocould not find pull request diff: HTTP 404: Not Found (https://api.github.com/repos/badlogic/pi-mono/pulls/926)
Command exited with code 1It seems that the URL is actually an issue, not a PR. Let me check if it's an issue instead.
The URL points to an issue, not a PR. Let me fetch it as an issue:
>gh issue view 926 --repo badlogic/pi-mono --json title,body,comments,labels,state,author{"author":{"id":"MDQ6VXNlcjczOTY=","is_bot":false,"login":"mitsuhiko","name":"Armin Ronacher"},"body":"### What do you want to change?\n\nI think it would be quite useful for extensions to be able to copy to the clipboard without having to reimplement this. `copyToClipboard` exists but it's not exported.\n\n### Why?\n\nBecause it's useful. \n\n### How? (optional)\n\n_No response_","comments":[],"labels":[],"state":"OPEN","title":"Expose copyToClipboard"}This is an issue (feature request), not a PR. The user asked me to review a PR, but provided an issue URL. Let me clarify this with the user.
Issue #926 (not a PR): "Expose copyToClipboard"
This is a feature request issue by @mitsuhiko, not a pull request.
Summary:
copyToClipboard function so extensions can use itLet me check the current state of copyToClipboard in the codebase:
>rg -l "copyToClipboard" packages/coding-agent/packages/coding-agent/src/utils/clipboard.ts
packages/coding-agent/src/core/export-html/template.js
packages/coding-agent/src/modes/interactive/interactive-mode.ts
packages/coding-agent/test/fixtures/before-compaction.jsonl>rg "export.*copyToClipboard|copyToClipboard.*export" packages/coding-agent/packages/coding-agent/test/fixtures/before-compaction.jsonl:{"type":"message","timestamp":"2025-12-09T00:11:29.396Z","message":{"role":"toolResult","toolCallId":"toolu_01A12mLckzGXWcStqTdvu7iP","toolName":"read","content":[{"type":"text","text":"\t\ttry {\n\t\t\tconst filePath = this.session.exportToHtml(outputPath);\n\t\t\tthis.showStatus(`Session exported to: ${filePath}`);\n\t\t} catch (error: unknown) {\n\t\t\tthis.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\t}\n\n\tprivate handleCopyCommand(): void {\n\t\tconst text = this.session.getLastAssistantText();\n\t\tif (!text) {\n\t\t\tthis.showError(\"No agent messages to copy yet.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tcopyToClipboard(text);\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"Copied last agent message to clipboard\"), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error) {\n\t\t\tthis.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tprivate handleSessionCommand(): void {\n\t\tconst stats = this.session.getSessionStats();\n\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${stats.sessionFile}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${stats.sessionId}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${stats.userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${stats.assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${stats.toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${stats.toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.totalMessages}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Tokens\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Input:\")} ${stats.tokens.input.toLocaleString()}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Output:\")} ${stats.tokens.output.toLocaleString()}\\n`;\n\t\tif (stats.tokens.cacheRead > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Read:\")} ${stats.tokens.cacheRead.toLocaleString()}\\n`;\n\t\t}\n\t\tif (stats.tokens.cacheWrite > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Write:\")} ${stats.tokens.cacheWrite.toLocaleString()}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.tokens.total.toLocaleString()}\\n`;\n\n\t\tif (stats.cost > 0) {\n\t\t\tinfo += `\\n${theme.bold(\"Cost\")}\\n`;\n\t\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.cost.toFixed(4)}`;\n\t\t}\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleChangelogCommand(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\tconst changelogMarkdown =\n\t\t\tallEntries.length > 0\n\t\t\t\t? allEntries\n\t\t\t\t\t\t.reverse()\n\t\t\t\t\t\t.map((e) => e.content)\n\t\t\t\t\t\t.join(\"\\n\\n\")\n\t\t\t\t: \"No changelog entries found.\";\n\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleClearCommand(): Promise<void> {\n\n[179 more lines in file. Use offset=1190 to continue]"}],"isError":false,"timestamp":1765239089389}}
packages/coding-agent/test/fixtures/before-compaction.jsonl:{"type":"message","timestamp":"2025-12-09T00:22:31.652Z","message":{"role":"assistant","content":[{"type":"text","text":"Now let me create DEVELOPMENT.md for the coding-agent package:"},{"type":"toolCall","id":"toolu_016v3zn8gbaRAfkxb1Tv4pLn","name":"write","arguments":{"path":"/Users/badlogic/workspaces/pi-mono/packages/coding-agent/DEVELOPMENT.md","content":"# coding-agent Development Guide\n\nThis document describes the architecture and development workflow for the coding-agent package.\n\n## Architecture Overview\n\nThe coding-agent is structured into distinct layers:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ CLI Layer │\n│ cli.ts → main.ts → cli/args.ts, cli/file-processor.ts │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ Mode Layer │\n│ modes/interactive/ modes/print-mode.ts modes/rpc-mode.ts │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ Core Layer │\n│ core/agent-session.ts (central abstraction) │\n│ core/session-manager.ts, core/model-config.ts, etc. │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ External Dependencies │\n│ @mariozechner/pi-agent-core (Agent, tools) │\n│ @mariozechner/pi-ai (models, providers) │\n│ @mariozechner/pi-tui (TUI components) │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## Directory Structure\n\n```\nsrc/\n├── cli.ts # CLI entry point (shebang, calls main)\n├── main.ts # Main orchestration, argument handling, mode routing\n├── index.ts # Public API exports\n\n├── cli/ # CLI-specific utilities\n│ ├── args.ts # parseArgs(), printHelp(), Args interface\n│ ├── file-processor.ts # processFileArguments() for @file args\n│ └── session-picker.ts # selectSession() TUI for --resume\n\n├── core/ # Core business logic (mode-agnostic)\n│ ├── agent-session.ts # AgentSession class - THE central abstraction\n│ ├── bash-executor.ts # executeBash() with streaming, abort\n│ ├── compaction.ts # Context compaction logic\n│ ├── export-html.ts # exportSession(), exportFromFile()\n│ ├── messages.ts # BashExecutionMessage, messageTransformer\n│ ├── model-config.ts # findModel(), getAvailableModels(), getApiKeyForModel()\n│ ├── model-resolver.ts # resolveModelScope(), restoreModelFromSession()\n│ ├── session-manager.ts # SessionManager class - JSONL persistence\n│ ├── settings-manager.ts # SettingsManager class - user preferences\n│ ├── slash-commands.ts # loadSlashCommands() from ~/.pi/agent/commands/\n│ ├── system-prompt.ts # buildSystemPrompt(), loadProjectContextFiles()\n│ ├── oauth/ # OAuth authentication (Anthropic, etc.)\n│ │ ├── anthropic.ts\n│ │ ├── storage.ts\n│ │ └── index.ts\n│ └── tools/ # Tool implementations\n│ ├── bash.ts, edit.ts, find.ts, grep.ts, ls.ts, read.ts, write.ts\n│ ├── truncate.ts # Output truncation utilities\n│ └── index.ts # Tool exports, allTools, codingTools\n\n├── modes/ # Run mode implementations\n│ ├── index.ts # Re-exports InteractiveMode, runPrintMode, runRpcMode\n│ ├── print-mode.ts # Non-interactive: process messages, print output, exit\n│ ├── rpc-mode.ts # JSON-RPC mode for programmatic control\n│ └── interactive/ # Interactive TUI mode\n│ ├── interactive-mode.ts # InteractiveMode class\n│ ├── components/ # TUI components (editor, selectors, etc.)\n│ │ ├── assistant-message.ts\n│ │ ├── bash-execution.ts\n│ │ ├── custom-editor.ts\n│ │ ├── footer.ts\n│ │ ├── model-selector.ts\n│ │ ├── session-selector.ts\n│ │ └── ... (other selectors)\n│ └── theme/\n│ ├── theme.ts # Theme loading, getEditorTheme(), etc.\n│ ├── dark.json\n│ ├── light.json\n│ └── theme-schema.json\n\n└── utils/ # Generic utilities\n ├── changelog.ts # parseChangelog(), getNewEntries()\n ├── clipboard.ts # copyToClipboard()\n ├── config.ts # APP_NAME, VERSION, paths (getAgentDir, etc.)\n ├── fuzzy.ts # Fuzzy string matching\n ├── shell.ts # getShellConfig()\n └── tools-manager.ts # ensureTool() - download fd, etc.\n```\n\n## Key Abstractions\n\n### AgentSession (core/agent-session.ts)\n\nThe central abstraction that wraps the low-level `Agent` with:\n- Session persistence (via SessionManager)\n- Settings persistence (via SettingsManager)\n- Model cycling with scoped models\n- Context compaction\n- Bash command execution\n- Message queuing\n\nAll three modes (interactive, print, rpc) use AgentSession.\n\n### InteractiveMode (modes/interactive/interactive-mode.ts)\n\nHandles TUI rendering and user interaction:\n- Subscribes to AgentSession events\n- Renders messages, tool executions, streaming\n- Manages editor, selectors, key handlers\n- Delegates all business logic to AgentSession\n\n### SessionManager (core/session-manager.ts)\n\nHandles session persistence:\n- JSONL format for append-only writes\n- Session file location management\n- Message loading/saving\n- Model/thinking level persistence\n\n### SettingsManager (core/settings-manager.ts)\n\nHandles user preferences:\n- Default model/provider\n- Theme selection\n- Queue mode\n- Thinking block visibility\n\n## Development Workflow\n\n### Running in Development\n\n```bash\n# From monorepo root\nnpx tsx packages/coding-agent/src/cli.ts\n\n# With arguments\nnpx tsx packages/coding-agent/src/cli.ts --help\nnpx tsx packages/coding-agent/src/cli.ts -p \"Hello\"\n```\n\n### Type Checking\n\n```bash\n# From monorepo root\nnpm run check\n```\n\n### Building\n\n```bash\n# Build all packages\nnpm run build\n\n# Build standalone binary\ncd packages/coding-agent\nnpm run build:binary\n```\n\n## Adding New Features\n\n### Adding a New Slash Command\n\n1. If it's a UI-only command (e.g., `/theme`), add handler in `interactive-mode.ts` `setupEditorSubmitHandler()`\n2. If it needs session logic, add method to `AgentSession` and call from mode\n\n### Adding a New Tool\n\n1. Create tool in `core/tools/` following existing patterns\n2. Export from `core/tools/index.ts`\n3. Add to `allTools` and optionally `codingTools`\n4. Add description to `toolDescriptions` in `core/system-prompt.ts`\n\n### Adding a New Selector\n\n1. Create component in `modes/interactive/components/`\n2. Use `showSelector()` helper in `interactive-mode.ts`:\n\n```typescript\nprivate showMySelector(): void {\n this.showSelector((done) => {\n const selector = new MySelectorComponent(\n // ... params\n (result) => {\n // Handle selection\n done();\n this.showStatus(`Selected: ${result}`);\n },\n () => {\n done();\n this.ui.requestRender();\n },\n );\n return { component: selector, focus: selector.getSelectList() };\n });\n}\n```\n\n## Testing\n\nThe package uses E2E tests only (no unit tests by design). Tests are in `test/`:\n\n```bash\n# Run tests\nnpm test\n```\n\n## Code Style\n\n- No `any` types unless absolutely necessary\n- No inline dynamic imports\n- Use `showStatus()` for dim status messages\n- Use `showError()` / `showWarning()` for errors/warnings\n- Keep InteractiveMode focused on UI, delegate logic to AgentSession\n"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-5","usage":{"input":1,"output":2269,"cacheRead":129988,"cacheWrite":862,"totalTokens":133120,"cost":{"input":0.000005,"output":0.056725000000000005,"cacheRead":0.064994,"cacheWrite":0.0053875,"total":0.1271115}},"stopReason":"toolUse","timestamp":1765239716696}}
[Showing lines 3-5 of 5 (50.0KB limit). Full output: /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/pi-bash-4644cd3eaa78812f.log]Good, so copyToClipboard exists in packages/coding-agent/src/utils/clipboard.ts. Let me check that file and also check what's currently exported from the package's index.ts.
Now I understand the situation:
copyToClipboard exists in packages/coding-agent/src/utils/clipboard.ts and is already exported from that filepackages/coding-agent/src/index.ts (the main entry point)This means extensions can't import it from the package's public API.
The issue requests that copyToClipboard be exposed in the public API so extensions can use it.
Since the user gave me an issue URL rather than a PR URL, they probably want me to summarize this issue. Let me provide that summary.
Issue #926: "Expose copyToClipboard"
This is a feature request (not a PR). Here's the analysis:
pkg:coding-agent)copyToClipboard from the public API so extensions can use itThe copyToClipboard function exists at:
packages/coding-agent/src/utils/clipboard.tspackages/coding-agent/src/index.tsThis means extensions cannot import it via:
import { copyToClipboard } from "@anthropic-ai/coding-agent";To implement this request:
Add export to packages/coding-agent/src/index.ts:
export { copyToClipboard } from "./utils/clipboard.js";Add changelog entry to packages/coding-agent/CHANGELOG.md:
### Added- Exposed `copyToClipboard` utility for extensions ([#926](https://github.com/badlogic/pi-mono/issues/926) by [@mitsuhiko](https://github.com/mitsuhiko))Consider documentation update in packages/coding-agent/docs/extensions.md if clipboard utilities should be documented for extension authors.
Would you like me to implement this feature request?
implement
The user wants me to implement the feature request from issue #926: expose copyToClipboard from the public API.
I need to:
copyToClipboardnpm run check to verifyLet me start by reading the necessary files.
Now I have all the information I need. Let me implement the feature:
copyToClipboard from packages/coding-agent/src/index.tsThe function is already exported from clipboard.ts, so I just need to re-export it from the main index.
Looking at the index.ts, there's a section for utilities at the end. I'll add the clipboard export there near the shell utilities.
Now I need to run npm run check to verify there are no errors.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 419 files in 262ms. Fixed 1 file.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. Let me also add the pkg:coding-agent label to the issue as per the guidelines.
All checks pass. Let me add the appropriate label to the issue:
>gh issue edit 926 --repo badlogic/pi-mono --add-label "pkg:coding-agent"https://github.com/badlogic/pi-mono/issues/926The implementation is complete. Let me summarize what was done.
Done. Implementation complete:
Changes made:
packages/coding-agent/src/index.ts - Added export:
export { copyToClipboard } from "./utils/clipboard.js";packages/coding-agent/CHANGELOG.md - Added entry under ### Added:
- Exposed `copyToClipboard` utility for extensions ([#926](https://github.com/badlogic/pi-mono/issues/926) by [@mitsuhiko](https://github.com/mitsuhiko))Issue #926 - Added pkg:coding-agent label
Extensions can now import and use the clipboard utility:
import { copyToClipboard } from "@mariozechner/pi-coding-agent";
copyToClipboard("text to copy");>git statusOn branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/coding-agent/CHANGELOG.md
modified: packages/coding-agent/src/index.ts
no changes added to commit (use "git add" and/or "git commit -a")
>git diffdiff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 266fe23e..8a96453f 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -11,6 +11,7 @@
### Added
+- Exposed `copyToClipboard` utility for extensions ([#926](https://github.com/badlogic/pi-mono/issues/926) by [@mitsuhiko](https://github.com/mitsuhiko))
- Skill invocation messages are now collapsible in chat output, showing collapsed by default with skill name and expand hint ([#894](https://github.com/badlogic/pi-mono/issues/894))
- Header values in `models.json` now support environment variables and shell commands, matching `apiKey` resolution ([#909](https://github.com/badlogic/pi-mono/issues/909))
- `markdown.codeBlockIndent` setting to customize code block indentation in rendered output
diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts
index f6509551..5ababa02 100644
--- a/packages/coding-agent/src/index.ts
+++ b/packages/coding-agent/src/index.ts
@@ -290,6 +290,8 @@ export {
Theme,
type ThemeColor,
} from "./modes/interactive/theme/theme.js";
+// Clipboard utilities
+export { copyToClipboard } from "./utils/clipboard.js";
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
// Shell utilities
export { getShellConfig } from "./utils/shell.js";
commit and push
The user wants me to commit and push the changes. I need to:
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/src/index.ts packages/coding-agent/CHANGELOG.md && git commit -m "feat(coding-agent): expose copyToClipboard for extensions
fixes #926" && git pushRunning formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 419 files in 260ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.
✅ All pre-commit checks passed!
[main 1f4da9f0] feat(coding-agent): expose copyToClipboard for extensions
2 files changed, 3 insertions(+)
To https://github.com/badlogic/pi-mono.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://github.com/badlogic/pi-mono.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Command exited with code 1Remote has changes. I need to pull --rebase first, then push.
>cd /Users/badlogic/workspaces/pi-mono && git pull --rebase && git pushFrom https://github.com/badlogic/pi-mono
cd8ce452..72de8f26 main -> origin/main
Rebasing (1/2)
Rebasing (2/2)
Successfully rebased and updated refs/heads/main.
To https://github.com/badlogic/pi-mono.git
72de8f26..48fc57f3 main -> mainCommit and push completed. The issue #926 will be automatically closed because of the "fixes #926" in the commit message.
Committed and pushed. Issue #926 will be automatically closed.
import { copyToClipboard } from "@anthropic-ai/coding-agent";export { copyToClipboard } from "./utils/clipboard.js";### Added- Exposed `copyToClipboard` utility for extensions ([#926](https://github.com/badlogic/pi-mono/issues/926) by [@mitsuhiko](https://github.com/mitsuhiko))export { copyToClipboard } from "./utils/clipboard.js";- Exposed `copyToClipboard` utility for extensions ([#926](https://github.com/badlogic/pi-mono/issues/926) by [@mitsuhiko](https://github.com/mitsuhiko))import { copyToClipboard } from "@mariozechner/pi-coding-agent";
copyToClipboard("text to copy");