diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c6c17..12a9c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ ### Fixed +- 切换不同 PR 时 diff 文件树「左栏空白 → 文件树整体弹出」的抖动:DiffView 改为 stale-while-loading——引入 `loadedPrId` 标记当前已渲染内容所属 PR,切 PR 期间保留旧树 / 旧内容渲染、上盖加载遮罩(延迟 150ms,命中缓存的快切换直接换新),并门控 content / comments / blame 拉取(避免「新 localId + 旧选中文件」错拉),新文件列表 ready 后整体替换。 +- diff 文件树首次加载时文件名被图标渲染推移的抖动:图标改用固定 16px 占位槽包裹,iconify 的 svg 晚一帧进 DOM 也不塌缩,文件名位置稳定。 +- 切换不同 PR 时评论页先闪「加载评论中」再渲新内容的空窗:改为 stale-while-loading——切 PR 期间保留旧评论渲染、上盖加载遮罩,新数据 ready 后整体替换;遮罩延迟 150ms 显示,命中本地缓存的快切换直接换新、零闪。 +- PR 主面板 tab 栏角标(评论 / 提交计数)异步加载导致的抖动:计数加载中渲染等宽占位 chip 预留宽度,消除计数到达时的横向弹簧拉伸;`.pr-tab` 改 flex 布局 + 固定行高,角标占位 / 出现 / 消失不再改变 tab 高度,消除 tab 栏 1~2px 竖向跳动。 +- PR 主面板各 tab(diff / 评论 / 草稿 / 提交 / 信息)切换抖动:此前 tab 内容按条件渲染,每次切换旧面板卸载、新面板重挂 → 重新拉数据、闪「加载中」、内嵌 Monaco 重建。改为 keep-alive——tab 首访才挂载(保留懒加载)、之后保活仅 CSS 显隐不卸载,切走再切回瞬时、无重拉、滚动位置与展开态保留;配合 Monaco `automaticLayout` 处理显隐后的重排。 +- 刷新(后台轮询 / 窗口聚焦)时编辑器渲染抖动:评论页内嵌代码片段(Monaco)与 diff 编辑器此前每次刷新都重渲染 / 重建。根因有二——其一,i18n 语言切换 effect 依赖整个 boot 对象,poll 刷新 setBoot 后对同一语言反复 `changeLanguage`,触发 `languageChanged` 致所有 `useTranslation` 的 `t` 换新引用,凡 effect 依赖 `t` 的组件(如内嵌代码片段抓取逻辑)都被无谓重跑、连带 Monaco 卸载重建;其二,DiffEditor 的 `options` 为渲染期新建对象,被 `@monaco-editor/react` 按引用判变而反复 `updateOptions`。现语言 effect 仅在语言真正变化时切换、DiffEditor options 稳定化,刷新不再抖动。 +- PR 详情页与评论页排版:正文限宽 960px 并居中,滚动条回到外层容器右缘(此前 max-width 加在滚动容器上,滚动条停在中部);详情页 reviewers 列表按字典序固定排序,刷新不再随平台返回顺序抖动。 +- 拉取变更文件列表偶发失败(`ENOENT … diff-base.json`):状态存储对同一 key 的并发写共用同一临时文件,先完成者 rename 后,后完成者 rename 即 ENOENT。临时文件名追加进程内自增序号去重,并发写各用独立临时文件。 - Agent 评审 / 规划步骤行的固定文案(如「判断是否存在需追问的严重问题」「严重,追问 N 个」)此前在 `@meebox/agent` 层写死中文、被渲染层逐字显示,日 / 英 / 德界面下漏出中文;现按会话语言落地(zh-CN / en-US / ja-JP / de-DE,缺省回落英文),与评审总结骨架同策略。 - 设置页手动「检查更新」查到的新版此前不同步到状态栏、也不缓存:手动检查只把结果回给设置页本地,与定时检查各自为政、无共享。现 main 侧统一为单一真相源——手动 / 定时检查都缓存结果并在有新版时广播 `app:updateAvailable`,状态栏即时出现升级 chip;新增只读 `app:getUpdateStatus`,窗口 / 状态栏挂载时水合已知结果,不因重挂载而丢失。 diff --git a/apps/desktop/src/renderer/src/components/features/pr/PrPanel.tsx b/apps/desktop/src/renderer/src/components/features/pr/PrPanel.tsx index beb627e..f0807e6 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/PrPanel.tsx +++ b/apps/desktop/src/renderer/src/components/features/pr/PrPanel.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; +import { lazy, Suspense, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import type { LocalPrStatus, PlatformCapabilities, StoredPullRequest } from '@meebox/shared'; import { invoke } from '../../../api'; @@ -154,7 +154,9 @@ export function PrPanel({ onSetRenderSideBySide={setRenderSideBySide} />
- {tab === 'diff' && ( + {/* keep-alive:各 tab 首访才挂载、之后保活仅 CSS 显隐(见 KeepAliveTab)。 + 切走再切回瞬时、无重拉、内嵌 Monaco / 滚动位置 / 展开态全部保留,消除切换抖动。 */} + }> - )} - {tab === 'comments' && ( + + setCommentCount(n)} capabilities={capabilities} /> - )} - {tab === 'drafts' && ( + + - )} - {tab === 'commits' && } - {tab === 'info' && } + + + + + + +
{publishModalOpen && ( ); } + +/** + * tab 内容保活容器:首次 active 才挂载(保留 DiffView 等的懒加载优势),此后**不卸载**, + * 仅靠 CSS `display` 显隐。切走再切回瞬时、无重拉、内嵌 Monaco / 滚动位置 / 展开态全保留 → + * 消除切换抖动。隐藏期 Monaco 容器尺寸为 0,再显示需重排——由编辑器侧 `automaticLayout` + * 自动处理(见 DiffView / InlineCodeContext)。 + */ +function KeepAliveTab({ active, children }: { active: boolean; children: ReactNode }) { + // 「一旦 active 过就保活」latch:ref 在 render 期写入是幂等闩锁,与本仓 stablePr 同模式。 + const mounted = useRef(false); + if (active) mounted.current = true; + if (!mounted.current) return null; + return ( +
+ {children} +
+ ); +} diff --git a/apps/desktop/src/renderer/src/components/features/pr/tabs/PrInfoView.tsx b/apps/desktop/src/renderer/src/components/features/pr/tabs/PrInfoView.tsx index f847987..3cbcb30 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/tabs/PrInfoView.tsx +++ b/apps/desktop/src/renderer/src/components/features/pr/tabs/PrInfoView.tsx @@ -23,54 +23,67 @@ export function PrInfoView({ pr }: PrInfoViewProps) { [pr.localId, pr.url], ); + // 各平台 adapter 产出的 reviewers 顺序不稳定(GitHub 按 Map 插入序,随评审推进 + // requested_reviewers 会被移除/补到末尾),每次 poll 列表抖动。展示层按 displayName + // 字典序固定排序,name 兜底兜稳,与平台无关。 + const reviewers = useMemo( + () => + [...pr.reviewers].sort( + (a, b) => a.displayName.localeCompare(b.displayName) || a.name.localeCompare(b.name), + ), + [pr.reviewers], + ); + return (
- {pr.description && ( +
+ {pr.description && ( +
+

描述

+
+ {/* Bitbucket 远端用 \r\n 行尾,remark 解析时 CR 跟 LF 各算一次换行 → 单换行 + 被当成段落分隔,每个 list item 之间多一段空白。归一化成 \n */} + + {pr.description.replace(/\r\n?/g, '\n')} + +
+
+ )} +
-

描述

-
- {/* Bitbucket 远端用 \r\n 行尾,remark 解析时 CR 跟 LF 各算一次换行 → 单换行 - 被当成段落分隔,每个 list item 之间多一段空白。归一化成 \n */} - - {pr.description.replace(/\r\n?/g, '\n')} - -
+

Reviewers ({reviewers.length})

+ {reviewers.length === 0 ? ( +

+ ) : ( +
    + {reviewers.map((r) => ( +
  • + {r.displayName} +
  • + ))} +
+ )}
- )} -
-

Reviewers ({pr.reviewers.length})

- {pr.reviewers.length === 0 ? ( -

- ) : ( -
    - {pr.reviewers.map((r) => ( -
  • - {r.displayName} -
  • - ))} -
- )} -
- -
-

时间线

-
-
远端创建
-
{new Date(pr.createdAt).toLocaleString()}
-
远端更新
-
{new Date(pr.updatedAt).toLocaleString()}
-
本地首次发现
-
{new Date(pr.discoveredAt).toLocaleString()}
-
最近一次 poll 看到
-
{new Date(pr.lastSeenAt).toLocaleString()}
-
-
+
+

时间线

+
+
远端创建
+
{new Date(pr.createdAt).toLocaleString()}
+
远端更新
+
{new Date(pr.updatedAt).toLocaleString()}
+
本地首次发现
+
{new Date(pr.discoveredAt).toLocaleString()}
+
最近一次 poll 看到
+
{new Date(pr.lastSeenAt).toLocaleString()}
+
+
+
); } diff --git a/apps/desktop/src/renderer/src/components/features/pr/tabs/PrTabs.tsx b/apps/desktop/src/renderer/src/components/features/pr/tabs/PrTabs.tsx index 2a2fe30..1287777 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/tabs/PrTabs.tsx +++ b/apps/desktop/src/renderer/src/components/features/pr/tabs/PrTabs.tsx @@ -55,14 +55,10 @@ export function PrTabs({ aria-selected={tab === 'comments'} > {t('mainPane.tabComments')} - {commentCount !== null && commentCount > 0 && ( - - {commentCount} - - )} + t('mainPane.commentCountAria', { count: n })} + /> {/* 草稿 tab:显示条件用总数 — 全发完仍能进 tab 看 posted/rejected 历史; 从未创建草稿的 PR 才完全隐藏 tab,避免冗余入口 */} @@ -94,14 +90,10 @@ export function PrTabs({ aria-selected={tab === 'commits'} > {t('mainPane.tabCommits')} - {commitCount !== null && commitCount > 0 && ( - - {commitCount} - - )} + t('mainPane.commitCountAria', { count: n })} + />