Repository navigation
fix(window): restore position before size and never fall back to a sub-minimum window - #2405
Conversation
…b-minimum window Restore now moves the main window before resizing it, as tauri-plugin-window-state does, so a scale change on the move (Windows DPI change) cannot undo the saved physical size. When the saved monitor is gone, a physical size saved at another scale can fall below minWidth/minHeight at the fallback monitor's scale. The window then opened clamped and re-saved the smaller size on every launch. Use the configured default size in that case. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
…located geometry in its logical units A rect saved on a 1x external to the right of a 2x laptop can still fall inside the laptop's physical rect after the external is unplugged, so it counted as reachable and skipped the minimum check. With a single monitor its scale is known, so apply the check there too. macOS and GTK setters convert physical values with the window's current scale, so a relocated window planned at the fallback monitor's scale is now sent in that monitor's logical units. Windows keeps physical values. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Replace the single-monitor special case with one rule: a monitor only holds the saved rect if the saved size meets the minimum at that monitor's scale. A smaller size was saved at a lower scale, so with two 2x monitors left after unplugging a 1x external the window is now relocated with the default size instead of opening clamped. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
…m rounding Keep header visibility geometric again and check the minimum separately: at least one monitor under the header must have a scale at which the saved size fits. A 1x window spanning onto a 2x laptop stays where it was. Compare against the minimum converted to pixels with tao's rounding, so a minimum-size window under a custom Windows scale is not reset. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
…s minimum rounding" This reverts commit 7e2393e. Signed-off-by: Cheng Peng <cheng@block.xyz>
…scale" This reverts commit 7de4895. Signed-off-by: Cheng Peng <cheng@block.xyz>
…e fix to the core cases Move the sub-minimum check into restore_request. It converts the configured minimum to pixels at the fallback monitor's scale with the rounding tao uses, so a lone monitor at a custom Windows scale keeps a window that is exactly at the minimum. reachable_client now only gets the default size to use, and the setters send a relocated window in logical units through one branch. Behavior for the unplugged external monitor case is unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
On Windows, tao applies minWidth/minHeight to the undecorated frame and the shadow insets come off the client, so the plugin saves a client a few pixels below the minimum at the same scale. The lone-monitor rule then reset that window to the default size on every launch. Treat a size as saved at a lower scale only when it is more than 8% below the minimum at the fallback's scale. The insets are about 2 to 3%; standard scale steps are at least 11% apart, and macOS and GTK enforce the minimum on the client. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Replace the single-monitor special case with one rule: a monitor only holds the saved rect if the saved size meets the minimum at that monitor's scale. A smaller size was saved at a lower scale, so with two 2x monitors left after unplugging a 1x external the window is now relocated with the default size instead of opening clamped. (cherry picked from commit 7de4895, resolved onto the restructured restore_request / reachable_client from block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Keep header visibility geometric again and check the minimum separately: at least one monitor under the header must have a scale at which the saved size fits. A 1x window spanning onto a 2x laptop stays where it was. (cherry picked from commit 7e2393e, resolved onto the restructured restore_request / reachable_client from block#2405; its minimum rounding part is already in block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
macOS does not clamp a programmatic resize to the minimum, so the 8% allowance let a 1x save of 900x750 open at 450x375 points on a 2x laptop. Keep the exact minimum (rounded as tao rounds it) on macOS and Linux, and allow 8% only on Windows, where tao applies the minimum to the undecorated frame and the shadow insets come off the saved client. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Replace the single-monitor special case with one rule: a monitor only holds the saved rect if the saved size meets the minimum at that monitor's scale. A smaller size was saved at a lower scale, so with two 2x monitors left after unplugging a 1x external the window is now relocated with the default size instead of opening clamped. (cherry picked from commit 7de4895, resolved onto the restructured restore_request / reachable_client from block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Keep header visibility geometric again and check the minimum separately: at least one monitor under the header must have a scale at which the saved size fits. A 1x window spanning onto a 2x laptop stays where it was. (cherry picked from commit 7e2393e, resolved onto the restructured restore_request / reachable_client from block#2405; its minimum rounding part is already in block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
…adow insets The 8% allowance kept sizes that were already below the minimum, such as a 1x save of 900x750 on a lone 2x laptop. tao does not clamp a programmatic resize on Windows either: it answers WM_WINDOWPOSCHANGING itself without DefWindowProc. Compare instead against the client tao leaves at the minimum, the frame minimum minus the undecorated shadow insets from calculate_insets_for_dpi with default metrics and the Windows 11 top inset. macOS and Linux still use the rounded minimum. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Replace the single-monitor special case with one rule: a monitor only holds the saved rect if the saved size meets the minimum at that monitor's scale. A smaller size was saved at a lower scale, so with two 2x monitors left after unplugging a 1x external the window is now relocated with the default size instead of opening clamped. (cherry picked from commit 7de4895, resolved onto the restructured restore_request / reachable_client from block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Keep header visibility geometric again and check the minimum separately: at least one monitor under the header must have a scale at which the saved size fits. A 1x window spanning onto a 2x laptop stays where it was. (cherry picked from commit 7e2393e, resolved onto the restructured restore_request / reachable_client from block#2405; its minimum rounding part is already in block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz>
Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
* upstream/main: (91 commits) test(browser): click timeline rows only after the timeline settles (block#2560) ci: split Vitest across six shards so 3/4 stops hitting the job cap (block#2571) feat(github): unfurl PR and issue links inline with their title (block#1693) fix(auth): style federated sign-in callback pages (block#2549) fix(agents): align creation model controls with identity (block#2480) Run native Codex through plugin execution targets (block#2150) test: split Solo access browser scenarios (block#2499) feat(agents): let any agent change Buzz settings through the MCP (block#2248) Let plugins decorate host sidebar rows and add sort modes (block#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (block#2083) feat(agents): move version history into settings (block#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (block#2466) feat(channels): starter channels: admin switch and relay auto_join flag (block#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (block#2465) feat(plugins): let agents request plugin changes for owner review (block#2280) fix(messages): keep the hover toolbar while its menu is open (block#2439) feat(workflows): suggest channel members for @ in Send Message text (block#2309) AIDA-2344: Exclude muted channels from Dock (block#2313) feat(search): add slack-style message search operators on desktop (block#2463) test: fix invitation and coordination checks on main (block#2517) ... Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
joahg
left a comment
There was a problem hiding this comment.
🤖 Read the restore/relocate logic and tests; the sub-minimum fallback only triggers when the saved size is below the min at the fallback scale, so normal restores are unchanged. Windows-specific shadow-inset math is only covered by cfg-gated asserts, worth a hand check on Windows.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Carl, an automated reviewer, commenting via Wes’s GitHub account.
No blockers found in source review. Two independent reviewers checked restore order, the sub-minimum fallback, the logical conversion, minimum rounding and the Windows shadow slack against tao 0.37.2 and the locked tauri-plugin-window-state 2.5.0. Neither found a regression beyond the limitations the PR already discloses. For the repaired normal-window case, the plugin tracks the resize and reads current geometry on close, so it shouldn't re-save the bad size.
Validation gap: this is source-checked only. Windows native validation is skipped in CI, and no one has reproduced it on hardware. Before merge it's worth one real run on macOS: quit on a 1x external monitor, unplug it, relaunch on a 2x laptop, then quit and relaunch again to confirm the size persists.
Ran it on macOS with this branch. Window state saved small and off the laptop, relaunched on the 2x laptop: it opened at 1200x800 points, centered under the menu bar. Quit, then relaunched with only the laptop attached: still 1200x800 at the same spot, so the size persists. Bounds read from the window server, not by eye. While doing this I also relaunched once with the 1x externals still plugged in. That hits the attached-monitor gap in the description: the window grew to fill the laptop and was saved larger on quit. It comes from the existing reachable path converting physical pixels at the startup monitor's scale, which this PR does not touch. |
…2402) Scope: let ⇧Esc reach Mark all as read while focus is in the Channel settings panel. Non-goals: other Escape handlers and any change to the shortcut itself. Reuses the guard from the sibling panels. One file, 2 production lines. ## What was wrong With focus inside a channel's settings panel, ⇧Esc (Mark all as read, from #2170) closed the panel and marked nothing read. ## Why On `main`, the panel's `onKeyDown` in `src/bundled/channels/ChannelSettingsPanel.tsx:101` closes on any `Escape` from inside the panel and calls `event.stopPropagation()`. Since #2506 it skips events that are already handled or come from a portalled layer, but it still has no `!event.shiftKey` check. The sibling panels got that check in #2170 (`src/features/panels/PanelCard.tsx:32`, `src/bundled/channels/useChannelPanels.tsx:152`). Because the panel stops the event, the window shortcut dispatcher (`src/features/shortcuts/service.ts`, `src/app/shell/MarkAllReadShortcut.tsx`) never sees ⇧Esc. The fix adds the same `!event.shiftKey` guard and comment the siblings use, next to the #2506 checks. Plain Esc still closes the panel. ## How verified - Extended the existing Escape test in `src/bundled/channels/ChannelSettingsPanel.test.tsx`, "closes with Escape or its close button without swallowing other keys": ⇧Esc does not call `close` and reaches a `window` keydown listener; plain Esc still closes and does not reach it. It fails without the fix and passes with it. - `bin/pnpm vitest run src/bundled/channels/ChannelSettingsPanel.test.tsx`: 28 passed after merging `main`, including #2506's handled and portalled Escape test. - `bin/just iterate`: passed. ## In-app check Build: this PR at `4b286248b`, with #2403, #2404 and #2405 at their heads then, merged locally onto `main` at `cb8c971e9`. Run as the macOS desktop app with `BUZZ_DEV_VIEWER= just desktop`. The branch was later merged with `main` twice to resolve conflicts with #1624 (test file only) and #2506 (the same Escape handler). The production behavior of this fix did not change: ⇧Esc still passes through and plain Esc from a focused control in the panel still closes it. #2506 only adds its own skips for handled and portalled events. With the mouse alone, focus never lands inside the panel in the WebKit app, so these steps need macOS Keyboard navigation (System Settings > Keyboard) or Tab focus. 1. Open Channel actions > View channel details. Press Tab until a control in the panel shows a focus ring. Esc closes the panel. 2. Reopen the panel and Tab in again. ⇧Esc keeps the panel open and shows "Mark everything as read?". Where focus goes after opening the panel from the menu is a separate issue. buzz-review-completed --------- Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Scope: make the "newest message" lookup behind ↑ (select the newest message) and react to latest use the rendered message order. Non-goals: the same whole-second pattern in `ThreadPanel.tsx` and `unread.ts`, which touches the unread contract and needs its own PR. Reuses `compareMessages`; one file, 2 production lines. ## What was wrong When several messages land in the same second (agents do this a lot), ↑ in an empty composer and react to latest (⌘⇧\) select an earlier message instead of the newest one. ## Why `newest()` in `src/features/messages/use-message-selection.ts:29` compared `row.createdAt > newest.createdAt`. `createdAt` is whole seconds and the comparison is strict, so on a tie it kept the first row. The timeline renders rows by `compareMessages` (`src/features/relay/message-order.ts:17-24`: send ms, then event id), so the first row of a same-second burst is the oldest. Thread rows render in tree order, so taking the last row is not a fix there. The fix calls `compareMessages(row, newest) > 0` in the same reduce. `ChannelMessage` already carries `id`, `createdAt` and `createdAtMs`. No API change. ## How verified - New `src/features/messages/use-message-selection.test.tsx` mounts the hook with React Testing Library in StrictMode and calls `selectNewestMessage`. It covers a same-second burst ordered by send ms, a same-second tie broken by id, and an ordinary cross-second case that skips membership notices. - The same-second test fails on `main` (`Expected: "a"`, `Received: "z"`) and passes with the fix. - `pnpm vitest run src/features/messages/use-message-selection.test.tsx src/features/messages/message-selection.test.ts src/app/message-shortcuts.test.ts`: 14 passed. - `just iterate` (format, lint, typecheck, frontend build): passed. ## In-app check - Build: a local merge of the heads of #2402, #2403, #2404 and #2405 on `main` at `cb8c971e9`. macOS desktop, started with `BUZZ_DEV_VIEWER= just desktop`. - In a Solo chat, an agent sent 1, 2 and 3. With an empty composer, ↑ selected 3, and ⌘⇧\ opened the reaction picker on 3. - The same check on 0.7.8 also selected 3, so those messages did not share a second. This hand test shows no regression. It does not show the fixed tie. - The same-second tie is covered by the new test in `use-message-selection.test.tsx`, which fails on `main`. buzz-review-completed --------- Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…menu (#2526) Scope: after Channel actions > View channel details, leave focus on the Channel Settings tab so Escape closes it. Non-goals: the settings panel's own Escape handling (#2402), the shared menu, and the `finalFocus` helper. Reuses the menu's existing `finalFocus` ref, which Move channel > Create new already sets. One file, 12 production lines added and 1 removed. ## What was wrong In the macOS app, opening View channel details from the Channel actions (…) menu with the mouse left focus on the … button. Escape then ran the Escape mark-read shortcut instead of closing the settings tab. Escape closed the tab only after Tab moved focus into the panel. ## Why The menu item only called `openDetails()` (`src/bundled/channels/ChannelHeaderMenu.tsx:88` on `main`). The menu's `finalFocus` then returns focus to the trigger when the popup unmounts after its exit animation. `ChannelsPage` (`src/bundled/channels/ChannelsPage.tsx:1756`) and `PanelWorkspace` (`src/features/panels/PanelWorkspace.tsx:298`) focus the settings tab first. But `useFinalFocusUnlessMoved` counts focus on the page body or in the closing menu as "not moved" (`src/shared/design-system/ui/finalFocus.ts:59-65`). In WebKit the tab can lose focus to the body before the menu unmounts, so the trigger wins. With focus in the main pane header, the side pane's Escape handler (`PanelWorkspace.tsx:376`) never sees the key. `docs/channels.md:412-414` says View channel details opens or focuses the Channel Settings tab. The fix sets `finalFocus.current` for that item to the settings tab (`[data-panel-workspace] [data-tab-value="settings"]`, the selector `ChannelsPage` already uses). If no such tab exists, it falls back to the trigger as before. The shared "moved" check is unchanged, so focus the person moved elsewhere still stays put. ## How verified - New test in `src/bundled/channels/ChannelHeaderMenu.test.tsx`, "hands focus to the opened settings tab when the menu finishes closing after focus drops to the body". It mounts the real menu and a `PanelWorkspace` with a closable settings tab, holds the menu's exit with a `getAnimations` stub, clicks the item with `userEvent`, and blurs to the body before the menu unmounts. Then it asserts the tab has focus and Escape calls `close`. It fails on `main` with focus on the Channel actions button and passes with the fix. The path where focus stays on the tab is already covered by `tests/browser/channel-settings.spec.mjs` and `channel-header-menu.spec.mjs` in Chromium and WebKit. - `bin/pnpm vitest run` on `ChannelHeaderMenu`, `PanelWorkspace`, `ChannelSettingsPanel`, and the five `ChannelsPage*` test files: 144 passed. - `bin/just iterate`: passed on the first commit. The second commit changes only the test. - jsdom and the Playwright click do not reproduce the WebKit focus drop, so the test forces it with the blur. - Hook exception, with my OK: I pushed with `LEFTHOOK_EXCLUDE=types-and-related-unit-tests`, so that one pre-push job was skipped and the other pre-push jobs ran. On the current head that job's type check passed and its full Vitest run failed only two `ChannelMembersDialog.test.tsx` tests (2 failed, 17351 passed). Those two fail the same way at `40401e4d8` without this change, both locally and in `main` CI (`JavaScript (4/4)`). ## In-app check Found in hand testing on the macOS app, and verified there with this fix. Build: this PR at `75fef69f8` (later commits only rerun CI and merge `main`; no code change) with #2402, #2403, #2404 and #2405, merged locally onto `main` at `cb8c971e9`. Run as the macOS desktop app with `BUZZ_DEV_VIEWER= just desktop`. 1. In a regular channel, open Channel actions (…) > View channel details with the mouse. 2. Press Esc right away, without clicking anything else or pressing Tab. 3. The Channel Settings tab closes. Before the fix, it stayed open and Esc marked the conversation as read. buzz-review-completed --------- Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
…-service * origin/main: (29 commits) fix(agents): treat what an agent reads as information, and give answering rules only to mentions (#2456) feat: add up arrow message editing preference (#2659) fix(channels): move focus to channel settings opened from the header menu (#2526) fix(window): restore position before size and never fall back to a sub-minimum window (#2405) fix(messages): pick the latest message with the shared ordering (#2403) fix(channels): let shift+escape through the channel settings panel (#2402) fix(builderlab): clarify signed-out agent settings (#2602) feat(builderlab): preselect the catalog's recommended model (#2552) feat(host-process): set BUZZ_VERSION for every spawned process (#2674) fix(agents): finish Delete for a synced agent with no config (#2692) ci(flake-check): select related tests within a 9-minute lane budget (#2607) feat(macos): show the closed window again when Buzz becomes active (#2658) fix(channels): show channel actions as soon as the menu opens (#2657) fix(huddles): confirm before starting a huddle from the header (#2687) perf(composer): reuse per-keystroke projections, snapshots and reference scans (#2590) perf(agents): skip the activity snapshot compare for unrelated channel traffic (#2586) test(solo): wait for the live header before opening a joined viewer's access (#2599) fix(messages): show who reacted in channels where you can't react (#2601) feat(agents): clarify remote execution across desktop (#2600) perf(relay): size paged history with the cached per-event sizes (#2597) ... # Conflicts: # src-tauri/src/host_process/tests.rs
Scope checkpoint: end bare `http(s)://` and `buzz://` links at fullwidth and CJK punctuation in rendered messages and in `messageLinkParts`, keep CJK letters inside URLs, and leave explicit `<url>` and `[label](url)` links unchanged. Non-goals: links without a scheme (`www.`), Markdown link syntax, link previews. Files: `message-link-parts.ts`, `MessageMarkdown.tsx`, their tests, and one line in `docs/buzz-help.md`. Budget: about 30 production lines (actual +30/−1). ## What was wrong A bare link followed by Chinese or Japanese punctuation took the punctuation and any text after it into the link. `https://github.com/block/buzz-app。` opened `…/buzz-app%E3%80%82` (404). `看这个 https://example.com/spec,大家看一下` linked the whole rest of the sentence. `(https://example.com)` opened the host `example.com)`, because the URL parser folds `)` to `)`. ## Why Two places find bare URLs, and both end them only at whitespace: - Rendered messages get bare links from `remark-gfm`. Its `findUrl` transform (`mdast-util-gfm-autolink-literal` `lib/index.js:151`) reads up to the next space and trims only ASCII punctuation (`splitUrl`, line 241). `messageLinkParts` never sees these URLs; it only gets the text around them. - `messageLinkParts` (`src/features/messages/message-link-parts.ts:98`) drives the composer link decorations, chat cards and clipboard. Its bare URL pattern is `` [^\s<>"`]+ ``, so the same input fails there too. ## How verified - `message-link-parts.ts:98`: the bare URL pattern now also stops at fullwidth and CJK punctuation (`,。、.!?;:"'()[]{}「」『』《》【】〈〉〔〕〖〗〝〞〟。「」、…~〜`) and curly quotes (`“”‘’`), which Chinese text uses as its quotes. CJK letters are still allowed, so `https://zh.wikipedia.org/wiki/中文` stays whole. - In the host name (before any `:`, `/`, `?` or `#`), `。.。` followed by an ASCII host label continue the link (unless another `://` follows that label), because the URL parser reads them as `.`: `https://github。com/block/buzz-app` still opens github.com, as on `main`. After the host name, including after a port, they end it. `\s` already covers the ideographic space. Bracket pairs need no extra trimming, because the match stops before them. - `MessageMarkdown.tsx:342`: a small remark step runs after `remark-gfm`. When `messageLinkParts` splits a literal autolink differently, the step replaces it with the parts `messageLinkParts` finds, so `https://a.com,https://b.com` becomes two links with the `,` between them as text. Links whose source is `<url>` or `[url](url)` keep their written destination. - `docs/buzz-help.md`: one bullet says where a bare http(s) or buzz link ends and that `<url>` keeps those characters. - New tests: `message-link-parts.test.ts` "ends a bare URL at fullwidth punctuation but keeps CJK letters" (18 cases, plus one that `https://a.com。https://b.com` gives two links), and `MessageMarkdown.test.tsx` "ends a bare link at fullwidth punctuation" (9 cases, checks the rendered `href`). 23 of the 28 fail on `main`. The other 5 guard IRIs, `<url>` and hosts written with `。`. - `bin/pnpm vitest run src/features/messages/message-link-parts.test.ts src/features/messages/MessageMarkdown.test.tsx`: 175 passed after the latest merge with `main`. - `bin/pnpm vitest run src/features/messages src/features/conversation src/features/relay src/bundled/links src/bundled/relay-notes`: 5574 passed, 4 skipped. - `bin/just iterate` (format, lint, typecheck, build): passed. - Known limits: GFM reads Markdown link syntax glued to a bare URL (`https://a.com,[label](https://b.com)`) as part of that URL, so the brackets still show, as they do on `main`. A bare URL that really contains one of these characters, such as a typographic `’` in a title, now ends there; `<url>` keeps it whole. A host followed by `。` and an English word (`https://example.com。OK`) still reads the word as part of the host, as on `main`. ## In-app check Build: this PR at `2dd13819d`, the SHA I tested by hand (the branch was later merged with `main` to resolve conflicts in `docs/buzz-help.md`; no behavior change), with #2402, #2403 and #2405, merged locally onto `main` at `cb8c971e9`. Run as the macOS desktop app with `BUZZ_DEV_VIEWER= just desktop`. I sent these nine lines as one message in a Solo chat. The link on each line covered exactly this text: | # | Line | Linked text | | --- | --- | --- | | 1 | `https://github.com/block/buzz-app。` | `https://github.com/block/buzz-app` | | 2 | `看这个 https://example.com/spec,大家看一下` | `https://example.com/spec` | | 3 | `https://a.com,https://b.com` | `https://a.com` and `https://b.com`, two links | | 4 | `https://zh.wikipedia.org/wiki/中文` | the whole URL | | 5 | `(https://example.com)` | `https://example.com` | | 6 | `<https://example.com/a。b>` | the whole URL | | 7 | `https://github。com/block/buzz-app` | the whole URL | | 8 | `http://localhost:3000。OK` | `http://localhost:3000` | | 9 | `https://github。com:/block/buzz-app` | the whole URL | Clicking lines 1 and 7 opened github.com/block/buzz-app, with no `%E3%80%82` in the address. ## Screenshots The same message in the installed 0.7.8 release and in the build above. In 0.7.8, lines 1, 2, 3 and 5 take the punctuation into the link, and line 8 is not linked at all. | Before (0.7.8) | After (this PR) | | --- | --- | | <img width="360" alt="Before, Buzz 0.7.8: the nine test lines, with punctuation inside links on lines 1, 2, 3 and 5 and line 8 not linked" src="https://github.com/user-attachments/assets/064dd7ff-cde4-476e-887c-7310288254d2" /> | <img width="360" alt="After, this PR: the nine test lines, with each link covering the text listed in the table above" src="https://github.com/user-attachments/assets/9569922c-3365-4df4-97ae-b469faab0637" /> | buzz-review-completed --------- Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
* origin/main: (36 commits) perf(mentions): rank only the shown rows and skip empty-query Space work (#2298) fix(messages): stop bare links at fullwidth and CJK punctuation (#2404) fix(agents): treat what an agent reads as information, and give answering rules only to mentions (#2456) feat: add up arrow message editing preference (#2659) fix(channels): move focus to channel settings opened from the header menu (#2526) fix(window): restore position before size and never fall back to a sub-minimum window (#2405) fix(messages): pick the latest message with the shared ordering (#2403) fix(channels): let shift+escape through the channel settings panel (#2402) fix(builderlab): clarify signed-out agent settings (#2602) feat(builderlab): preselect the catalog's recommended model (#2552) feat(host-process): set BUZZ_VERSION for every spawned process (#2674) fix(agents): finish Delete for a synced agent with no config (#2692) ci(flake-check): select related tests within a 9-minute lane budget (#2607) feat(macos): show the closed window again when Buzz becomes active (#2658) fix(channels): show channel actions as soon as the menu opens (#2657) fix(huddles): confirm before starting a huddle from the header (#2687) perf(composer): reuse per-keystroke projections, snapshots and reference scans (#2590) perf(agents): skip the activity snapshot compare for unrelated channel traffic (#2586) test(solo): wait for the live header before opening a joined viewer's access (#2599) fix(messages): show who reacted in channels where you can't react (#2601) ... # Conflicts: # docs/buzz-help.md
…page * origin/main: (49 commits) fix(browser): recheck the FI session when the app viewer exchange returns 401 (#2678) fix(members): find verified people who have no profile (#2554) fix(dev): clear React's dev render measures so long sessions do not grow (#2495) Move app navigation to a dock and unify conversation headers (#2626) perf(mentions): rank only the shown rows and skip empty-query Space work (#2298) fix(messages): stop bare links at fullwidth and CJK punctuation (#2404) fix(agents): treat what an agent reads as information, and give answering rules only to mentions (#2456) feat: add up arrow message editing preference (#2659) fix(channels): move focus to channel settings opened from the header menu (#2526) fix(window): restore position before size and never fall back to a sub-minimum window (#2405) fix(messages): pick the latest message with the shared ordering (#2403) fix(channels): let shift+escape through the channel settings panel (#2402) fix(builderlab): clarify signed-out agent settings (#2602) feat(builderlab): preselect the catalog's recommended model (#2552) feat(host-process): set BUZZ_VERSION for every spawned process (#2674) fix(agents): finish Delete for a synced agent with no config (#2692) ci(flake-check): select related tests within a 9-minute lane budget (#2607) feat(macos): show the closed window again when Buzz becomes active (#2658) fix(channels): show channel actions as soon as the menu opens (#2657) fix(huddles): confirm before starting a huddle from the header (#2687) ... Signed-off-by: Nathan Thillairajah <nthillairajah@squareup.com> # Conflicts: # docs/browser.md # src-tauri/src/browser/builderlab.rs
…dylan/browser-unavailable-page * origin/dylan/browser-denied-page: (49 commits) fix(browser): recheck the FI session when the app viewer exchange returns 401 (#2678) fix(members): find verified people who have no profile (#2554) fix(dev): clear React's dev render measures so long sessions do not grow (#2495) Move app navigation to a dock and unify conversation headers (#2626) perf(mentions): rank only the shown rows and skip empty-query Space work (#2298) fix(messages): stop bare links at fullwidth and CJK punctuation (#2404) fix(agents): treat what an agent reads as information, and give answering rules only to mentions (#2456) feat: add up arrow message editing preference (#2659) fix(channels): move focus to channel settings opened from the header menu (#2526) fix(window): restore position before size and never fall back to a sub-minimum window (#2405) fix(messages): pick the latest message with the shared ordering (#2403) fix(channels): let shift+escape through the channel settings panel (#2402) fix(builderlab): clarify signed-out agent settings (#2602) feat(builderlab): preselect the catalog's recommended model (#2552) feat(host-process): set BUZZ_VERSION for every spawned process (#2674) fix(agents): finish Delete for a synced agent with no config (#2692) ci(flake-check): select related tests within a 9-minute lane budget (#2607) feat(macos): show the closed window again when Buzz becomes active (#2658) fix(channels): show channel actions as soon as the menu opens (#2657) fix(huddles): confirm before starting a huddle from the header (#2687) ... Signed-off-by: Nathan Thillairajah <nthillairajah@squareup.com> # Conflicts: # src-tauri/src/browser/builderlab.rs
Replace the single-monitor special case with one rule: a monitor only holds the saved rect if the saved size meets the minimum at that monitor's scale. A smaller size was saved at a lower scale, so with two 2x monitors left after unplugging a 1x external the window is now relocated with the default size instead of opening clamped. (cherry picked from commit 7de4895, resolved onto the restructured restore_request / reachable_client from block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz> (cherry picked from commit 55d8657)
Keep header visibility geometric again and check the minimum separately: at least one monitor under the header must have a scale at which the saved size fits. A 1x window spanning onto a 2x laptop stays where it was. (cherry picked from commit 7e2393e, resolved onto the restructured restore_request / reachable_client from block#2405; its minimum rounding part is already in block#2405) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Cheng Peng <cheng@block.xyz> (cherry picked from commit 6f018b1)
Scope: fix how the main window restores its saved size after the monitor it was saved on is unplugged and the remaining monitor has another scale. Non-goals: storing the scale in the state file (tauri-plugin-window-state stays the only writer), plugin version changes, onboarding geometry. Production diff is 34 added and 10 removed lines in
src-tauri/src/window_state.rs, plus tests. The case with several monitors left needs more logic and is handled in a follow-up PR that builds on this one. It and the remaining limitations are listed under Known gaps.What was wrong
Quit Buzz with the window on a 1x external monitor, unplug it, and relaunch on a 2x laptop. The window comes back at half size, at or below the 480x400 minimum. The plugin then saves that small size, so later launches keep it.
Why
The plugin saves size in physical pixels. A 602x470 window saved at 1x is 301x235 points at 2x, below
minWidth/minHeightintauri.conf.json. macOS does not clamp a programmatic resize to the minimum (taoplatform_impl/macos/util/async.rs:88-101), so it can open below it; a later user resize is clamped to it.reachable_client(src-tauri/src/window_state.rs:215-220onmain) moves the window onto the primary monitor but keeps the physical size.tao0.37.2platform_impl/macos/window.rs:702-731,platform_impl/linux/window.rs:472-510). When Buzz starts on a monitor with another scale, the window lands at the wrong size and place.Separately,
restore_geometry_fromresized before it moved (window_state.rs:158-159onmain). On Windows, moving to a monitor with another DPI rescales the window (taoplatform_impl/windows/event_loop.rs,WM_DPICHANGED), which undoes a resize sent first. The plugin's ownrestore_statemoves first.What changed
restore_requestgets the fallback monitor's scale and the main window config, and stays pure. If the saved size is below the configured minimum at that scale, it was saved at a lower scale, and it gets the configured default size (1200x800) instead, still capped to the work area and horizontally centered at its top. The minimum is converted to pixels with the rounding tao uses.taoplatform_impl/windows/event_loop.rs:1783-1810,:2124-2130), so a window at the minimum is saved below it (464x391 at 100%). The check there compares with that client: the rounded minimum minus the insetscalculate_insets_for_dpicomputes (windows/util.rs:419-447) with default metrics (4 px size frame plus 4 px padded border, scaled by DPI) and the Windows 11 top inset.WM_WINDOWPOSCHANGINGwithoutDefWindowProc,windows/event_loop.rs:1145).set_positionnow runs beforeset_size.Known gaps
Handled in a follow-up PR that builds on this one (opens after this one merges):
Not fixed here or in the follow-up:
SM_CXSIZEFRAME/SM_CXPADDEDBORDER) saves a smaller client at the minimum, so a window dragged to the minimum there is reset to the default size. Reading tao'sget_frame_thicknesswould cover it; this PR assumes the default.How verified
saved_size_is_kept_with_monitors_attached_and_recovered_on_a_lone_monitorfallback_uses_default_size_when_saved_size_is_below_minimum_at_its_scaleminimum_allows_windows_shadow_insets_but_a_lower_scale_size_is_replaced: Windows minimum clients at 100%, 125% and 200% are kept only on Windows, and one pixel under them is replaced everywhere. A 1x save of 900x750 on a 2x laptop is replaced everywhere. At 110 and 100 DPI the rounded minimum and the Windows client are checked on both sides. A minimum saved one step lower, including 225% to 250%, is replaced everywhere.restore_requesttests now also check whether the window was relocated and at which scale.cfg!(windows)to true; CI runs only the host's branch.bin/cargo test -p Buzz --lib --locked window_state -- --skip onboarding_snapshot_is_kept_until_next_launch_restore: 15 passed. That skipped test segfaults on macOS on unmodifiedmaintoo. It callsns_window()throughMockRuntime.bin/cargo clippy -p Buzz --lib --tests --locked -- -D warnings,bin/cargo fmt --all --checkandgit diff --checkpass.In-app check
Build: this PR at
94bc460e5(later commits only rerun CI and mergemain; no code change), #2402, #2403 and #2404, merged locally ontomainatcb8c971e9. Run as the macOS desktop app withBUZZ_DEV_VIEWER= just desktopon a 2x laptop with two 1x external monitors. Window bounds were read from the window server (CGWindowListCopyWindowInfo), not by eye.onboarding-window-restore.jsonis used once, then deleted). That snapshot was about 1720x1410 pixels at 1x, so the window opened at 860x705 points. That is above the minimum at half the points, the first "Not fixed here or in the follow-up" case under Known gaps.buzz-review-completed