Skip to content

feat(desktop): drag-to-resize columns, double-click reset, title wrap (#119)#125

Merged
legeling merged 2 commits into
legeling:mainfrom
TuTouPower:feat/issue-119-resize-columns-wrap
May 10, 2026
Merged

feat(desktop): drag-to-resize columns, double-click reset, title wrap (#119)#125
legeling merged 2 commits into
legeling:mainfrom
TuTouPower:feat/issue-119-resize-columns-wrap

Conversation

@TuTouPower
Copy link
Copy Markdown
Contributor

@TuTouPower TuTouPower commented May 9, 2026

Summary

Three small usability wins asked for in #119, landed together.

  1. Drag-to-resize sidebar columns. The folder sidebar and the card-view prompt list pane had hard-coded Tailwind widths (`w-72` / `w-80`), which wasted screen real estate on 4K displays. They are now fully resizable via a vertical drag handle and persist across sessions via the existing Zustand `localStorage`. Widths clamp to safe bounds so a runaway drag cannot hide the UI.

  2. Double-click / keyboard reset. Double-clicking the drag handle restores the column to its default width. Keyboard users can nudge with ArrowLeft / ArrowRight (Shift = 64 px steps) and reset with Enter / Space / Home / End. Fully ARIA-labelled as `role="separator" aria-orientation="vertical"` with live `aria-valuenow` for assistive tech.

  3. Title auto-wrap in list / card / gallery views. Prompt titles in PromptCard (card-view list pane), PromptListView, and PromptGalleryView previously used `truncate`, so titles longer than the column were silently cut off. They now use `line-clamp-2 break-words` — users see a second line before ellipsis, with the full title still available via the `title=""` tooltip.

Architecture

  • New reusable `` under `components/ui/` so future panes can opt in trivially.
  • New `sidebarPanelWidth` / `promptListPaneWidth` fields in `useUIStore`, with a `merge` function that re-clamps out-of-range persisted values, so schema drift cannot leave the layout stuck in a broken state.

Tests

  • `tests/unit/stores/ui-columns.test.ts` — 8 tests: defaults match the legacy Tailwind widths, clamping on both ends, NaN / Infinity reject to min (defensive), reset round-trips, and the persist `merge` repair for out-of-range localStorage values.
  • `tests/unit/components/column-resizer.test.tsx` — 7 tests: pointer drag math, clamp during drag, primary-button filtering (right-click does not start a drag), double-click reset, keyboard step + reset, ARIA attribute exposure.

Verification

  • `pnpm lint` → clean
  • `pnpm test -- --run tests/unit/components/column-resizer.test.tsx tests/unit/stores/ui-columns.test.ts` → 15 / 15 passing

Notes

Refs #119

Summary by CodeRabbit

  • 新功能

    • 侧边栏与提示列表面板支持拖拽调整并记住宽度(含重置功能)
    • 提示标题与描述改为最多两行显示,提升可读性
    • 添加键盘操作与无障碍支持以调整面板宽度
    • 扩展多语言文案以支持面板调整的 ARIA 文本
  • 测试

    • 为面板拖拽、键盘交互与持久化行为新增单元测试

Review Change Stack

…legeling#119)

Three small usability wins asked for in issue legeling#119, landed together:

1. **Drag-to-resize sidebar columns.** The folder sidebar (`<aside>`
   panel) and the card-view prompt list pane both had hard-coded
   Tailwind widths (`w-72` / `w-80`), which wasted screen real estate
   on 4K displays. They are now fully resizable via a vertical drag
   handle and persist across sessions via the existing Zustand
   localStorage. The handles clamp to safe bounds so a runaway drag
   can't hide the UI.

2. **Double-click / keyboard reset.** Double-clicking the drag handle
   restores the column to its default width. Keyboard users can nudge
   with ArrowLeft / ArrowRight (Shift for 64 px steps) and reset with
   Enter / Space / Home / End. Fully ARIA-labelled as
   `role="separator" aria-orientation="vertical"` with live
   `aria-valuenow` for assistive tech.

3. **Title auto-wrap in list/card/gallery views.** Prompt titles in
   PromptCard (the card-view list pane), PromptListView, and
   PromptGalleryView previously used `truncate`, so titles longer
   than the column were silently cut off. They now use
   `line-clamp-2 break-words` so users see a second line before
   ellipsis, with the full title still available via the `title=""`
   tooltip.

### Architecture

- Added a reusable `<ColumnResizer />` primitive under
  `components/ui/` so future panes can opt in trivially.
- Added `sidebarPanelWidth` / `promptListPaneWidth` to `useUIStore`
  with a `merge` function that re-clamps out-of-range persisted
  values, so older / future schema drift cannot leave the layout
  stuck in a broken state.

### Tests

- `tests/unit/stores/ui-columns.test.ts` (8 tests): defaults match the
  legacy Tailwind widths, clamping on both ends, NaN / Infinity reject
  to min (defensive), reset round-trips, and the persist `merge`
  repair for out-of-range localStorage values.
- `tests/unit/components/column-resizer.test.tsx` (7 tests): pointer
  drag math, clamp during drag, primary-button filtering, double-click
  reset, keyboard step + reset, and ARIA exposure.

### Verification

- `pnpm lint` → clean
- `pnpm test -- --run tests/unit/components/column-resizer.test.tsx tests/unit/stores/ui-columns.test.ts`
  → 15/15 passing

### Notes

- legeling#113 (card-view inline edit) was already addressed by a prior change
  in this repo — see the card-detail-inline-edit change folder.
- No IPC, schema, or data migration changes.

Refs legeling#119
@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

@TuTouPower is attempting to deploy a commit to the legeling's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23c85a29-350f-40f0-bcce-f3e3203e6f80

📥 Commits

Reviewing files that changed from the base of the PR and between c0116da and 29f9b8a.

📒 Files selected for processing (5)
  • apps/desktop/src/renderer/components/layout/MainContent.tsx
  • apps/desktop/src/renderer/components/layout/Sidebar.tsx
  • apps/desktop/src/renderer/components/ui/ColumnResizer.tsx
  • apps/desktop/src/renderer/stores/ui.store.ts
  • apps/desktop/tests/unit/stores/ui-columns.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/desktop/src/renderer/components/layout/Sidebar.tsx
  • apps/desktop/src/renderer/components/layout/MainContent.tsx
  • apps/desktop/src/renderer/stores/ui.store.ts
  • apps/desktop/src/renderer/components/ui/ColumnResizer.tsx
  • apps/desktop/tests/unit/stores/ui-columns.test.ts

📝 Walkthrough

Walkthrough

该 PR 为桌面端添加持久化的可调整列宽(侧边栏与提示列表),引入 ColumnResizer 组件并在 MainContent 与 Sidebar 中集成;同时将提示标题/描述改为两行显示并新增相关多语言 ARIA 字符串与单元测试。

变更

可调整列宽和文本包装

层级 / 文件 摘要
UI存储架构
apps/desktop/src/renderer/stores/ui.store.ts
引入列宽常量(边界值和默认值)、clamp辅助函数、UIState扩展(sidebarPanelWidthpromptListPaneWidth及其setter)、resetColumnWidths操作,以及支持持久化和恢复的自定义merge函数。
ColumnResizer组件
apps/desktop/src/renderer/components/ui/ColumnResizer.tsx
新增可访问的分隔符组件,支持指针拖动、键盘箭头步进和双击重置,所有宽度值都被钳制在[min, max]范围内。
MainContent集成
apps/desktop/src/renderer/components/layout/MainContent.tsx
导入列宽常量和ColumnResizer,连接store读取promptListPaneWidth,将窗格从固定宽度改为内联动态宽度,添加可拖动调整句柄。
Sidebar集成
apps/desktop/src/renderer/components/layout/Sidebar.tsx
导入列宽常量和ColumnResizer,连接store读取sidebarPanelWidth,将面板从固定Tailwind宽度改为内联动态宽度(panel布局),添加绝对定位调整句柄。
文本显示优化
apps/desktop/src/renderer/components/layout/MainContent.tsx, apps/desktop/src/renderer/components/prompt/PromptGalleryView.tsx, apps/desktop/src/renderer/components/prompt/PromptListView.tsx
更新提示卡片和列表中的标题/描述从单行截断(truncate)改为双行限制(line-clamp-2)和break-words
国际化支持
apps/desktop/src/renderer/i18n/locales/*.json
为调整大小句柄的ARIA标签在英语、德语、西班牙语、法语、日语、繁体中文和简体中文中添加prompt.resizeListPaneAria翻译。
ColumnResizer测试
apps/desktop/tests/unit/components/column-resizer.test.tsx
覆盖指针拖动、边界钳制、非主要按钮排除、双击重置、键盘箭头步进(含Shift修饰符)、重置快捷键(Home/End/Enter/Space)和ARIA属性验证。
UI存储测试
apps/desktop/tests/unit/stores/ui-columns.test.ts
验证默认宽度匹配、setter钳制行为、范围内值接纳、非有限值防御处理、resetColumnWidths恢复、以及持久化重新水化时的边界钳制。

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ColumnResizer
  participant UIStore
  participant Layout (MainContent/Sidebar)
  User->>ColumnResizer: pointerDown / drag / keyDown / doubleClick
  ColumnResizer->>ColumnResizer: compute delta & clamp
  ColumnResizer->>UIStore: onResize(clampedWidth)
  UIStore->>Layout: provide updated width (rehydrate/persist)
  Layout->>User: updated pane width applied (CSS var)
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 我在代码间蹦跳着,
推拽列宽如翻书页,
文字不再被截断挤,
两行温柔见全景,
国际化与测试一并随。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 标题清晰准确地总结了主要变更:添加拖动调整列宽功能、双击重置和标题换行,与文件变更内容完全匹配。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

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

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add drag-to-resize columns, double-click reset, and title wrapping

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add drag-to-resize functionality for sidebar and prompt list panes
  - Vertical drag handles with pointer, keyboard, and double-click support
  - Widths persist across sessions via Zustand localStorage with clamping
• Replace truncated titles with line-clamp-2 for better visibility
  - Applied to PromptCard, PromptListView, and PromptGalleryView
  - Full titles available via tooltip attribute
• New reusable ColumnResizer component for future resizable panes
  - ARIA-labeled with live aria-valuenow for accessibility
  - Supports ArrowLeft/ArrowRight (Shift for 64px steps) and reset keys
• Add comprehensive unit tests for store and component behavior
  - 8 tests for column width clamping and persistence merge logic
  - 7 tests for pointer drag, keyboard, double-click, and ARIA attributes
Diagram
flowchart LR
  A["User drags handle"] --> B["ColumnResizer calculates delta"]
  B --> C["Clamp to min/max bounds"]
  C --> D["Call onResize callback"]
  D --> E["Update store width"]
  E --> F["Persist to localStorage"]
  G["Double-click handle"] --> H["Reset to defaultWidth"]
  H --> D
  I["Keyboard ArrowLeft/Right"] --> J["Step 16px or 64px with Shift"]
  J --> D
  K["Truncated titles"] --> L["Replace with line-clamp-2"]
  L --> M["Show 2 lines before ellipsis"]
Loading

Grey Divider

File Changes

1. apps/desktop/src/renderer/stores/ui.store.ts ✨ Enhancement +82/-1

Add resizable column width state and persistence

apps/desktop/src/renderer/stores/ui.store.ts


2. apps/desktop/tests/unit/stores/ui-columns.test.ts 🧪 Tests +125/-0

Add regression tests for column width clamping

apps/desktop/tests/unit/stores/ui-columns.test.ts


3. apps/desktop/src/renderer/components/ui/ColumnResizer.tsx ✨ Enhancement +214/-0

Create reusable drag-to-resize handle component

apps/desktop/src/renderer/components/ui/ColumnResizer.tsx


View more (13)
4. apps/desktop/tests/unit/components/column-resizer.test.tsx 🧪 Tests +117/-0

Add behavioral tests for ColumnResizer component

apps/desktop/tests/unit/components/column-resizer.test.tsx


5. apps/desktop/src/renderer/components/layout/MainContent.tsx ✨ Enhancement +33/-3

Integrate resizable prompt list pane with drag handle

apps/desktop/src/renderer/components/layout/MainContent.tsx


6. apps/desktop/src/renderer/components/layout/Sidebar.tsx ✨ Enhancement +35/-2

Integrate resizable sidebar panel with drag handle

apps/desktop/src/renderer/components/layout/Sidebar.tsx


7. apps/desktop/src/renderer/components/prompt/PromptCard.tsx ✨ Enhancement +0/-0

Replace truncate with line-clamp-2 for title wrapping

apps/desktop/src/renderer/components/prompt/PromptCard.tsx


8. apps/desktop/src/renderer/components/prompt/PromptListView.tsx ✨ Enhancement +7/-3

Replace truncate with line-clamp-2 for title wrapping

apps/desktop/src/renderer/components/prompt/PromptListView.tsx


9. apps/desktop/src/renderer/components/prompt/PromptGalleryView.tsx ✨ Enhancement +4/-1

Replace truncate with line-clamp-2 for title wrapping

apps/desktop/src/renderer/components/prompt/PromptGalleryView.tsx


10. apps/desktop/src/renderer/i18n/locales/en.json 📝 Documentation +1/-0

Add English translation for resize aria label

apps/desktop/src/renderer/i18n/locales/en.json


11. apps/desktop/src/renderer/i18n/locales/de.json 📝 Documentation +1/-0

Add German translation for resize aria label

apps/desktop/src/renderer/i18n/locales/de.json


12. apps/desktop/src/renderer/i18n/locales/es.json 📝 Documentation +1/-0

Add Spanish translation for resize aria label

apps/desktop/src/renderer/i18n/locales/es.json


13. apps/desktop/src/renderer/i18n/locales/fr.json 📝 Documentation +1/-0

Add French translation for resize aria label

apps/desktop/src/renderer/i18n/locales/fr.json


14. apps/desktop/src/renderer/i18n/locales/ja.json 📝 Documentation +1/-0

Add Japanese translation for resize aria label

apps/desktop/src/renderer/i18n/locales/ja.json


15. apps/desktop/src/renderer/i18n/locales/zh-TW.json 📝 Documentation +1/-0

Add Traditional Chinese translation for resize aria label

apps/desktop/src/renderer/i18n/locales/zh-TW.json


16. apps/desktop/src/renderer/i18n/locales/zh.json 📝 Documentation +1/-0

Add Simplified Chinese translation for resize aria label

apps/desktop/src/renderer/i18n/locales/zh.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 9, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Inline style used ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The PR introduces React inline styles for layout widths and sizing, which violates the Tailwind-only
styling requirement. This reduces theme consistency and makes styling harder to audit/enforce.
Code

apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[R177-201]

+  const style: CSSProperties = {
+    // Hit-testable wider than the visible bar so the handle is easy to grab.
+    // 可点击区域比可见条宽,便于瞄准。
+    width: 8,
+    flexShrink: 0,
+    touchAction: "none",
+  };
+
+  return (
+    <div
+      role="separator"
+      aria-orientation="vertical"
+      aria-valuenow={Math.round(currentWidth)}
+      aria-valuemin={min}
+      aria-valuemax={max}
+      aria-label={ariaLabel}
+      tabIndex={0}
+      onPointerDown={handlePointerDown}
+      onPointerMove={handlePointerMove}
+      onPointerUp={endDrag}
+      onPointerCancel={endDrag}
+      onDoubleClick={handleDoubleClick}
+      onKeyDown={handleKeyDown}
+      style={style}
+      className={`group relative flex cursor-col-resize items-stretch outline-none focus-visible:bg-primary/20 ${className}`}
Evidence
PR Compliance ID 30 forbids inline styles, but the new resizer and resized panes apply style={...}
for width/sizing.

AGENTS.md
apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[177-201]
apps/desktop/src/renderer/components/layout/MainContent.tsx[1640-1643]
apps/desktop/src/renderer/components/layout/Sidebar.tsx[516-519]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Inline `style` props were added for pane widths and the resizer hit area, but compliance requires Tailwind-only styling.
## Issue Context
Resizable panes currently pass pixel widths via React inline styles.
## Fix Focus Areas
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[177-201]
- apps/desktop/src/renderer/components/layout/MainContent.tsx[1640-1643]
- apps/desktop/src/renderer/components/layout/Sidebar.tsx[516-519]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Chinese text in comments ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Chinese characters were added to non-locale source files (in comments), which violates the rule
prohibiting Chinese characters outside locale resources. This can create untracked localization
drift and fails text-audit requirements.
Code

apps/desktop/src/renderer/components/layout/MainContent.tsx[R241-243]

+  // Resizable prompt-list pane width (#119)
+  // 可拖拽的 Prompt 列表栏宽度 (#119)
+  const promptListPaneWidth = useUIStore((state) => state.promptListPaneWidth);
Evidence
PR Compliance ID 6 requires Chinese characters to appear only in locale/translation files, but the
PR adds Chinese text in comments within renderer source files.

AGENTS.md
apps/desktop/src/renderer/components/layout/MainContent.tsx[241-243]
apps/desktop/src/renderer/components/layout/Sidebar.tsx[254-257]
apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[22-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Chinese characters were added in source-code comments outside locale JSON files.
## Issue Context
Compliance requires Chinese characters to exist only in locale resources.
## Fix Focus Areas
- apps/desktop/src/renderer/components/layout/MainContent.tsx[241-243]
- apps/desktop/src/renderer/components/layout/Sidebar.tsx[254-257]
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[22-24]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Empty catch blocks ✓ Resolved 📘 Rule violation ◔ Observability
Description
The new ColumnResizer swallows exceptions with no logging/handling, creating silent failures that
are difficult to diagnose. This violates the requirement to avoid empty catches and silent error
suppression.
Code

apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[R106-112]

+      try {
+        (event.currentTarget as HTMLDivElement).setPointerCapture?.(
+          event.pointerId,
+        );
+      } catch {
+        // noop — pointer capture is a nice-to-have for sticky drags
+      }
Evidence
PR Compliance ID 15 prohibits empty catch blocks/silent failures, but the code catches errors and
does nothing (noop) in multiple places.

AGENTS.md
apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[106-112]
apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[133-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ColumnResizer` uses `catch {}` / noop catches that silently swallow pointer-capture errors.
## Issue Context
Even if failures are non-fatal, compliance requires catch blocks to either rethrow, log with context, or otherwise handle the error meaningfully.
## Fix Focus Areas
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[106-112]
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[133-142]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Unjustified as assertions 📘 Rule violation ⚙ Maintainability
Description
The PR introduces as type assertions without an explanatory justification comment, which violates
the casting restriction rule. This can hide real typing issues and reduce type safety guarantees.
Code

apps/desktop/src/renderer/stores/ui.store.ts[R108-122]

+        const merged = { ...current, ...(persisted as Partial<UIState>) };
+        return {
+          ...merged,
+          sidebarPanelWidth: clamp(
+            merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT,
+            SIDEBAR_PANEL_WIDTH_MIN,
+            SIDEBAR_PANEL_WIDTH_MAX,
+          ),
+          promptListPaneWidth: clamp(
+            merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT,
+            PROMPT_LIST_PANE_WIDTH_MIN,
+            PROMPT_LIST_PANE_WIDTH_MAX,
+          ),
+        } as UIState;
+      },
Evidence
PR Compliance ID 12 requires as assertions to be necessary and include a justification comment,
but new assertions are added without rationale.

AGENTS.md
apps/desktop/src/renderer/stores/ui.store.ts[108-122]
apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[107-109]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New `as` assertions were introduced without justification comments.
## Issue Context
Compliance requires either removing unnecessary assertions or documenting why the cast is required (interop/unsafe boundary).
## Fix Focus Areas
- apps/desktop/src/renderer/stores/ui.store.ts[108-122]
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[107-109]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Hardcoded ARIA label fallback 📘 Rule violation ⚙ Maintainability
Description
A user-facing ARIA label fallback string is hardcoded in renderer code instead of relying solely on
i18next keys. This violates the requirement that all user-visible text be produced via t() with
translation resources (no hardcoded strings).
Code

apps/desktop/src/renderer/components/layout/Sidebar.tsx[R1420-1427]

+          <ColumnResizer
+            currentWidth={sidebarPanelWidth}
+            min={SIDEBAR_PANEL_WIDTH_MIN}
+            max={SIDEBAR_PANEL_WIDTH_MAX}
+            defaultWidth={SIDEBAR_PANEL_WIDTH_DEFAULT}
+            onResize={setSidebarPanelWidth}
+            ariaLabel={t('sidebar.resizeAria', 'Resize folder sidebar')}
+          />
Evidence
PR Compliance ID 5 prohibits hardcoded user-facing strings; the new ariaLabel provides a hardcoded
default value (Resize folder sidebar).

AGENTS.md
apps/desktop/src/renderer/components/layout/Sidebar.tsx[1420-1427]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The ARIA label uses `t('...', '...')` with a hardcoded fallback string, which violates the no-hardcoded-strings i18n requirement.
## Issue Context
ARIA labels are user-facing (assistive tech) and must be localized.
## Fix Focus Areas
- apps/desktop/src/renderer/components/layout/Sidebar.tsx[1420-1427]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Conflicting sidebar transitions ✓ Resolved 🐞 Bug ➹ Performance
Description
Sidebar panel resizing adds an inline width style while the <aside> still includes transition-all,
and panel styling also adds transition-[opacity,transform]; depending on Tailwind CSS rule
ordering, transition-all can still win and animate width updates during drag. This can cause
resize lag/jank and makes the transition behavior fragile.
Code

apps/desktop/src/renderer/components/layout/Sidebar.tsx[R254-269]

+  // Tailwind can't inline arbitrary dynamic values, so for the resizable
+  // panel layout we supply width via inline style.
+  // 可拖拽的 panel 布局宽度通过内联 style 设置。
+  const panelStyleWidth = layout === 'panel' && !isCollapsed
+    ? { width: sidebarPanelWidth }
+    : undefined;
const asideClassName =
  layout === 'rail'
    ? `${railWidthClass} border-r border-sidebar-border/60 bg-sidebar-accent/25`
    : layout === 'panel'
-        ? `border-r border-sidebar-border bg-sidebar-background/85 app-wallpaper-panel-strong transition-[width,opacity,transform] duration-300 ease-out ${
+        ? `border-r border-sidebar-border bg-sidebar-background/85 app-wallpaper-panel-strong transition-[opacity,transform] duration-300 ease-out ${
          isCollapsed
            ? 'w-0 -translate-x-4 opacity-0 pointer-events-none border-r-0'
-              : 'w-72 translate-x-0 opacity-100'
+              : 'translate-x-0 opacity-100'
        }`
      : `border-r border-sidebar-border app-left-rail-glass app-wallpaper-panel-strong ${
Evidence
The sidebar panel width is now set via inline style (panelStyleWidth), but the rendered <aside>
still applies transition-all; separately, the panel layout string includes
transition-[opacity,transform], creating conflicting transition-property utilities on the same
element.

apps/desktop/src/renderer/components/layout/Sidebar.tsx[254-271]
apps/desktop/src/renderer/components/layout/Sidebar.tsx[516-520]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The sidebar `<aside>` currently combines `transition-all` with panel-specific `transition-[opacity,transform]` while also updating width via inline styles during resizing. This can inadvertently animate width changes and cause drag lag/jank depending on Tailwind’s generated CSS ordering.
### Issue Context
The panel layout is the one being resized (`style={{ width: sidebarPanelWidth }}`), so it should avoid any width transitions during drag, while other layouts may still want width transitions for collapse/expand.
### Fix Focus Areas
- apps/desktop/src/renderer/components/layout/Sidebar.tsx[254-271]
- apps/desktop/src/renderer/components/layout/Sidebar.tsx[516-520]
### Suggested fix
Make the `<aside>` transition class conditional by layout, e.g. use `transition-[opacity,transform]` (or similar) for `layout === 'panel'`, and keep `transition-all` only for non-panel layouts that rely on width transitions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Resizer ends wrong pointer 🐞 Bug ☼ Reliability
Description
ColumnResizer’s endDrag clears drag state on any pointerup/pointercancel without verifying the
pointerId matches the active drag. In multi-pointer scenarios (e.g., trackpad + touch, or
multi-touch), a different pointer ending can terminate the resize unexpectedly.
Code

apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[R129-145]

+  const endDrag = useCallback(
+    (event: ReactPointerEvent<HTMLDivElement>) => {
+      const state = dragStateRef.current;
+      if (!state) return;
+      try {
+        (event.currentTarget as HTMLDivElement).releasePointerCapture?.(
+          state.pointerId,
+        );
+      } catch {
+        // releasePointerCapture can throw if capture was already lost (for
+        // example the pointer was canceled). That's fine — we just want to
+        // reset local state.
+        // releasePointerCapture 在已经失去捕获时会抛错,这里忽略即可。
+      }
+      dragStateRef.current = null;
+      setIsDragging(false);
+    },
Evidence
handlePointerMove ignores events from a different pointerId, but endDrag does not; this
asymmetry allows unrelated pointer events to cancel an active drag.

apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[118-127]
apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[129-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`endDrag` should only end the drag for the pointer that started it. Right now it clears drag state for any pointerup/cancel event.
### Issue Context
`handlePointerMove` already validates `event.pointerId`, so `endDrag` should be consistent to avoid multi-pointer interference.
### Fix Focus Areas
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[129-147]
### Suggested fix
Add a guard:
- `if (!state || state.pointerId !== event.pointerId) return;`
Optionally also ignore `pointerDown` while already dragging to prevent state overwrite in multi-touch cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread apps/desktop/src/renderer/components/ui/ColumnResizer.tsx Outdated
Comment thread apps/desktop/src/renderer/components/layout/MainContent.tsx
Comment thread apps/desktop/src/renderer/components/ui/ColumnResizer.tsx
Comment on lines +108 to +122
const merged = { ...current, ...(persisted as Partial<UIState>) };
return {
...merged,
sidebarPanelWidth: clamp(
merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT,
SIDEBAR_PANEL_WIDTH_MIN,
SIDEBAR_PANEL_WIDTH_MAX,
),
promptListPaneWidth: clamp(
merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT,
PROMPT_LIST_PANE_WIDTH_MIN,
PROMPT_LIST_PANE_WIDTH_MAX,
),
} as UIState;
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Unjustified as assertions 📘 Rule violation ⚙ Maintainability

The PR introduces as type assertions without an explanatory justification comment, which violates
the casting restriction rule. This can hide real typing issues and reduce type safety guarantees.
Agent Prompt
## Issue description
New `as` assertions were introduced without justification comments.

## Issue Context
Compliance requires either removing unnecessary assertions or documenting why the cast is required (interop/unsafe boundary).

## Fix Focus Areas
- apps/desktop/src/renderer/stores/ui.store.ts[108-122]
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[107-109]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1420 to +1427
<ColumnResizer
currentWidth={sidebarPanelWidth}
min={SIDEBAR_PANEL_WIDTH_MIN}
max={SIDEBAR_PANEL_WIDTH_MAX}
defaultWidth={SIDEBAR_PANEL_WIDTH_DEFAULT}
onResize={setSidebarPanelWidth}
ariaLabel={t('sidebar.resizeAria', 'Resize folder sidebar')}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Hardcoded aria label fallback 📘 Rule violation ⚙ Maintainability

A user-facing ARIA label fallback string is hardcoded in renderer code instead of relying solely on
i18next keys. This violates the requirement that all user-visible text be produced via t() with
translation resources (no hardcoded strings).
Agent Prompt
## Issue description
The ARIA label uses `t('...', '...')` with a hardcoded fallback string, which violates the no-hardcoded-strings i18n requirement.

## Issue Context
ARIA labels are user-facing (assistive tech) and must be localized.

## Fix Focus Areas
- apps/desktop/src/renderer/components/layout/Sidebar.tsx[1420-1427]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

@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: 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/renderer/stores/ui.store.ts`:
- Around line 107-122: The two type assertions inside the merge function (the
cast of persisted to Partial<UIState> and the final cast to UIState) need brief
explanatory comments: add an inline comment next to the persisted cast
explaining that the persist middleware provides an unknown type and we must
assert to Partial<UIState> for safe spread during merge, and add an inline
comment next to the final "as UIState" explaining that TypeScript cannot
statically infer the fully populated UIState after applying defaults/clamps so
an assertion is required to satisfy the persist API; keep the comments concise
and reference the interplay with Zustand persist and UIState so reviewers
understand why the asserts are necessary.
🪄 Autofix (Beta)

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: CHILL

Plan: Pro

Run ID: 32504a1d-88f5-47a1-9fb0-1e87c09d8b38

📥 Commits

Reviewing files that changed from the base of the PR and between ebc43d9 and c0116da.

📒 Files selected for processing (15)
  • apps/desktop/src/renderer/components/layout/MainContent.tsx
  • apps/desktop/src/renderer/components/layout/Sidebar.tsx
  • apps/desktop/src/renderer/components/prompt/PromptGalleryView.tsx
  • apps/desktop/src/renderer/components/prompt/PromptListView.tsx
  • apps/desktop/src/renderer/components/ui/ColumnResizer.tsx
  • apps/desktop/src/renderer/i18n/locales/de.json
  • apps/desktop/src/renderer/i18n/locales/en.json
  • apps/desktop/src/renderer/i18n/locales/es.json
  • apps/desktop/src/renderer/i18n/locales/fr.json
  • apps/desktop/src/renderer/i18n/locales/ja.json
  • apps/desktop/src/renderer/i18n/locales/zh-TW.json
  • apps/desktop/src/renderer/i18n/locales/zh.json
  • apps/desktop/src/renderer/stores/ui.store.ts
  • apps/desktop/tests/unit/components/column-resizer.test.tsx
  • apps/desktop/tests/unit/stores/ui-columns.test.ts

Comment on lines +107 to +122
merge: (persisted, current) => {
const merged = { ...current, ...(persisted as Partial<UIState>) };
return {
...merged,
sidebarPanelWidth: clamp(
merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT,
SIDEBAR_PANEL_WIDTH_MIN,
SIDEBAR_PANEL_WIDTH_MAX,
),
promptListPaneWidth: clamp(
merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT,
PROMPT_LIST_PANE_WIDTH_MIN,
PROMPT_LIST_PANE_WIDTH_MAX,
),
} as UIState;
},
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 | 🟡 Minor | ⚡ Quick win

为类型断言添加说明注释

merge 函数中的两处类型断言(第 108 行和第 121 行)缺少解释性注释。根据编码规范,类型断言仅在与外部库互操作时必要,且必须附带注释说明原因。

这两处断言对于 Zustand 的 persist 中间件类型是必需的:

  • 第 108 行:persisted 的类型是 unknown,需要断言为 Partial<UIState> 才能安全展开
  • 第 121 行:TypeScript 无法推断展开操作后的对象完全满足 UIState 接口
建议添加的注释
 merge: (persisted, current) => {
+  // Zustand persist provides persisted state as `unknown`; cast to Partial
+  // to safely merge with current state.
   const merged = { ...current, ...(persisted as Partial<UIState>) };
   return {
     ...merged,
     sidebarPanelWidth: clamp(
       merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT,
       SIDEBAR_PANEL_WIDTH_MIN,
       SIDEBAR_PANEL_WIDTH_MAX,
     ),
     promptListPaneWidth: clamp(
       merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT,
       PROMPT_LIST_PANE_WIDTH_MIN,
       PROMPT_LIST_PANE_WIDTH_MAX,
     ),
+    // TypeScript cannot verify that the spread + explicit fields cover all
+    // UIState properties; assertion is safe because we spread current (the
+    // full default state) first.
   } as UIState;
 },

根据编码规范:"Type assertions with as are prohibited unless truly necessary for interop, and must include a comment explaining why"

📝 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
merge: (persisted, current) => {
const merged = { ...current, ...(persisted as Partial<UIState>) };
return {
...merged,
sidebarPanelWidth: clamp(
merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT,
SIDEBAR_PANEL_WIDTH_MIN,
SIDEBAR_PANEL_WIDTH_MAX,
),
promptListPaneWidth: clamp(
merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT,
PROMPT_LIST_PANE_WIDTH_MIN,
PROMPT_LIST_PANE_WIDTH_MAX,
),
} as UIState;
},
merge: (persisted, current) => {
// Zustand persist provides persisted state as `unknown`; cast to Partial
// to safely merge with current state.
const merged = { ...current, ...(persisted as Partial<UIState>) };
return {
...merged,
sidebarPanelWidth: clamp(
merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT,
SIDEBAR_PANEL_WIDTH_MIN,
SIDEBAR_PANEL_WIDTH_MAX,
),
promptListPaneWidth: clamp(
merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT,
PROMPT_LIST_PANE_WIDTH_MIN,
PROMPT_LIST_PANE_WIDTH_MAX,
),
// TypeScript cannot verify that the spread + explicit fields cover all
// UIState properties; assertion is safe because we spread current (the
// full default state) first.
} as UIState;
},
🤖 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/renderer/stores/ui.store.ts` around lines 107 - 122, The two
type assertions inside the merge function (the cast of persisted to
Partial<UIState> and the final cast to UIState) need brief explanatory comments:
add an inline comment next to the persisted cast explaining that the persist
middleware provides an unknown type and we must assert to Partial<UIState> for
safe spread during merge, and add an inline comment next to the final "as
UIState" explaining that TypeScript cannot statically infer the fully populated
UIState after applying defaults/clamps so an assertion is required to satisfy
the persist API; keep the comments concise and reference the interplay with
Zustand persist and UIState so reviewers understand why the asserts are
necessary.

- Drop Chinese comment lines introduced by this PR (AGENTS.md rule)
- Replace direct inline width styles with Tailwind arbitrary utilities
  that read from a single CSS custom property. Runtime-dynamic widths
  now land in the DOM as `w-[var(--sidebar-panel-width)]` /
  `w-[var(--prompt-list-pane-width)]`, with the pixel value supplied
  via a CSS variable instead of a direct `style={{ width: n }}`. This
  is Tailwind's recommended pattern for dynamic sizing and keeps the
  visible utilities in the class string.
- ColumnResizer's hit area is now expressed purely in Tailwind
  (`w-2 shrink-0 touch-none`) — no more static-value inline style.
- Replace the two empty catch blocks in ColumnResizer with
  `console.warn` / `console.debug` so pointer-capture unavailability
  is no longer invisible to support traces.
@legeling legeling merged commit 443045d into legeling:main May 10, 2026
1 of 2 checks passed
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