diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a9c01..76fd810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ ## [Unreleased] +### Added + +- PR 详情「评论」标签页演进为「活动」时间线(GitHub / Bitbucket):把评论、提交更新、reviewer 评审决断(approve / needs-work / unapprove / dismiss)按时间倒序归并为一条活动时间线,保留原有评论内容、排序与编辑 / 回复 / 删除 / 内联代码能力。新增统一的 `listPullRequestActivity` 平台契约——GitHub 取自 `/pulls/{n}/reviews`、Bitbucket 取自 `/activities`(带时间戳的决断事件)。提交另保留独立「提交」标签页。 + - 视觉:各条目统一为「图标节点 + 头像 + 加粗作者名 + 动词 + 时间」,一条竖向虚线轨贯穿图标列连接相邻条目;评论标题统一为「xxx 评论」并前置评论图标,正文整体缩进成挂在轨上的卡片;作者头像 / 文本与评论主体人一致,不做差异化。评审决断动词统一为「批准 / 要求修改」并用带色 chip(绿 / 琥珀 / 中性)突出。时间标签 hover 显示精确到秒的实际时间点。 + - 新建评论:标签栏右侧「评论」按钮可直接发一条不锚到文件的 summary 评论,编辑框作为时间线首个节点(头像在轨上、编辑框缩进)展开,发布后新评论即时出现在顶部(新增 `publishSummaryComment` 平台契约 + `comments:create` 通道)。 + - GitLab 走差异化设计:无统一活动事件源(CE 无审批、审批系统 note 解析脆弱),标签页保持纯「评论」视图(`capabilities.activityTimeline=false`),不混入提交 / 决断。 + ### Changed - **前端代码结构重构(可维护性)**:纯结构调整,对外接口与界面 / 交互行为均不变。重点: @@ -16,6 +23,7 @@ ### Fixed +- PR 头部与详情页的评审状态 chip(pending / approved / needs_work、reviewer 的 approved / needs work / pending)此前为写死英文,现按界面语言出国际化文案(新增 `prStatus` 文案集,四语言)。 - 切换不同 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 显示,命中本地缓存的快切换直接换新、零闪。 diff --git a/apps/desktop/src/main/controllers/pr.ts b/apps/desktop/src/main/controllers/pr.ts index 66d946d..dd47fb8 100644 --- a/apps/desktop/src/main/controllers/pr.ts +++ b/apps/desktop/src/main/controllers/pr.ts @@ -37,6 +37,22 @@ export const replyComment: IpcController<'comments:reply'> = async (_event, req) return reply; }; +/** + * 在 PR 上新建一条 summary(顶层、不锚文件)评论,成功后清评论缓存 + 广播 comments:changed 让 UI 重拉。 + */ +export const createComment: IpcController<'comments:create'> = async (_event, req) => { + const ctx = getContext(); + const pr = await ctx.pr.findPrOrThrow(req.localId); + const adapter = ctx.pr.adapterForOrThrow(pr); + const created = await adapter.publishSummaryComment( + { projectKey: pr.repo.projectKey, repoSlug: pr.repo.repoSlug }, + pr.remoteId, + req.body, + ); + await ctx.pr.invalidateCommentsCache(pr.localId); + return created; +}; + /** * 删除自己作者的远端评论(带 version 乐观锁)。失败原文抛给 renderer;成功后清缓存 + 广播。 */ @@ -242,6 +258,20 @@ export const listCommits: IpcController<'diff:listCommits'> = async (_event, req ); }; +/** + * 拉评审决断活动事件(approve / needs-work / unapprove / dismiss)。不缓存,量小; + * 进活动时间线时与评论 / 提交归并。平台取不到历史决断时 adapter 返回 []。 + */ +export const listActivity: IpcController<'diff:listActivity'> = async (_event, req) => { + const ctx = getContext(); + const pr = await ctx.pr.findPrOrThrow(req.localId); + const adapter = ctx.pr.adapterForOrThrow(pr); + return adapter.listPullRequestActivity( + { projectKey: pr.repo.projectKey, repoSlug: pr.repo.repoSlug }, + pr.remoteId, + ); +}; + /** * 本地 git 算 PR 引入提交数(base=targetRef.sha 排除合入的目标提交);镜像未齐返回 null。 */ diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts index 7651c76..b7e4b51 100644 --- a/apps/desktop/src/main/ipc.ts +++ b/apps/desktop/src/main/ipc.ts @@ -58,6 +58,7 @@ export function registerIpcHandlers(deps: RegisterDeps): { * 评论 / 列表 / 状态 / 合并 / 镜像 / diff / 草稿 / pr-agent run 队列 */ ipcMain.handle('comments:reply', pr.replyComment); // 回复评论 + ipcMain.handle('comments:create', pr.createComment); // 新建 summary 评论 ipcMain.handle('comments:delete', pr.deleteComment); // 删除自己的评论 ipcMain.handle('comments:edit', pr.editComment); // 编辑自己的评论 ipcMain.handle('comments:fetchAttachment', pr.fetchAttachment); // 拉评论内嵌图片(代理带 PAT) @@ -72,6 +73,7 @@ export function registerIpcHandlers(deps: RegisterDeps): { ipcMain.handle('diff:commentCountCached', pr.getCommentCountCached); // 评论数角标(仅缓存) ipcMain.handle('diff:listComments', pr.listComments); // 拉评论(缓存 + in-flight 去重) ipcMain.handle('diff:listCommits', pr.listCommits); // 提交列表 + ipcMain.handle('diff:listActivity', pr.listActivity); // 评审决断活动事件(时间线) ipcMain.handle('diff:commitCount', pr.getCommitCount); // 提交数角标(本地 git) ipcMain.handle('diff:getBlame', pr.getBlame); // blame + PR 引入行 ipcMain.handle('repo:getTotalSize', pr.getTotalSize); // 本地镜像总占用(设置页) diff --git a/apps/desktop/src/renderer/src/components/common/icons.tsx b/apps/desktop/src/renderer/src/components/common/icons.tsx index d3c36e7..44e2acd 100644 --- a/apps/desktop/src/renderer/src/components/common/icons.tsx +++ b/apps/desktop/src/renderer/src/components/common/icons.tsx @@ -408,6 +408,27 @@ export function NeedsWorkIcon({ size = 14 }: IconProps) { ); } +/** git commit 字形:横线上一个实心节点(活动时间线提交事件用)。 */ +export function CommitIcon({ size = 14 }: IconProps) { + return ( + + ); +} + /** 机器人头像:AutoPilot 启用态。天线 + 头框 + 双眼 + 两侧耳。 */ export function RobotIcon({ size = 14 }: IconProps) { return ( diff --git a/apps/desktop/src/renderer/src/components/features/pr/PrHeader.tsx b/apps/desktop/src/renderer/src/components/features/pr/PrHeader.tsx index 19e16e6..2689eab 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/PrHeader.tsx +++ b/apps/desktop/src/renderer/src/components/features/pr/PrHeader.tsx @@ -60,7 +60,9 @@ export function PrHeader({ · {pr.sourceRef.displayId} → {pr.targetRef.displayId} · - {pr.localStatus} + + {t(`prStatus.${pr.localStatus === 'needs_work' ? 'needsWork' : pr.localStatus}`)} +
('diff'); + // 活动标签页「新建评论」编辑框开关(由标签栏「评论」按钮触发,编辑框出现在时间线顶部) + const [composingComment, setComposingComment] = useState(false); // 收到跳转请求 → 强制切到 Diff tab,DiffView 自己负责消费 anchor useEffect(() => { if (pendingDiffNav) setTab('diff'); @@ -146,6 +148,8 @@ export function PrPanel({ commitCount={commitCount} totalDraftCount={totalDraftCount} publishableCount={publishableCount} + activityTimeline={capabilities?.activityTimeline ?? false} + onNewComment={() => setComposingComment(true)} showWhitespace={showWhitespace} onToggleWhitespace={() => setShowWhitespace((b) => !b)} showBlame={showBlame} @@ -169,11 +173,14 @@ export function PrPanel({ /> - - + setCommentCount(n)} capabilities={capabilities} + composing={composingComment} + onComposeClose={() => setComposingComment(false)} + currentUserName={currentUserName} /> 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 3cbcb30..f169bf4 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 @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import type { ReviewerStatus, StoredPullRequest } from '@meebox/shared'; @@ -11,9 +12,11 @@ interface PrInfoViewProps { } function ReviewerStatusTag({ status }: { status: ReviewerStatus }) { - if (status === 'approved') return ✓ approved; - if (status === 'needsWork') return ✗ needs work; - return pending; + const { t } = useTranslation(); + if (status === 'approved') return ✓ {t('prStatus.approved')}; + if (status === 'needsWork') + return ✗ {t('prStatus.needsWork')}; + return {t('prStatus.pending')}; } export function PrInfoView({ pr }: PrInfoViewProps) { 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 1287777..63d31b9 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 @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; -import { PersonIcon, WhitespaceIcon } from '../../../common/icons'; +import { ChatIcon, PersonIcon, WhitespaceIcon } from '../../../common/icons'; -export type PrTab = 'diff' | 'comments' | 'drafts' | 'commits' | 'info'; +export type PrTab = 'diff' | 'activity' | 'drafts' | 'commits' | 'info'; /** * PR 详情 tab 栏:diff / 评论 / 草稿 / 提交 / 信息(带计数徽标),diff tab 时右侧附带 @@ -14,6 +14,8 @@ export function PrTabs({ commitCount, totalDraftCount, publishableCount, + activityTimeline, + onNewComment, showWhitespace, onToggleWhitespace, showBlame, @@ -27,6 +29,10 @@ export function PrTabs({ commitCount: number | null; totalDraftCount: number; publishableCount: number; + /** 该平台是否提供活动时间线(见 capabilities.activityTimeline);否则该 tab 标题退化为「评论」 */ + activityTimeline: boolean; + /** 活动标签页右侧「评论」按钮:新建一条不锚到文件的评论 */ + onNewComment: () => void; showWhitespace: boolean; onToggleWhitespace: () => void; showBlame: boolean; @@ -46,15 +52,16 @@ export function PrTabs({ > {t('mainPane.tabDiff')} - {/* comments 在 commits 前:评审决断时评论的权重大于 commit 时间线 */} + {/* 活动时间线(评论 + 提交 + 评审决断)在 commits 前:评审决断时讨论权重大于纯 commit 列表。 + 角标仍取评论数——讨论量最具行动指引,提交另有独立 tab 计数。 */} + {tab === 'activity' && ( +
+ +
+ )} {tab === 'diff' && (