diff --git a/README.md b/README.md
index b3191ce6..c555d9e6 100644
--- a/README.md
+++ b/README.md
@@ -202,6 +202,7 @@ The farm refreshes live over the pub/sub bus (a watered build appears on the own
- **Emoji reactions** - react with **any** emoji on posts, comments, gists, and projects, separate from voting and carrying no ranking weight. A short quick-pick palette covers the common reactions, and a `+` button next to it opens the full searchable emoji picker (every standard emoji, including skin tones), so a reaction is never limited to a preset list. Emoji already used on an item are shown as counted chips beside the palette.
- **Emoji shortcodes** - typing a `:name:` shortcode in any content (posts, comments, titles, project and gist descriptions, news, and direct messages) renders the matching emoji, using the full GitHub/Discord standard set (for example `:rocket:` becomes a rocket). Server-rendered and live content share one shortcode list; unknown names and shortcodes inside code are left untouched. Documented at `/docs/emoji-shortcodes`. This is distinct from the visual emoji-picker button in the composer, which inserts the literal emoji character.
- **Polls** - a post can carry a poll (question plus up to six options); results appear as live bars once the viewer votes, one vote per member. A poll can be attached when the post is created or added later by editing a post that has none.
+- **Paste an image to attach it** - pressing Ctrl+V (Cmd+V) with a screenshot or copied image on the clipboard while writing a post, a comment, a direct message, an issue, a gist, or a project attaches it immediately, with no trip through the file picker. The upload, its limits, and the resulting attachment are identical to picking the file by hand.
- **Bookmarks** - save posts, gists, projects, and news to a personal list at `/bookmarks/saved`.
- **Private projects** - an owner can mark a project private so it is visible only to them (and administrators) and excluded from listings, profiles, search, the sitemap, and zip access. Set at creation or toggled later from the project page.
- **Read-only projects** - an owner can mark a project read-only, making its entire virtual filesystem immutable: every write, edit, line-edit, move, delete, and upload is refused from all paths (the web UI, the HTTP API, the Devii agent, and container workspace sync) until read-only is turned off. Devii may toggle read-only only after the user explicitly confirms.
diff --git a/devplacepy/static/js/CLAUDE.md b/devplacepy/static/js/CLAUDE.md
index 0fc87154..f130c745 100644
--- a/devplacepy/static/js/CLAUDE.md
+++ b/devplacepy/static/js/CLAUDE.md
@@ -104,6 +104,8 @@ Every file-upload UI is the one custom element `dp-upload` (`static/js/component
- `direct` (the project file browser, `project_files.html`): uploads to a custom `endpoint` with `field-name` plus a settable `extraFields` (e.g. `{path}`) and emits `dp-upload:uploaded` / `dp-upload:done` / `dp-upload:error`. `ProjectFiles.uploadTo(dir)` sets `extraFields` then calls `widget.open()`, and refreshes the tree on `done`.
- `field` (create-post image, `feed.html`): wraps a real `` that submits with the form - no AJAX, inline-image flow unchanged.
+**Paste-to-attach is the opt-in `paste` boolean attribute**, not a per-form handler. With it set, `dp-upload` binds ONE `paste` listener on its `closest("form")` and routes the clipboard's image files through the same `handleFiles` path as the picker and the drop target, so validation, limits, the terms gate and the hidden `attachment_uids` field are shared - never re-implement a clipboard reader in a page controller. It is opt-in because a form may hold several upload buttons (`projects.html` has cover + logo beside the attachment one) and a form-wide default would attach one pasted image to all of them; mark exactly the button that owns the form's content attachments. Set on `_attachment_form.html` (so every form including it - post composer, post edit, gists, projects, issues, screenshots - inherits it), `_comment_form.html`, `messages.html`, and the `mode="embed"` skeleton `AppChat` builds. Non-image clipboard payloads fall through untouched, so pasting text still types.
+
The component validates size/type/count and reports errors via `app.toast`. CSS is `.dp-upload-*` in `components.css`; the old `.attachment-upload-*` upload-widget styles were removed, but the `.attachment-gallery`/`.attachment-lightbox` display styles (for already-saved attachments) remain.
## ReportDialog (`ReportDialog.js`, `app.reportDialog`)
diff --git a/devplacepy/static/js/components/AppChat.js b/devplacepy/static/js/components/AppChat.js
index 0967cdd1..560405a9 100644
--- a/devplacepy/static/js/components/AppChat.js
+++ b/devplacepy/static/js/components/AppChat.js
@@ -199,6 +199,7 @@ export class AppChat extends Component {
this.upload = document.createElement("dp-upload");
this.upload.setAttribute("multiple", "");
+ this.upload.setAttribute("paste", "");
this.upload.setAttribute("max-files", String(this.maxAttachments));
this.sendBtn = document.createElement("button");
diff --git a/devplacepy/static/js/components/AppUpload.js b/devplacepy/static/js/components/AppUpload.js
index 69b6e0c6..db50cda2 100644
--- a/devplacepy/static/js/components/AppUpload.js
+++ b/devplacepy/static/js/components/AppUpload.js
@@ -91,6 +91,9 @@ export class AppUpload extends Component {
this.pendingSubmitForm = null;
const form = this.closest("form");
if (form) {
+ if (this.boolAttr("paste")) {
+ form.addEventListener("paste", (event) => this.handlePaste(event));
+ }
form.addEventListener("submit", (event) => {
if (this.busyCount > 0) {
event.preventDefault();
@@ -108,6 +111,19 @@ export class AppUpload extends Component {
this.input.click();
}
+ handlePaste(event) {
+ const data = event.clipboardData;
+ const files = Array.from(data ? data.files : [])
+ .filter((file) => file.type.startsWith("image/"));
+ if (!files.length) {
+ return;
+ }
+ if (!data.getData("text/plain")) {
+ event.preventDefault();
+ }
+ this.handleFiles(files);
+ }
+
clear() {
this.items = [];
if (this.mode === "field") {
diff --git a/devplacepy/templates/_attachment_form.html b/devplacepy/templates/_attachment_form.html
index 7b9d625d..26a72a92 100644
--- a/devplacepy/templates/_attachment_form.html
+++ b/devplacepy/templates/_attachment_form.html
@@ -1,4 +1,4 @@
-