mirror of
https://github.com/block/buzz.git
synced 2026-11-01 23:11:58 +00:00
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:
commit
50f615ecbd
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())?
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user