Skip to content

fix(search): keep requested pin state#289

Open
jiang171 wants to merge 1 commit into
TouchAI-org:mainfrom
jiang171:codex/fix-window-pin-toggle
Open

fix(search): keep requested pin state#289
jiang171 wants to merge 1 commit into
TouchAI-org:mainfrom
jiang171:codex/fix-window-pin-toggle

Conversation

@jiang171

@jiang171 jiang171 commented May 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #287.

Keep the requested search-window pinned state after the user toggles the pin button. This prevents Linux/Wayland from immediately reverting the UI state when isAlwaysOnTop() reports false even after setAlwaysOnTop(true) was requested.

Changes:

  • Track the user's requested pinned state in useSearchWindowPin().
  • Keep app-blur hide policy driven by the requested pinned state.
  • Preserve the native setAlwaysOnTop() call.
  • Add a regression test for platforms that report isAlwaysOnTop() as false after a pin request.

Related issue or RFC

AI assistance disclosure

  • Tool(s) used: Codex
  • Scope of assistance: implementation, focused test, local validation, PR preparation
  • Human review or rewrite performed: I reviewed the diff and manually validated the behavior on Ubuntu GNOME using a Linux package build
  • Architecture or boundary impact: none; this is isolated to SearchView window pin state handling

Testing evidence

Local checks:

pnpm --filter @touchai/desktop exec vitest run tests/composables/SearchView/useSearchPage.test.ts --environment happy-dom --configLoader runner
pnpm --filter @touchai/desktop type:check
pnpm --filter @touchai/desktop exec prettier --check src/views/SearchView/composables/useSearchPage.ts tests/composables/SearchView/useSearchPage.test.ts

Observed results:

  • Focused test passed: 8 tests passed
  • Type check passed
  • Prettier check passed for touched files

Manual verification:

  • Built a Linux .deb from the fix branch through GitHub Actions on my fork
  • Installed and tested it on Ubuntu 24.04 GNOME / Wayland
  • Confirmed the pin button state changes and the pinned window no longer auto-hides on blur

Notes:

  • I did not run the full pnpm test:pr locally due to runtime cost; this PR includes a focused regression test and relies on repository CI for the full suite.
  • I did not follow strict TDD because the issue was first reproduced manually on Ubuntu, then covered with a regression test.

Risk notes

  • AgentService, runtime, MCP, or schema impact: none
  • database baseline or migration impact: none
  • release or packaging impact: none

Screenshots or recordings

Manual Ubuntu/GNOME verification was performed locally after installing the generated .deb.

Checklist

  • The PR title follows Conventional Commits and is valid for squash merge.
  • This PR is either ready for review or explicitly marked as a Draft PR.
  • I did not use [WIP] or similar title prefixes.
  • If AI materially assisted this PR, I disclosed the tools and scope and I personally reviewed every affected change.
  • I can explain the why, what, and how of this change without relying on an AI tool.
  • If this touches AgentService, runtime, MCP, or schema boundaries, there is an accepted RFC.
  • If this changes architecture or adds a new cross-boundary abstraction, there is an accepted RFC.
  • I ran pnpm test:pr for this code PR, or this is a docs-only change.
  • If I changed Rust behavior or tests, I reviewed pnpm test:coverage:rust or relied on CI coverage evidence.
  • If I changed desktop startup/window/search/popup/settings/E2E paths, I ran pnpm test:e2e locally or documented why CI is the first valid proof.
  • I added tests or explained why tests are not appropriate.
  • I updated docs when behavior changed.

Summary by Sourcery

Ensure the search window pin state is driven by the user’s requested state instead of relying solely on the native always-on-top result.

Bug Fixes:

  • Prevent the search window from unpinning or auto-hiding when the platform reports an incorrect always-on-top state, particularly on Linux/Wayland.

Tests:

  • Add regression tests for useSearchWindowPin to verify requested pin state is preserved and correctly synced with the native window state.

@sourcery-ai

sourcery-ai Bot commented May 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adjusts the search window pin composable to track and honor the user’s requested pinned state instead of relying solely on the native window’s reported always-on-top flag, and adds targeted regression tests for this behavior.

Sequence diagram for search window pinning with desired state tracking

sequenceDiagram
    actor User
    participant SearchView
    participant useSearchWindowPin
    participant currentWindow

    User->>SearchView: clickPinButton
    SearchView->>useSearchWindowPin: toggleWindowPin()
    useSearchWindowPin->>useSearchWindowPin: queuePinOperation(operation)
    useSearchWindowPin->>useSearchWindowPin: desiredPinnedState = !isPinned.value
    useSearchWindowPin->>currentWindow: setAlwaysOnTop(desiredPinnedState)
    useSearchWindowPin-->>SearchView: return desiredPinnedState (isPinned updated)

    rect rgb(235, 235, 235)
    SearchView->>useSearchWindowPin: syncWindowPinState()
    useSearchWindowPin->>useSearchWindowPin: queuePinOperation(operation)
    alt desiredPinnedState is not null
        useSearchWindowPin-->>SearchView: return desiredPinnedState (no native check)
    else desiredPinnedState is null
        useSearchWindowPin->>currentWindow: isAlwaysOnTop()
        currentWindow-->>useSearchWindowPin: nextState
        useSearchWindowPin-->>SearchView: return nextState
    end
    end
Loading

File-Level Changes

Change Details Files
Track a desired pinned state in the search window pin composable and prefer it over native window state when syncing.
  • Introduce a local desired pinned state variable alongside the reactive isPinned ref in the pin composable
  • Short-circuit syncWindowPinState to return and apply the desired state when it has been set instead of querying the native window
  • Keep the native isAlwaysOnTop call only for the initial sync before the user has expressed a preference
apps/desktop/src/views/SearchView/composables/useSearchPage.ts
Update pinning operations to be driven by the composable’s tracked state while still issuing native setAlwaysOnTop calls.
  • In setWindowPinned, set desiredPinnedState and update isPinned directly, returning the requested value without re-reading native state
  • In toggleWindowPin, toggle based on isPinned.value, update desiredPinnedState accordingly, and call setAlwaysOnTop with the computed next state
  • Preserve the queuePinOperation sequencing so pin operations remain serialized
apps/desktop/src/views/SearchView/composables/useSearchPage.ts
Add regression tests to validate requested pin state behavior and interaction with native window state reporting.
  • Import useSearchWindowPin into the SearchView composable test suite
  • Add a test ensuring the requested pinned state is preserved and native isAlwaysOnTop is not re-read after a user-initiated toggle
  • Add a test ensuring initial sync still reflects the native always-on-top state before the user requests a pin change
apps/desktop/tests/composables/SearchView/useSearchPage.test.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#287 Maintain a user-requested pinned state independent of unreliable isAlwaysOnTop() results, so that clicking the pin button immediately updates and preserves the UI pinned state and the blur-hide behavior.
#287 Continue calling setAlwaysOnTop() while avoiding immediate overwrites from isAlwaysOnTop() after the user has interacted, only syncing from the native window state before the user changes the pin state.
#287 Add regression tests that cover the case where the platform reports false for isAlwaysOnTop() after a pin request, ensuring the requested pinned state remains true.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added the area:frontend Frontend UI or view-layer changes label May 30, 2026
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved search window pinning so rapid pin/unpin actions reliably preserve the intended pinned/always-on-top state. The window now maintains the correct pinned status during quick successive requests, preventing unexpected toggles or desync between user intent and window behavior.

Walkthrough

Caches the user's requested pinned/always-on-top state in useSearchWindowPin and uses that cache for sync, set, and toggle operations so UI state isn't overwritten by unreliable platform isAlwaysOnTop() reads. Tests exercise toggle behavior and initial sync.

Changes

Window pin state consistency on Linux/Wayland

Layer / File(s) Summary
Desired pinned state cache and pin operations
apps/desktop/src/views/SearchView/composables/useSearchPage.ts
Adds desiredPinnedState and updates syncWindowPinState, setWindowPinned, and toggleWindowPin to use the cached desired value rather than immediately re-reading isAlwaysOnTop().
useSearchWindowPin test suite
apps/desktop/tests/composables/SearchView/useSearchPage.test.ts
Imports and tests useSearchWindowPin. New tests mock native window APIs to verify toggle preserves the requested pinned state when platform reports false and that syncWindowPinState initializes from the native isAlwaysOnTop when no desired state is pending.

Suggested labels

area:tauri, area:frontend

Suggested reviewers

  • hiqiancheng

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I cached your wish to stay on top,
A tiny hop so toggles stop.
When Wayland lies and answers slow,
The rabbit keeps your pin aglow.
Now clicks persist — behold the stop.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows Conventional Commits format with 'fix' prefix and clearly describes the main change: keeping the requested pin state of the search window.
Description check ✅ Passed The PR description is comprehensive, covering summary, related issue, AI assistance disclosure, testing evidence, risk notes, and completed checklist items per the template.
Linked Issues check ✅ Passed The PR directly addresses issue #287 by implementing all required objectives: tracking requested pinned state, preventing auto-hide when pinned, preserving setAlwaysOnTop() calls, and adding regression tests.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the window pin state issue [#287]; modifications to useSearchPage.ts composable and corresponding tests are in scope and necessary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot requested a review from hiqiancheng May 30, 2026 05:37
@coderabbitai coderabbitai Bot added the area:tauri Tauri shell or desktop runtime changes label May 30, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Because desiredPinnedState short-circuits syncWindowPinState, any external/native changes after the first user interaction will be ignored; consider clearing or expiring desiredPinnedState after a successful sync or after some condition so later syncs can reflect native state again.
  • toggleWindowPin now relies solely on the local isPinned ref, which defaults to false; if syncWindowPinState hasn’t been run yet, the first toggle may invert the wrong starting state—consider either initializing from isAlwaysOnTop() on composable creation or having toggleWindowPin fall back to the native state when desiredPinnedState/isPinned is still in its initial state.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Because `desiredPinnedState` short-circuits `syncWindowPinState`, any external/native changes after the first user interaction will be ignored; consider clearing or expiring `desiredPinnedState` after a successful sync or after some condition so later syncs can reflect native state again.
- `toggleWindowPin` now relies solely on the local `isPinned` ref, which defaults to `false`; if `syncWindowPinState` hasn’t been run yet, the first toggle may invert the wrong starting state—consider either initializing from `isAlwaysOnTop()` on composable creation or having `toggleWindowPin` fall back to the native state when `desiredPinnedState`/`isPinned` is still in its initial state.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 30, 2026
@hiqiancheng

Copy link
Copy Markdown
Collaborator

Please fix the following conflict.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/views/SearchView/composables/useSearchPage.ts`:
- Around line 65-80: setWindowPinned and toggleWindowPin set desiredPinnedState
before awaiting currentWindow.setAlwaysOnTop, so if the native call rejects the
cached desiredPinnedState will be wrong and syncWindowPinState will keep
replaying it; fix by capturing the previousPinned value at start of each
function, wrap the await currentWindow.setAlwaysOnTop(...) in a try/catch, and
on failure restore desiredPinnedState (and do not update isPinned) to the
previous value before rethrowing/returning the error; modify queuePinOperation
closures in setWindowPinned and toggleWindowPin to perform this rollback so the
cached state and UI stay consistent with the real window state.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 16c126e7-7309-46cb-844f-1462edfe5a9c

📥 Commits

Reviewing files that changed from the base of the PR and between a827a5f and fcc1b7d.

📒 Files selected for processing (2)
  • apps/desktop/src/views/SearchView/composables/useSearchPage.ts
  • apps/desktop/tests/composables/SearchView/useSearchPage.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Frontend Tests
  • GitHub Check: Rust Checks
  • GitHub Check: Desktop E2E Smoke (Windows)
  • GitHub Check: CodeQL (rust)
🧰 Additional context used
🪛 ast-grep (0.42.3)
apps/desktop/tests/composables/SearchView/useSearchPage.test.ts

[warning] 292-292: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: document.body.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

Comment on lines 65 to 80
function setWindowPinned(value: boolean): Promise<boolean> {
return queuePinOperation(async () => {
desiredPinnedState = value;
await currentWindow.setAlwaysOnTop(value);
const nextState = await currentWindow.isAlwaysOnTop();
isPinned.value = nextState;
return nextState;
isPinned.value = value;
return value;
});
}

function toggleWindowPin(): Promise<boolean> {
return queuePinOperation(async () => {
const currentState = await currentWindow.isAlwaysOnTop();
await currentWindow.setAlwaysOnTop(!currentState);
const nextState = await currentWindow.isAlwaysOnTop();
const nextState = !isPinned.value;
desiredPinnedState = nextState;
await currentWindow.setAlwaysOnTop(nextState);
isPinned.value = nextState;
return nextState;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rollback the cached pin state if the native request fails.

desiredPinnedState is updated before the awaited pin/unpin call completes. If that call rejects, later syncWindowPinState() calls will keep replaying the cached value and stop consulting the real window state, so the pin button and blur-hide policy can stay flipped even though the OS never applied the change.

Suggested fix
 function setWindowPinned(value: boolean): Promise<boolean> {
     return queuePinOperation(async () => {
+        const previousDesiredPinnedState = desiredPinnedState;
         desiredPinnedState = value;
-        await currentWindow.setAlwaysOnTop(value);
-        isPinned.value = value;
-        return value;
+        try {
+            await currentWindow.setAlwaysOnTop(value);
+            isPinned.value = value;
+            return value;
+        } catch (error) {
+            desiredPinnedState = previousDesiredPinnedState;
+            throw error;
+        }
     });
 }
 
 function toggleWindowPin(): Promise<boolean> {
     return queuePinOperation(async () => {
         const nextState = !isPinned.value;
+        const previousDesiredPinnedState = desiredPinnedState;
         desiredPinnedState = nextState;
-        await currentWindow.setAlwaysOnTop(nextState);
-        isPinned.value = nextState;
-        return nextState;
+        try {
+            await currentWindow.setAlwaysOnTop(nextState);
+            isPinned.value = nextState;
+            return nextState;
+        } catch (error) {
+            desiredPinnedState = previousDesiredPinnedState;
+            throw error;
+        }
     });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function setWindowPinned(value: boolean): Promise<boolean> {
return queuePinOperation(async () => {
desiredPinnedState = value;
await currentWindow.setAlwaysOnTop(value);
const nextState = await currentWindow.isAlwaysOnTop();
isPinned.value = nextState;
return nextState;
isPinned.value = value;
return value;
});
}
function toggleWindowPin(): Promise<boolean> {
return queuePinOperation(async () => {
const currentState = await currentWindow.isAlwaysOnTop();
await currentWindow.setAlwaysOnTop(!currentState);
const nextState = await currentWindow.isAlwaysOnTop();
const nextState = !isPinned.value;
desiredPinnedState = nextState;
await currentWindow.setAlwaysOnTop(nextState);
isPinned.value = nextState;
return nextState;
function setWindowPinned(value: boolean): Promise<boolean> {
return queuePinOperation(async () => {
const previousDesiredPinnedState = desiredPinnedState;
desiredPinnedState = value;
try {
await currentWindow.setAlwaysOnTop(value);
isPinned.value = value;
return value;
} catch (error) {
desiredPinnedState = previousDesiredPinnedState;
throw error;
}
});
}
function toggleWindowPin(): Promise<boolean> {
return queuePinOperation(async () => {
const nextState = !isPinned.value;
const previousDesiredPinnedState = desiredPinnedState;
desiredPinnedState = nextState;
try {
await currentWindow.setAlwaysOnTop(nextState);
isPinned.value = nextState;
return nextState;
} catch (error) {
desiredPinnedState = previousDesiredPinnedState;
throw error;
}
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/views/SearchView/composables/useSearchPage.ts` around lines
65 - 80, setWindowPinned and toggleWindowPin set desiredPinnedState before
awaiting currentWindow.setAlwaysOnTop, so if the native call rejects the cached
desiredPinnedState will be wrong and syncWindowPinState will keep replaying it;
fix by capturing the previousPinned value at start of each function, wrap the
await currentWindow.setAlwaysOnTop(...) in a try/catch, and on failure restore
desiredPinnedState (and do not update isPinned) to the previous value before
rethrowing/returning the error; modify queuePinOperation closures in
setWindowPinned and toggleWindowPin to perform this rollback so the cached state
and UI stay consistent with the real window state.

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:frontend Frontend UI or view-layer changes area:tauri Tauri shell or desktop runtime changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Linux/Ubuntu 下固定窗口按钮点击后状态不生效

2 participants