Skip to content

Conversation

@neoragex2002
Copy link
Contributor

@neoragex2002 neoragex2002 commented Aug 29, 2025

解决悬浮按钮拖动乱飞问题

Summary by CodeRabbit

  • Bug Fixes
    • Dragging the floating button is more reliable and responsive: drags start immediately at threshold, concurrent timers are prevented, and click events are suppressed during a drag to avoid accidental toggles.
    • Mouse interactions now ignore non-left-button presses to prevent unintended behavior.
    • Floating chat window positioning improved: smarter side-first placement, bounds clamping, and restored visibility after drag preserve consistent placement and on-screen layout.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 29, 2025

Walkthrough

Adds stricter mouse-button guards and clears a pending drag-start timer in the renderer; restructures presenter drag-state to use a nullable state and an explicit wasVisible-before-drag flag, suppresses clicks during drag, and tightens drag move/end guards; simplifies FloatingChatWindow placement to a right/left/fallback algorithm with clamped coordinates.

Changes

Cohort / File(s) Change Summary
Renderer: floating button input
src/renderer/floating/FloatingButton.vue
Added left-button guards on mousedown/mouseup, and on threshold-exceeding mousemove call clearDragTimer() before startDragging() to prevent the delayed drag timer from firing after an immediate drag start. No public signatures changed.
Presenter: drag/session management
src/main/presenter/floatingButtonPresenter/index.ts
Reworked drag state to use a nullable dragState plus wasFloatingChatVisibleBeforeDrag; introduced isDuringDragSession to suppress click handling during drags; added guards on DRAG_MOVE/DRAG_END; ensures final clamped positioning and restores FloatingChatWindow visibility when appropriate.
Window placement: chat window positioning
src/main/presenter/windowPresenter/FloatingChatWindow.ts
Rewrote calculatePosition to derive dynamic window size, attempt right-side placement first, then left-side, then fallback to screen-right; center-vertical alignment with clamping and rounded coordinates; removed previous multi-priority placements.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant FB as FloatingButton.vue
  participant P as Presenter

  U->>FB: mousedown (button === 0)
  FB->>FB: start DRAG_DELAY timer
  U->>FB: mousemove (small)
  Note right of FB: still pending
  U->>FB: mousemove (exceeds threshold)
  rect rgba(200,240,255,0.25)
    FB->>FB: clearDragTimer()
    FB->>P: dispatch DRAG_START
  end
  P->>P: set isDuringDragSession = true\ninit dragState, hide FloatingChatWindow if visible
  U->>FB: mousemove (while dragging)
  FB->>P: dispatch DRAG_MOVE
  P->>P: guard dragState && isDragging\nupdate window position
  U->>FB: mouseup
  FB->>P: dispatch DRAG_END
  P->>P: guard dragState && isDragging\nclamp position, restore FloatingChatWindow if needed\nreset dragState, isDuringDragSession = false
Loading
sequenceDiagram
  participant U as User
  participant FB as FloatingButton.vue
  participant P as Presenter

  U->>FB: click
  FB->>P: dispatch CLICKED
  alt isDuringDragSession == true
    P-->>FB: return early (suppress click)
  else
    P->>P: toggle/show FloatingChatWindow
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • zerob13

Poem

I twitch my whiskers, clear the tick and start,
No double-trigger mischief, just a steady heart.
I hop, I nudge, I keep the chat in sight,
A tidy drag, a soft-righted flight. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/floating/FloatingButton.vue (2)

12-14: Avoid double mousemove handling

You bind @mousemove on the element and also add a document-level mousemove on mousedown. During drag both will fire, doubling work and risking jitter. Rely on the document listener only.

-      @mousemove="handleMouseMove"

No other changes required since you already add/remove document listeners in handleMouseDown/Up.

Also applies to: 81-83


31-62: Comments must be in English per repo guidelines

Several comments are Chinese. Please translate to English to comply with src/**/*.{ts,tsx,vue} guideline. Example at Line 93:

-    // 位移启动拖拽时,立即清除定时器,防止二次触发
+    // When starting drag due to displacement, clear the pending timer to prevent a second start

Also applies to: 241-342

🧹 Nitpick comments (4)
src/renderer/floating/FloatingButton.vue (4)

94-95: Prettier failure: remove stray semicolon/trailing space

Local style appears to be no-semi. Also remove trailing whitespace. Apply:

-    clearDragTimer(); 
+    clearDragTimer()

138-141: Safer timer null-check

Guard against a falsy timer id (defensive) and be explicit:

-  if (dragState.value.dragTimer) {
-    clearTimeout(dragState.value.dragTimer)
+  if (dragState.value.dragTimer !== null) {
+    window.clearTimeout(dragState.value.dragTimer)
     dragState.value.dragTimer = null
   }

205-210: Prevent stacked timeouts on mouse leave

Repeated enter/leave schedules multiple timers. Track and clear the previous one.

-  // 延迟恢复脉冲动画
-  setTimeout(() => {
-    isPulsing.value = true
-  }, 3000)
+  // Resume pulse after delay (deduplicated)
+  clearPulseTimer()
+  pulseTimer = window.setTimeout(() => {
+    isPulsing.value = true
+    pulseTimer = null
+  }, 3000)

Add near the top (outside this hunk):

let pulseTimer: number | null = null
const clearPulseTimer = () => {
  if (pulseTimer !== null) {
    window.clearTimeout(pulseTimer)
    pulseTimer = null
  }
}

And in onUnmounted(), call clearPulseTimer().


156-164: Prefer presenters over global window API

Guideline says: from renderer to main, call presenters via usePresenter.ts. If floatingButtonAPI is a bridge, consider wrapping it in a presenter/composable to keep renderer decoupled and testable.

Also applies to: 189-197, 221-224

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2944005 and 62428d4.

📒 Files selected for processing (1)
  • src/renderer/floating/FloatingButton.vue (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
src/renderer/**/*.{vue,ts,js,tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

渲染进程代码放在 src/renderer

Files:

  • src/renderer/floating/FloatingButton.vue
src/renderer/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (.cursor/rules/vue-shadcn.mdc)

src/renderer/**/*.{ts,tsx,vue}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use TypeScript for all code; prefer types over interfaces.
Avoid enums; use const objects instead.
Use arrow functions for methods and computed properties.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.

Files:

  • src/renderer/floating/FloatingButton.vue
src/renderer/**/*.{vue,ts}

📄 CodeRabbit inference engine (.cursor/rules/vue-shadcn.mdc)

Implement lazy loading for routes and components.

Files:

  • src/renderer/floating/FloatingButton.vue
src/renderer/**/*.{ts,vue}

📄 CodeRabbit inference engine (.cursor/rules/vue-shadcn.mdc)

src/renderer/**/*.{ts,vue}: Use useFetch and useAsyncData for data fetching.
Implement SEO best practices using Nuxt's useHead and useSeoMeta.

src/renderer/**/*.{ts,vue}: Use Pinia for frontend state management
From renderer to main, call presenters via the usePresenter.ts composable

Files:

  • src/renderer/floating/FloatingButton.vue
src/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use English for all logs and comments

Files:

  • src/renderer/floating/FloatingButton.vue
src/renderer/**/*.vue

📄 CodeRabbit inference engine (CLAUDE.md)

src/renderer/**/*.vue: Use Vue 3 Composition API for all components
Use Tailwind CSS with scoped styles for component styling

Files:

  • src/renderer/floating/FloatingButton.vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: neoragex2002
PR: ThinkInAIXYZ/deepchat#550
File: src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts:258-324
Timestamp: 2025-06-21T15:53:34.641Z
Learning: User neoragex2002 prefers concise, brief responses rather than chatty or verbose communication. Avoid lengthy explanations, excessive enthusiasm, or ASCII art in responses.
🪛 GitHub Actions: PR Check
src/renderer/floating/FloatingButton.vue

[error] 1-1: Prettier formatting check failed during 'pnpm run format:check' for this file. Run 'prettier --write' to fix.

🔇 Additional comments (3)
src/renderer/floating/FloatingButton.vue (3)

93-95: Good fix: timer race eliminated

Clearing the pending DRAG_DELAY timer before immediate drag start prevents double onDragStart. This addresses the “dragging jumps/erratic” issue.


1-343: Formatting pipeline failure

Run prettier to align with repo style:

  • pnpm run format:write

Ensure CI passes after the small style tweak on Line 94.


235-237: Don't call removeAllListeners() on window.floatingButtonAPI — remove only the handler you added

Removing all listeners on the shared API can break other components; unregister the specific event(s) you registered instead.

Suggested replacement:

-  if (window.floatingButtonAPI) {
-    window.floatingButtonAPI.removeAllListeners()
-  }
+  window.floatingButtonAPI?.off?.('configUpdate', handleConfigUpdate)
+  // or: window.floatingButtonAPI?.removeListener?.('configUpdate', handleConfigUpdate)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/presenter/windowPresenter/FloatingChatWindow.ts (1)

100-101: Respect config.alwaysOnTop; don’t force true.

You override the supplied config and always set on-top.

-      this.window.setAlwaysOnTop(true, 'floating')
+      this.window.setAlwaysOnTop(this.config.alwaysOnTop, 'floating')
src/main/presenter/floatingButtonPresenter/index.ts (2)

231-256: Clamp to current display’s workArea (uses 0-based coords now, breaks on multi-monitor).

Current logic ignores workArea.x/y and uses require in a hot path. Import screen once and clamp against the matching display.

-    ipcMain.on(FLOATING_BUTTON_EVENTS.DRAG_END, (_event, _data: { x: number; y: number }) => {
+    ipcMain.on(FLOATING_BUTTON_EVENTS.DRAG_END, (_event, _data: { x: number; y: number }) => {
       try {
         if (
           dragState &&
           dragState.isDragging &&
           this.floatingWindow &&
           this.floatingWindow.exists()
         ) {
           const buttonWindow = this.floatingWindow.getWindow()
           if (buttonWindow && !buttonWindow.isDestroyed()) {
-            // 简单边界检查
-            const { screen } = require('electron')
-            const primaryDisplay = screen.getPrimaryDisplay()
-            const { workArea } = primaryDisplay
-
-            const bounds = buttonWindow.getBounds()
-
-            // 确保悬浮球完全在屏幕范围内
-            const targetX = Math.max(0, Math.min(bounds.x, workArea.width - bounds.width))
-            const targetY = Math.max(0, Math.min(bounds.y, workArea.height - bounds.height))
+            // Bounds clamp within current display workArea
+            const bounds = buttonWindow.getBounds()
+            const display = screen.getDisplayMatching(bounds)
+            const { workArea } = display
+            const targetX = Math.max(
+              workArea.x,
+              Math.min(bounds.x, workArea.x + workArea.width - bounds.width)
+            )
+            const targetY = Math.max(
+              workArea.y,
+              Math.min(bounds.y, workArea.y + workArea.height - bounds.height)
+            )
 
             // 只有在越界时才调整位置
             if (targetX !== bounds.x || targetY !== bounds.y) {
               buttonWindow.setPosition(targetX, targetY)
             }
           }
         }

Additionally, import screen at the top:

-import { ipcMain, Menu, app } from 'electron'
+import { ipcMain, Menu, app, screen } from 'electron'

159-166: Also suppress context menu during drag.

Mirror the click guard to avoid menu popups mid-drag.

-    ipcMain.on(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED, () => {
+    ipcMain.on(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED, () => {
+      if (isDuringDragSession) {
+        return
+      }
       try {
         this.showContextMenu()
       } catch (error) {
         console.error('Failed to handle floating button right click:', error)
       }
     })
🧹 Nitpick comments (1)
src/main/presenter/floatingButtonPresenter/index.ts (1)

208-229: Optional: sanity-check incoming coordinates.

Validate that x/y are finite numbers before using them to move windows.

-    ipcMain.on(FLOATING_BUTTON_EVENTS.DRAG_MOVE, (_event, { x, y }: { x: number; y: number }) => {
+    ipcMain.on(FLOATING_BUTTON_EVENTS.DRAG_MOVE, (_event, { x, y }: { x: number; y: number }) => {
       try {
+        if (!Number.isFinite(x) || !Number.isFinite(y)) return
         if (
           dragState &&
           dragState.isDragging &&
           this.floatingWindow &&
           this.floatingWindow.exists()
         ) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 62428d4 and b4a7e30.

📒 Files selected for processing (3)
  • src/main/presenter/floatingButtonPresenter/index.ts (7 hunks)
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts (1 hunks)
  • src/renderer/floating/FloatingButton.vue (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/floating/FloatingButton.vue
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)

**/*.{js,jsx,ts,tsx}: 使用 OxLint 进行代码检查
Log和注释使用英文书写

Files:

  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts
src/{main,renderer}/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/electron-best-practices.mdc)

src/{main,renderer}/**/*.ts: Use context isolation for improved security
Implement proper inter-process communication (IPC) patterns
Optimize application startup time with lazy loading
Implement proper error handling and logging for debugging

Files:

  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts
src/main/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/electron-best-practices.mdc)

Use Electron's built-in APIs for file system and native dialogs

From main to renderer, broadcast events via EventBus using mainWindow.webContents.send()

Files:

  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/error-logging.mdc)

**/*.{ts,tsx}: 始终使用 try-catch 处理可能的错误
提供有意义的错误信息
记录详细的错误日志
优雅降级处理
日志应包含时间戳、日志级别、错误代码、错误描述、堆栈跟踪(如适用)、相关上下文信息
日志级别应包括 ERROR、WARN、INFO、DEBUG
不要吞掉错误
提供用户友好的错误信息
实现错误重试机制
避免记录敏感信息
使用结构化日志
设置适当的日志级别

Enable and adhere to strict TypeScript type checking

Files:

  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts
src/main/**/*.{ts,js,tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

主进程代码放在 src/main

Files:

  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts
src/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use English for all logs and comments

Files:

  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts
src/main/presenter/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain one presenter per functional domain in src/main/presenter/

Files:

  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/windowPresenter/FloatingChatWindow.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: neoragex2002
PR: ThinkInAIXYZ/deepchat#550
File: src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts:258-324
Timestamp: 2025-06-21T15:53:34.641Z
Learning: User neoragex2002 prefers concise, brief responses rather than chatty or verbose communication. Avoid lengthy explanations, excessive enthusiasm, or ASCII art in responses.
🧬 Code graph analysis (1)
src/main/presenter/floatingButtonPresenter/index.ts (1)
src/main/events.ts (1)
  • FLOATING_BUTTON_EVENTS (180-189)
⏰ 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). (1)
  • GitHub Check: build-check (x64)
🔇 Additional comments (2)
src/main/presenter/windowPresenter/FloatingChatWindow.ts (1)

90-96: Harden renderer security: enable webSecurity and sandbox.

webSecurity and sandbox are currently disabled (false), which weakens the renderer’s isolation—set both to true (even in dev) unless there’s a documented requirement to disable them.

src/main/presenter/floatingButtonPresenter/index.ts (1)

127-134: Good guard to suppress clicks during drag.

Prevents spurious toggles while dragging.

Comment on lines 191 to 193
console.log('FloatingChatWindow hidden during drag start')
}

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Use structured logger and English logs; replace console.*.

Repo guidelines require English logs and structured logging. Replace console.* with the shared logger for consistency.

+import logger from '../../../shared/logger'
@@
-      console.log('FloatingButtonPresenter.enable called, current enabled:', this.config.enabled, 'has window:', !!this.floatingWindow)
+      logger.info('FloatingButtonPresenter.enable called', { enabled: this.config.enabled, hasWindow: !!this.floatingWindow })
@@
-      console.log('FloatingButton window already exists, showing it')
+      logger.info('FloatingButton window already exists, showing it')
@@
-              console.log('FloatingChatWindow hidden during drag start')
+              logger.info('FloatingChatWindow hidden during drag start')
@@
-            console.log('FloatingChatWindow shown after drag end')
+            logger.info('FloatingChatWindow shown after drag end')
@@
-      console.error('Failed to handle drag end:', error)
+      logger.error('Failed to handle drag end', { error })

Note: please migrate remaining Chinese comments/logs to English in a follow-up sweep.

Also applies to: 281-282, 289-292, 73-90


🏁 Script executed:

#!/bin/bash
set -e

# Locate the floatingButtonPresenter index file
echo "===== Locating floatingButtonPresenter/index.ts ====="
FILE_PATH=$(fd floatingButtonPresenter/index.ts)

# Report if the file was found
if [[ -z "$FILE_PATH" ]]; then
  echo "ERROR: floatingButtonPresenter/index.ts not found"
  exit 1
fi

echo "Found at: $FILE_PATH"

# Search for console.log and console.error usages
echo "===== console.log / console.error occurrences ====="
rg -n "console\.(log|error)" "$FILE_PATH" || true

# Check for existing import of shared logger
echo "===== Existing logger import ====="
rg -n "import .*logger" "$FILE_PATH" || true

# Identify any Chinese characters in comments or logs
echo "===== Chinese characters in file ====="
rg -nP "[\u4e00-\u9fff]" "$FILE_PATH" || true

Length of output: 309


🏁 Script executed:

#!/bin/bash
set -e

# Find the floatingButtonPresenter directory
echo "===== Finding floatingButtonPresenter directory ====="
DIR=$(fd floatingButtonPresenter -t d)

if [[ -z "$DIR" ]]; then
  echo "ERROR: Directory floatingButtonPresenter not found"
  exit 1
fi

echo "Directory found at: $DIR"

# Confirm index.ts exists
INDEX_FILE="$DIR/index.ts"
if [[ ! -f "$INDEX_FILE" ]]; then
  echo "ERROR: index.ts not found in $DIR"
  exit 1
fi

echo "===== Searching for console.log / console.error usages in $INDEX_FILE ====="
rg -n "console\.(log|error)" "$INDEX_FILE" || true

echo "===== Checking for existing logger import in $INDEX_FILE ====="
rg -n "import .*logger" "$INDEX_FILE" || true

echo "===== Identifying Chinese characters in $INDEX_FILE ====="
rg -nP "[\u4e00-\u9fff]" "$INDEX_FILE" || true

Length of output: 3106


🏁 Script executed:

#!/bin/bash
set -e

FILE="src/main/presenter/floatingButtonPresenter/index.ts"
echo "===== Identifying Chinese characters in $FILE ====="
rg -nP '\p{Han}' "$FILE" || true

Length of output: 918


Replace all console. calls with shared logger and translate Chinese comments to English*
• In src/main/presenter/floatingButtonPresenter/index.ts (19 occurrences): remove console.log/console.error, import logger from ../../../shared/logger, and use logger.info/logger.error with structured args.
• In the same file: migrate all Chinese comments (e.g. 初始化悬浮按钮功能, 销毁悬浮按钮功能, etc.) to English in a follow-up sweep.

🤖 Prompt for AI Agents
In src/main/presenter/floatingButtonPresenter/index.ts around lines 191–193,
replace the console.log/console.error calls (there are 19 occurrences across the
file) with the shared logger: add import logger from '../../../shared/logger' at
the top, change console.log(...) to logger.info({ msg: 'FloatingChatWindow
hidden during drag start', context: 'floatingButtonPresenter', ...optionalFields
}) and console.error(...) to logger.error({ msg: '...', error, context:
'floatingButtonPresenter' }) using structured args; also convert all Chinese
comments in this file (e.g., 初始化悬浮按钮功能, 销毁悬浮按钮功能) to English equivalents in a
follow-up sweep for consistency.

Comment on lines 243 to +281
const primaryDisplay = screen.getPrimaryDisplay()
const { workArea } = primaryDisplay
let x: number, y: number
const windowWidth = this.window?.getBounds().width ?? this.config.size.width
const windowHeight = this.window?.getBounds().height ?? this.config.size.height

if (!floatingButtonPosition) {
x = workArea.x + workArea.width - this.config.size.width - 20
y = workArea.y + workArea.height - this.config.size.height - 20
const x = workArea.x + workArea.width - windowWidth - 20
const y = workArea.y + workArea.height - windowHeight - 20
return { x, y }
}

const buttonX = floatingButtonPosition.x
const buttonY = floatingButtonPosition.y
const buttonWidth = floatingButtonPosition.width
const buttonHeight = floatingButtonPosition.height
const windowWidth = this.config.size.width
const windowHeight = this.config.size.height
const gap = 15
const buttonCenterX = buttonX + buttonWidth / 2
const buttonCenterY = buttonY + buttonHeight / 2
const screenCenterX = workArea.x + workArea.width / 2
const screenCenterY = workArea.y + workArea.height / 2

let positions: Array<{ x: number; y: number; priority: number }> = []
if (buttonX + buttonWidth + gap + windowWidth <= workArea.x + workArea.width) {
positions.push({
x: buttonX + buttonWidth + gap,
y: Math.max(
workArea.y,
Math.min(
buttonY + (buttonHeight - windowHeight) / 2,
workArea.y + workArea.height - windowHeight
)
),
priority: buttonCenterX < screenCenterX ? 1 : 3
})
}
const buttonBounds = floatingButtonPosition

if (buttonX - gap - windowWidth >= workArea.x) {
positions.push({
x: buttonX - gap - windowWidth,
y: Math.max(
workArea.y,
Math.min(
buttonY + (buttonHeight - windowHeight) / 2,
workArea.y + workArea.height - windowHeight
)
),
priority: buttonCenterX >= screenCenterX ? 1 : 3
})
}
let finalX: number

if (buttonY + buttonHeight + gap + windowHeight <= workArea.y + workArea.height) {
positions.push({
x: Math.max(
workArea.x,
Math.min(
buttonX + (buttonWidth - windowWidth) / 2,
workArea.x + workArea.width - windowWidth
)
),
y: buttonY + buttonHeight + gap,
priority: buttonCenterY < screenCenterY ? 2 : 4
})
// 1. Prioritize placing the window on the right side of the button
const rightPositionX = buttonBounds.x + buttonBounds.width + gap
if (rightPositionX + windowWidth <= workArea.x + workArea.width) {
finalX = rightPositionX
} else {
// 2. If right side has no space, try the left side
const leftPositionX = buttonBounds.x - windowWidth - gap
if (leftPositionX >= workArea.x) {
finalX = leftPositionX
} else {
// 3. Fallback: If both sides lack space, align window's right edge with screen's right edge.
finalX = workArea.x + workArea.width - windowWidth
}
}

if (buttonY - gap - windowHeight >= workArea.y) {
positions.push({
x: Math.max(
workArea.x,
Math.min(
buttonX + (buttonWidth - windowWidth) / 2,
workArea.x + workArea.width - windowWidth
)
),
y: buttonY - gap - windowHeight,
priority: buttonCenterY >= screenCenterY ? 2 : 4
})
}
// Calculate vertical position: try to center with the button, but stay within screen bounds.
const idealY = buttonBounds.y + (buttonBounds.height - windowHeight) / 2
const finalY = Math.max(
workArea.y,
Math.min(idealY, workArea.y + workArea.height - windowHeight)
)

if (positions.length === 0) {
x = workArea.x + workArea.width - windowWidth - 20
y = workArea.y + workArea.height - windowHeight - 20
} else {
positions.sort((a, b) => a.priority - b.priority)
x = positions[0].x
y = positions[0].y
}
x = Math.max(workArea.x + 10, Math.min(x, workArea.x + workArea.width - windowWidth - 10))
y = Math.max(workArea.y + 10, Math.min(y, workArea.y + workArea.height - windowHeight - 10))
return { x, y }
return { x: Math.round(finalX), y: Math.round(finalY) }
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Fix multi-display placement: choose display nearest to button/window, not primary.

Using the primary display misplaces/clamps when the button/window is on another monitor or when workArea has non-zero x/y. Select the display via getDisplayMatching and then derive workArea.

-    const primaryDisplay = screen.getPrimaryDisplay()
-    const { workArea } = primaryDisplay
+    const display = floatingButtonPosition
+      ? screen.getDisplayMatching({
+          x: floatingButtonPosition.x,
+          y: floatingButtonPosition.y,
+          width: floatingButtonPosition.width,
+          height: floatingButtonPosition.height
+        })
+      : (this.window && !this.window.isDestroyed()
+          ? screen.getDisplayMatching(this.window.getBounds())
+          : screen.getPrimaryDisplay())
+    const { workArea } = display
📝 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
const primaryDisplay = screen.getPrimaryDisplay()
const { workArea } = primaryDisplay
let x: number, y: number
const windowWidth = this.window?.getBounds().width ?? this.config.size.width
const windowHeight = this.window?.getBounds().height ?? this.config.size.height
if (!floatingButtonPosition) {
x = workArea.x + workArea.width - this.config.size.width - 20
y = workArea.y + workArea.height - this.config.size.height - 20
const x = workArea.x + workArea.width - windowWidth - 20
const y = workArea.y + workArea.height - windowHeight - 20
return { x, y }
}
const buttonX = floatingButtonPosition.x
const buttonY = floatingButtonPosition.y
const buttonWidth = floatingButtonPosition.width
const buttonHeight = floatingButtonPosition.height
const windowWidth = this.config.size.width
const windowHeight = this.config.size.height
const gap = 15
const buttonCenterX = buttonX + buttonWidth / 2
const buttonCenterY = buttonY + buttonHeight / 2
const screenCenterX = workArea.x + workArea.width / 2
const screenCenterY = workArea.y + workArea.height / 2
let positions: Array<{ x: number; y: number; priority: number }> = []
if (buttonX + buttonWidth + gap + windowWidth <= workArea.x + workArea.width) {
positions.push({
x: buttonX + buttonWidth + gap,
y: Math.max(
workArea.y,
Math.min(
buttonY + (buttonHeight - windowHeight) / 2,
workArea.y + workArea.height - windowHeight
)
),
priority: buttonCenterX < screenCenterX ? 1 : 3
})
}
const buttonBounds = floatingButtonPosition
if (buttonX - gap - windowWidth >= workArea.x) {
positions.push({
x: buttonX - gap - windowWidth,
y: Math.max(
workArea.y,
Math.min(
buttonY + (buttonHeight - windowHeight) / 2,
workArea.y + workArea.height - windowHeight
)
),
priority: buttonCenterX >= screenCenterX ? 1 : 3
})
}
let finalX: number
if (buttonY + buttonHeight + gap + windowHeight <= workArea.y + workArea.height) {
positions.push({
x: Math.max(
workArea.x,
Math.min(
buttonX + (buttonWidth - windowWidth) / 2,
workArea.x + workArea.width - windowWidth
)
),
y: buttonY + buttonHeight + gap,
priority: buttonCenterY < screenCenterY ? 2 : 4
})
// 1. Prioritize placing the window on the right side of the button
const rightPositionX = buttonBounds.x + buttonBounds.width + gap
if (rightPositionX + windowWidth <= workArea.x + workArea.width) {
finalX = rightPositionX
} else {
// 2. If right side has no space, try the left side
const leftPositionX = buttonBounds.x - windowWidth - gap
if (leftPositionX >= workArea.x) {
finalX = leftPositionX
} else {
// 3. Fallback: If both sides lack space, align window's right edge with screen's right edge.
finalX = workArea.x + workArea.width - windowWidth
}
}
if (buttonY - gap - windowHeight >= workArea.y) {
positions.push({
x: Math.max(
workArea.x,
Math.min(
buttonX + (buttonWidth - windowWidth) / 2,
workArea.x + workArea.width - windowWidth
)
),
y: buttonY - gap - windowHeight,
priority: buttonCenterY >= screenCenterY ? 2 : 4
})
}
// Calculate vertical position: try to center with the button, but stay within screen bounds.
const idealY = buttonBounds.y + (buttonBounds.height - windowHeight) / 2
const finalY = Math.max(
workArea.y,
Math.min(idealY, workArea.y + workArea.height - windowHeight)
)
if (positions.length === 0) {
x = workArea.x + workArea.width - windowWidth - 20
y = workArea.y + workArea.height - windowHeight - 20
} else {
positions.sort((a, b) => a.priority - b.priority)
x = positions[0].x
y = positions[0].y
}
x = Math.max(workArea.x + 10, Math.min(x, workArea.x + workArea.width - windowWidth - 10))
y = Math.max(workArea.y + 10, Math.min(y, workArea.y + workArea.height - windowHeight - 10))
return { x, y }
return { x: Math.round(finalX), y: Math.round(finalY) }
// Choose the display that contains the floating button (or window), falling back to primary
const display = floatingButtonPosition
? screen.getDisplayMatching({
x: floatingButtonPosition.x,
y: floatingButtonPosition.y,
width: floatingButtonPosition.width,
height: floatingButtonPosition.height
})
: (this.window && !this.window.isDestroyed()
? screen.getDisplayMatching(this.window.getBounds())
: screen.getPrimaryDisplay())
const { workArea } = display
const windowWidth = this.window?.getBounds().width ?? this.config.size.width
const windowHeight = this.window?.getBounds().height ?? this.config.size.height
if (!floatingButtonPosition) {
const x = workArea.x + workArea.width - windowWidth - 20
const y = workArea.y + workArea.height - windowHeight - 20
return { x, y }
}
const gap = 15
const buttonBounds = floatingButtonPosition
let finalX: number
// 1. Prioritize placing the window on the right side of the button
const rightPositionX = buttonBounds.x + buttonBounds.width + gap
if (rightPositionX + windowWidth <= workArea.x + workArea.width) {
finalX = rightPositionX
} else {
// 2. If right side has no space, try the left side
const leftPositionX = buttonBounds.x - windowWidth - gap
if (leftPositionX >= workArea.x) {
finalX = leftPositionX
} else {
// 3. Fallback: align the window’s right edge with the display’s right edge
finalX = workArea.x + workArea.width - windowWidth
}
}
// Calculate vertical position: center with the button but clamp to display
const idealY = buttonBounds.y + (buttonBounds.height - windowHeight) / 2
const finalY = Math.max(
workArea.y,
Math.min(idealY, workArea.y + workArea.height - windowHeight)
)
return { x: Math.round(finalX), y: Math.round(finalY) }
🤖 Prompt for AI Agents
In src/main/presenter/windowPresenter/FloatingChatWindow.ts around lines
243-281, replace use of screen.getPrimaryDisplay() with logic that picks the
display containing the button or window using screen.getDisplayMatching, so
calculations use the correct workArea for that display: if
floatingButtonPosition is present, call getDisplayMatching with the button
bounds; else if this.window exists, call getDisplayMatching with
this.window.getBounds(); otherwise fallback to getPrimaryDisplay(); then derive
workArea from that selected display and continue the existing placement logic so
x/y are computed relative to the correct display.

Copy link
Collaborator

@yyhhyyyyyy yyhhyyyyyy left a comment

Choose a reason for hiding this comment

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

LGTM

@yyhhyyyyyy yyhhyyyyyy merged commit e6de02f into ThinkInAIXYZ:dev Aug 29, 2025
2 checks passed
@neoragex2002 neoragex2002 deleted the PR branch August 29, 2025 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants