Merge remote-tracking branch 'origin/main' into duncan/workspace-scoped-agent-store

* origin/main:
  chore(release): release Buzz Desktop version 0.5.5 (#4809)
  feat: paste composer text without formatting (#4801)
  Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808)

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-08-04 20:33:54 -04:00
9 changed files with 151 additions and 20 deletions

View File

@@ -1,8 +1,8 @@
{
"schema": 1,
"version": "0.5.5",
"base_sha": "4a2305170eef565bf1836e2859247e67c030f8af",
"base_sha": "25a9cf1be6d245fbd7373cb1160dbc790baf5bd5",
"previous_tag": "desktop-v0.5.4",
"tag": "desktop-v0.5.5",
"commit_count": 41
"commit_count": 44
}

View File

@@ -4,6 +4,9 @@
### Desktop and shared changes
- feat: paste composer text without formatting ([#4801](https://github.com/block/buzz/pull/4801)) ([`25a9cf1be6d245fbd7373cb1160dbc790baf5bd5`](https://github.com/block/buzz/commit/25a9cf1be6d245fbd7373cb1160dbc790baf5bd5))
- Revert "chore(release): release Buzz Desktop version 0.5.5" ([#4808](https://github.com/block/buzz/pull/4808)) ([`79c52166cfe6b6d36bdc7686f943595c74e2f578`](https://github.com/block/buzz/commit/79c52166cfe6b6d36bdc7686f943595c74e2f578))
- chore(release): release Buzz Desktop version 0.5.5 ([#4800](https://github.com/block/buzz/pull/4800)) ([`a0ed13de14ee64dd90c32335790f7d3b4e94330d`](https://github.com/block/buzz/commit/a0ed13de14ee64dd90c32335790f7d3b4e94330d))
- fix: reauthenticate databricks model discovery ([#4008](https://github.com/block/buzz/pull/4008)) ([`4a2305170eef565bf1836e2859247e67c030f8af`](https://github.com/block/buzz/commit/4a2305170eef565bf1836e2859247e67c030f8af))
- Revert "chore(release): release Buzz Desktop version 0.5.5" ([#4797](https://github.com/block/buzz/pull/4797)) ([`8faf09f9aedb4989e57c7b6c5bd1052a444a3370`](https://github.com/block/buzz/commit/8faf09f9aedb4989e57c7b6c5bd1052a444a3370))
- feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues ([#4695](https://github.com/block/buzz/pull/4695)) ([`a1d78f2959b41c63f063ff818076d38c31071a47`](https://github.com/block/buzz/commit/a1d78f2959b41c63f063ff818076d38c31071a47))

View File

@@ -34,3 +34,22 @@ pub fn with_clipboard<T>(
operation(stored.as_mut().expect("clipboard initialized"))
.map_err(|e| format!("clipboard error: {e}"))
}
/// Read plain text from the system clipboard through the native shell.
///
/// Browser clipboard reads are permission-gated or unavailable in embedded
/// webviews. Arboard provides one consistent path across WKWebView, WebView2,
/// and WebKitGTK. The operation runs on the main thread for macOS/AppKit safety.
#[tauri::command]
pub async fn read_clipboard_text(app: tauri::AppHandle) -> Result<String, String> {
let (tx, rx) = std::sync::mpsc::sync_channel::<Result<String, String>>(1);
let clipboard_app = app.clone();
app.run_on_main_thread(move || {
let result = with_clipboard(&clipboard_app, arboard::Clipboard::get_text);
let _ = tx.send(result);
})
.map_err(|e| format!("main thread dispatch failed: {e}"))?;
rx.recv()
.map_err(|_| "clipboard result channel closed unexpectedly".to_string())?
}

View File

@@ -724,6 +724,7 @@ pub fn run() {
fetch_media_bytes,
copy_image_to_clipboard,
copy_text_to_clipboard,
read_clipboard_text,
fetch_snapshot_bytes,
relay_requires_membership,
list_relay_members,

View File

@@ -358,15 +358,12 @@ fn hydrate_keys(records: &mut [ManagedAgentRecord]) {
// Without this guard `load_managed_agents_at` blocks on a macOS Security
// daemon IPC call (`SecKeychainFindGenericPassword`) which hangs in
// headless test environments.
#[cfg(test)]
{
let _ = records;
return;
#[cfg(not(test))]
if let Some(store) = agent_secret_store() {
hydrate_keys_with(store, records);
}
let Some(store) = agent_secret_store() else {
return;
};
hydrate_keys_with(store, records);
#[cfg(test)]
let _ = records;
}
/// Testable core of [`hydrate_keys`], generic over the [`KeyStore`] seam.
@@ -544,16 +541,12 @@ fn persist_agent_keys(records: &mut [ManagedAgentRecord]) {
// empty `private_key_nsec` where the keyring write is a no-op anyway.
// Without this guard `save_managed_agents_at` blocks on a macOS Security
// daemon IPC write call in headless test environments.
#[cfg(test)]
{
let _ = records;
return;
#[cfg(not(test))]
if let Some(store) = agent_secret_store() {
persist_agent_keys_with(store, records);
}
let Some(store) = agent_secret_store() else {
// No keyring backend: keys stay inline.
return;
};
persist_agent_keys_with(store, records);
#[cfg(test)]
let _ = records;
}
/// Testable core of [`persist_agent_keys`], generic over the [`KeyStore`] seam.

View File

@@ -9,6 +9,7 @@ import { Extension, type KeyboardShortcutCommand } from "@tiptap/core";
import { Plugin, Selection, TextSelection } from "@tiptap/pm/state";
import type { ResolvedPos } from "@tiptap/pm/model";
import { readTextFromSystemClipboard } from "@/shared/api/tauriMedia";
import {
hasPrimaryShortcutModifier,
isMacPlatform,
@@ -532,6 +533,39 @@ export function useRichTextEditor({
return true;
}
// Cmd+Shift+V / Ctrl+Shift+V → paste the clipboard's plain-text
// representation. Embedded webviews permission-gate the browser
// clipboard API differently across operating systems, so packaged
// builds read through the native arboard command. Browser builds use
// navigator.clipboard as a fallback. Feed the result through
// ProseMirror's paste pipeline with clipboardData populated so its
// plain-text observers keep normal paste behavior.
if (
event.key.toLowerCase() === "v" &&
hasPrimaryShortcutModifier(event) &&
event.shiftKey &&
!event.altKey &&
!event.repeat &&
!event.isComposing
) {
event.preventDefault();
void readTextFromSystemClipboard()
.then((text) => {
const clipboardData = new DataTransfer();
clipboardData.setData("text/plain", text);
view.pasteText(
text,
new ClipboardEvent("paste", { clipboardData }),
);
})
.catch(() => {
// The key is already consumed. Letting a delayed native paste
// race the asynchronous read could duplicate or unexpectedly
// format content.
});
return true;
}
// ⌘K / Ctrl+K → link editor. The formatting toolbar has always
// advertised this shortcut on its link button; bind it here so it
// actually works. Kept alongside the ArrowUp handling below rather

View File

@@ -1,4 +1,4 @@
import { invoke as invokeTauriRaw } from "@tauri-apps/api/core";
import { invoke as invokeTauriRaw, isTauri } from "@tauri-apps/api/core";
import { type BlobDescriptor, invokeTauri } from "./tauri";
function encodeRawIpcHeader(value: string): string {
@@ -66,6 +66,23 @@ export async function fetchMediaBytes(
return new Uint8Array(bytes);
}
/** Read plain text without depending on embedded-webview clipboard grants. */
export async function readTextFromSystemClipboard(): Promise<string> {
// E2E installs Tauri's mocked IPC surface in a browser page, where the SDK's
// `isTauri()` marker remains false. Exercise the packaged-app command path in
// that build so tests detect accidental regressions to permission-gated DOM
// clipboard reads.
if (isTauri() || import.meta.env.MODE === "e2e") {
return invokeTauri<string>("read_clipboard_text");
}
const clipboard = navigator.clipboard;
if (!clipboard?.readText) {
throw new Error("Clipboard text reading is unavailable");
}
return clipboard.readText();
}
/** Write text through the native clipboard after an asynchronous workflow. */
export async function copyTextToSystemClipboard(
text: string,

View File

@@ -12531,6 +12531,8 @@ export function maybeInstallE2eTauriMocks() {
case "copy_text_to_clipboard":
await navigator.clipboard.writeText((payload as { text: string }).text);
return;
case "read_clipboard_text":
return navigator.clipboard.readText();
case "get_event":
return handleGetEvent(
payload as Parameters<typeof handleGetEvent>[0],

View File

@@ -177,6 +177,68 @@ async function applyCaretFormat(
await page.getByRole("button", { name: label, exact: true }).click();
}
for (const platform of [
{ name: "macOS", navigatorPlatform: "MacIntel", shortcut: "Meta+Shift+V" },
{
name: "Windows/Linux",
navigatorPlatform: "Win32",
shortcut: "Control+Shift+V",
},
]) {
test(`pastes rich clipboard content without formatting on ${platform.name}`, async ({
page,
}) => {
await page.addInitScript((navigatorPlatform) => {
Object.defineProperty(navigator, "platform", {
configurable: true,
value: navigatorPlatform,
});
}, platform.navigatorPlatform);
await page
.context()
.grantPermissions(["clipboard-read", "clipboard-write"], {
origin: "http://127.0.0.1:4173",
});
await openGeneral(page);
await page.evaluate(async () => {
await navigator.clipboard.write([
new ClipboardItem({
"text/html": new Blob(
[
'<p><strong>Bold</strong> and <a href="https://example.com">linked</a></p><ul><li>list item</li></ul>',
],
{ type: "text/html" },
),
"text/plain": new Blob(["Bold and linked\nlist item"], {
type: "text/plain",
}),
}),
]);
});
const input = page.getByTestId("message-input");
await input.click();
await page.keyboard.press(platform.shortcut);
await expect
.poll(() =>
page.evaluate(
() =>
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
.__BUZZ_E2E_COMMANDS__ ?? [],
),
)
.toContain("read_clipboard_text");
await expect(input).toHaveText("Bold and linkedlist item");
await expect(input.locator("strong, a, ul, li")).toHaveCount(0);
await expect(input.locator(":scope > p")).toHaveText([
"Bold and linked",
"list item",
]);
});
}
for (const format of [
{ label: "Code block", selector: "pre" },
{ label: "Bullet list", selector: "ul" },