feat: add native clipboard support for screenshot paste (#67)
CI / Lint & Test (push) Successful in 14m34s
CI / Build Linux (push) Successful in 18m19s
CI / Build Windows (cross-compile) (push) Successful in 27m57s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m3s

## Summary
- Adds Tauri clipboard-manager plugin to read images from native clipboard
- Falls back to native clipboard when WebView clipboard API returns empty (fixes screenshot paste)
- Allows sending messages with just attachments (no text required)
- Logs attached files to output with 📎 emoji

## Test plan
- [ ] Build and run the app natively on Windows
- [ ] Copy a screenshot (Win+Shift+S) and paste in the chat input
- [ ] Verify the screenshot appears as an attachment preview
- [ ] Send the attachment and verify Claude receives the file path
- [ ] Test sending a message with only an attachment (no text)
- [ ] Verify the 📎 log line shows the attached filename

**Note:** Paste will not work in WSLg dev environment due to clipboard isolation - needs native Windows build to test.

 This PR was created with help from Hikari~ 🌸

Co-authored-by: Hikari <hikari@nhcarrigan.com>
Reviewed-on: #67
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #67.
This commit is contained in:
2026-01-25 13:08:38 -08:00
committed by Naomi Carrigan
parent bbeff7ae2e
commit 852a4d6661
15 changed files with 1397 additions and 19 deletions
+8
View File
@@ -25,6 +25,7 @@ export const claudeStore = {
isProcessing: conversationsStore.isProcessing,
grantedTools: conversationsStore.grantedTools,
pendingRetryMessage: conversationsStore.pendingRetryMessage,
attachments: conversationsStore.attachments,
// New conversation-aware subscriptions
conversations: conversationsStore.conversations,
@@ -67,6 +68,12 @@ export const claudeStore = {
saveScrollPosition: conversationsStore.saveScrollPosition,
getScrollPosition: conversationsStore.getScrollPosition,
// Attachment management
addAttachment: conversationsStore.addAttachment,
removeAttachment: conversationsStore.removeAttachment,
clearAttachments: conversationsStore.clearAttachments,
getAttachments: conversationsStore.getAttachments,
getGrantedTools: (): string[] => {
let tools: string[] = [];
conversationsStore.grantedTools.subscribe((t) => (tools = Array.from(t)))();
@@ -86,6 +93,7 @@ export const claudeStore = {
conversationsStore.setWorkingDirectory("");
conversationsStore.setProcessing(false);
conversationsStore.revokeAllTools();
conversationsStore.clearAttachments();
// Also clear history restoration
clearHistoryRestore();
},
+60 -3
View File
@@ -4,6 +4,7 @@ import type {
ConnectionStatus,
PermissionRequest,
UserQuestionEvent,
Attachment,
} from "$lib/types/messages";
import type { CharacterState } from "$lib/types/states";
import { cleanupConversationTracking } from "$lib/tauri";
@@ -24,6 +25,7 @@ export interface Conversation {
scrollPosition: number;
createdAt: Date;
lastActivityAt: Date;
attachments: Attachment[];
}
function createConversationsStore() {
@@ -59,6 +61,7 @@ function createConversationsStore() {
scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll)
createdAt: new Date(),
lastActivityAt: new Date(),
attachments: [],
};
}
@@ -109,6 +112,7 @@ function createConversationsStore() {
);
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
const attachments = derived(activeConversation, ($conv) => $conv?.attachments || []);
return {
// Expose derived stores for compatibility
@@ -122,6 +126,7 @@ function createConversationsStore() {
grantedTools: { subscribe: grantedTools.subscribe },
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
scrollPosition: { subscribe: scrollPosition.subscribe },
attachments: { subscribe: attachments.subscribe },
// New conversation-specific stores
conversations: { subscribe: conversations.subscribe },
@@ -272,7 +277,7 @@ function createConversationsStore() {
return newConv.id;
},
deleteConversation: (id: string) => {
deleteConversation: async (id: string) => {
ensureInitialized();
const convs = get(conversations);
const activeId = get(activeConversationId);
@@ -282,8 +287,8 @@ function createConversationsStore() {
return false;
}
// Clean up tracking for this conversation
cleanupConversationTracking(id);
// Clean up tracking for this conversation (including temp files)
await cleanupConversationTracking(id);
conversations.update((c) => {
c.delete(id);
@@ -571,6 +576,58 @@ function createConversationsStore() {
return conv?.grantedTools.has(toolName) || false;
},
// Attachment management
addAttachment: (attachment: Attachment) => {
const activeId = get(activeConversationId);
if (!activeId) return;
conversations.update((convs) => {
const conv = convs.get(activeId);
if (conv) {
conv.attachments.push(attachment);
conv.lastActivityAt = new Date();
}
return convs;
});
},
removeAttachment: (id: string) => {
const activeId = get(activeConversationId);
if (!activeId) return;
conversations.update((convs) => {
const conv = convs.get(activeId);
if (conv) {
conv.attachments = conv.attachments.filter((a) => a.id !== id);
conv.lastActivityAt = new Date();
}
return convs;
});
},
clearAttachments: () => {
const activeId = get(activeConversationId);
if (!activeId) return;
conversations.update((convs) => {
const conv = convs.get(activeId);
if (conv) {
conv.attachments = [];
conv.lastActivityAt = new Date();
}
return convs;
});
},
getAttachments: (): Attachment[] => {
const activeId = get(activeConversationId);
if (!activeId) return [];
const convs = get(conversations);
const conv = convs.get(activeId);
return conv?.attachments || [];
},
// Add initialization helper
initialize: () => {
ensureInitialized();