Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
All notable changes to this project are recorded here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the versioning follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### ✨ Added

- Binary files in the diff (images, office documents, PDFs, …) now show their Git LFS status — a "Git LFS · <size>" tag for LFS-managed files, or a "⚠ Not LFS" tag for files stored inline in git.
- Comments can now be anchored to a whole file (not only a single line) where the platform supports it (Bitbucket / GitHub): a "comment on file" entry in the diff, and existing file-level comments now display correctly instead of being shown as generic PR comments.

## [0.11.0] - 2026-07-07

> Highlights of this release:
Expand Down Expand Up @@ -465,6 +472,7 @@ and the versioning follows [Semantic Versioning](https://semver.org/).

License: [Apache-2.0](LICENSE). The package bundles third-party components (pr-agent, Electron, etc.), each distributed under its own license, see [NOTICE](NOTICE).

[Unreleased]: https://github.com/huhamhire/code-meeseeks/compare/v0.11.0...HEAD
[0.11.0]: https://github.com/huhamhire/code-meeseeks/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/huhamhire/code-meeseeks/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/huhamhire/code-meeseeks/compare/v0.8.0...v0.9.0
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
本项目所有重要变更记录于此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## [Unreleased]

### ✨ 新增

- diff 中的二进制文件(图片、Office 文档、PDF 等)现会显示其 Git LFS 状态——受 LFS 管理的文件显示「Git LFS · <大小>」标记,直接内联存入 git 的文件显示「⚠ 非 LFS」标记。
- 现可对整个文件(而不仅是某一行)添加评论(平台支持时,Bitbucket / GitHub):diff 中新增「对文件评论」入口,且已存在的文件级评论现能正确归属显示,不再被当作 PR 通用评论。

## [0.11.0] - 2026-07-07

> 本次发布要点:
Expand Down Expand Up @@ -465,6 +472,7 @@

许可证:[Apache-2.0](LICENSE)。打包内含第三方组件(pr-agent、Electron 等),各按其许可证分发,见 [NOTICE](NOTICE)。

[Unreleased]: https://github.com/huhamhire/code-meeseeks/compare/v0.11.0...HEAD
[0.11.0]: https://github.com/huhamhire/code-meeseeks/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/huhamhire/code-meeseeks/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/huhamhire/code-meeseeks/compare/v0.8.0...v0.9.0
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/main/controllers/pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,25 @@ export const createComment: IpcController<'comments:create'> = async (_event, re
return created;
};

/**
* Post a file-level comment (anchored to a whole file, no line) via the inline-comment publish path with a line-less
* anchor. Only reachable where the fileLevelComments capability is true (the UI gates the entry). On success clears the
* comments cache + broadcasts comments:changed so the UI refetches.
*/
export const createFileComment: IpcController<'comments:createFile'> = async (_event, req) => {
const ctx = getContext();
const pr = await ctx.pr.findPrOrThrow(req.localId);
const adapter = ctx.pr.adapterForOrThrow(pr);
const created = await adapter.comments.publishInlineComment(
{ projectKey: pr.repo.projectKey, repoSlug: pr.repo.repoSlug },
pr.remoteId,
{ path: req.path, side: req.side ?? 'new' },
req.body,
);
await ctx.pr.invalidateCommentsCache(pr.localId);
return created;
};

/** Minimum query length before hitting the remote user-search endpoint (avoids a request per keystroke on 1 char). */
const MENTION_SEARCH_MIN_QUERY = 2;

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function registerIpcHandlers(deps: RegisterDeps): {
*/
ipcMain.handle('comments:reply', pr.replyComment); // Reply to a comment
ipcMain.handle('comments:create', pr.createComment); // Create a summary comment
ipcMain.handle('comments:createFile', pr.createFileComment); // Create a file-level comment (whole file, no line)
ipcMain.handle('comments:delete', pr.deleteComment); // Delete your own comment
ipcMain.handle('comments:edit', pr.editComment); // Edit your own comment
ipcMain.handle('comments:toggleReaction', pr.toggleReaction); // Toggle a comment emoji reaction
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop/src/main/services/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ function activateOnClick(e: PollNotificationEvent): () => void {
broadcast('notification:activate', {
localId: e.localId,
kind: e.kind,
anchor: e.comment?.anchor
? { path: e.comment.anchor.path, line: e.comment.anchor.line }
: null,
anchor:
// File-level comments (no line) aren't line-navigable → no anchor (clicking just opens the PR).
e.comment?.anchor && e.comment.anchor.line != null
? { path: e.comment.anchor.path, line: e.comment.anchor.line }
: null,
});
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,13 @@ export function PrPanel({
onComposeClose={() => setComposingComment(false)}
currentUserName={currentUserName}
onViewCommit={viewCommit}
onJumpToAnchor={(a: PrCommentAnchor) =>
onJumpToAnchor={(a: PrCommentAnchor) => {
// File-level anchors (no line) aren't line-navigable; CommentItem doesn't make them clickable, guard anyway.
if (a.line == null) return;
onRequestDiffNav?.({
anchor: { path: a.path, startLine: a.line, endLine: a.line },
})
}
});
}}
/>
</KeepAliveTab>
<KeepAliveTab active={tab === 'drafts'}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@ interface CommentComposerProps {
attachmentsEnabled?: boolean;
/** Whether the platform supports remote user search (capabilities.userSearch); enables the mention editor's remote fallback when true. */
userSearchEnabled?: boolean;
/**
* Override the post action. Default posts a PR summary comment (`comments:create`); pass this to post elsewhere with
* the same composer (e.g. a file-level comment via `comments:createFile`). Receives the body, resolves on success.
*/
onSubmit?: (body: string) => Promise<unknown>;
onCancel: () => void;
/** Called after posting succeeds (collapses the composer; the timeline auto-refreshes via the comments:changed event, the new comment appears at the top) */
onPosted: () => void;
}

/**
* Composer for a new summary (not anchored to a file) comment: textarea + send/cancel. Appears at the top of the activity timeline.
* Cmd/Ctrl+Enter sends, Esc cancels; send is disabled on an empty body. Layout reuses the reply composer's styles.
* Composer for a new comment: textarea + send/cancel. Posts a PR summary comment by default (top of the activity
* timeline), or whatever `onSubmit` overrides it to (e.g. a file-level comment). Cmd/Ctrl+Enter sends, Esc cancels;
* send is disabled on an empty body. Layout reuses the reply composer's styles.
*/
export function CommentComposer({
prLocalId,
mentionCandidates = [],
platform,
attachmentsEnabled = false,
userSearchEnabled = false,
onSubmit,
onCancel,
onPosted,
}: CommentComposerProps) {
Expand All @@ -46,7 +53,8 @@ export function CommentComposer({
setPosting(true);
setError(null);
try {
await invoke('comments:create', { localId: prLocalId, body });
if (onSubmit) await onSubmit(body);
else await invoke('comments:create', { localId: prLocalId, body });
onPosted();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,32 +141,43 @@ export function CommentItem({
// inline comment anchor chip: path:line + side (old=base / new=head), letting the user locate the code position from the comment.
// When onJumpToAnchor is provided (activity view) the chip becomes clickable → jump to the corresponding file/line in the Diff.
const anchor = comment.anchor;
// File-level comments (no line) show just the path; line comments show `path:line`.
const anchorLabel = anchor ? (
<>
<code>{anchor.path}</code>
{anchor.line != null ? `:${String(anchor.line)}` : ''}
</>
) : null;
const anchorHint = anchor
? anchor.line == null
? t('commentsPanel.anchorFileTitle')
: t('commentsPanel.anchorTitle', {
side: anchor.side === 'old' ? 'base' : 'head',
lineType: anchor.lineType,
})
: '';
const anchorChip = anchor ? (
onJumpToAnchor ? (
// File-level anchors (no line) have no line to jump to → render as a non-clickable chip even in the activity view.
onJumpToAnchor && anchor.line != null ? (
<button
type="button"
className={`pr-comment-anchor pr-comment-anchor-${anchor.side} pr-comment-anchor-link`}
onClick={() => onJumpToAnchor(anchor)}
title={t('commentsPanel.anchorJumpTitle')}
>
<code>{anchor.path}</code>:{anchor.line}
{anchorLabel}
</button>
) : (
<span
className={`pr-comment-anchor pr-comment-anchor-${anchor.side}`}
title={t('commentsPanel.anchorTitle', {
side: anchor.side === 'old' ? 'base' : 'head',
lineType: anchor.lineType,
})}
>
<code>{anchor.path}</code>:{anchor.line}
<span className={`pr-comment-anchor pr-comment-anchor-${anchor.side}`} title={anchorHint}>
{anchorLabel}
</span>
)
) : null;

// inline comment: embed a code context (Monaco read-only) above the body. replies (depth > 0) do not repeat it.
// File-level comments (no line) have no single line to show → skip the code context.
const inlineCode =
comment.anchor && depth === 0 ? (
comment.anchor && comment.anchor.line != null && depth === 0 ? (
<Suspense
fallback={<div className="pane-loading muted">{t('commentsPanel.loadingCodeContext')}</div>}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ function InlineCodeContextImpl({
let cancelled = false;
setSnippet(null);
setError(null);
// File-level anchors (no line) have no line to contextualize; the caller doesn't mount this for them, but guard anyway.
// Capture as a const so the narrowing carries into the async closure below.
const line = anchor.line;
if (line == null) return;
void (async () => {
try {
const c = await invoke('diff:getFileContent', {
Expand All @@ -72,10 +76,10 @@ function InlineCodeContextImpl({
return;
}
const allLines = c.content.split('\n');
const startLine = Math.max(1, anchor.line - contextLines);
const endLine = Math.min(allLines.length, anchor.line + contextLines);
const startLine = Math.max(1, line - contextLines);
const endLine = Math.min(allLines.length, line + contextLines);
const text = allLines.slice(startLine - 1, endLine).join('\n');
setSnippet({ text, startLine, anchorInSnippet: anchor.line - startLine + 1 });
setSnippet({ text, startLine, anchorInSnippet: line - startLine + 1 });
} catch (e) {
if (!cancelled) {
setError(e instanceof Error ? e.message : String(e));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useMonacoEditorTheme } from '../../../../../hooks/useTheme';
import { useEditorAppearance } from '../../../../../stores/editor-appearance-store';
import { resolveEditorFontFamily } from '../../../../../theme';
import { languageFor } from '../../../../../utils/language';
import { formatBytes } from '../../../settings/utils';
import { PaneLoading } from '../../../../common';
import { Spinner } from './DiffStatus';
import type { LoadedContent } from './diff-types';
Expand Down Expand Up @@ -119,7 +120,27 @@ export function DiffPane({
);
}
if (content.base.binary || content.head.binary) {
return <div className="diff-binary">{t('diffView.binaryNotRendered')}</div>;
// Git LFS status: prefer the head side's pointer info (the current version), fall back to base (e.g. a deleted file).
const headLfs = content.head.binary ? content.head.lfs : undefined;
const baseLfs = content.base.binary ? content.base.lfs : undefined;
const lfs = headLfs ?? baseLfs;
return (
<div className="diff-binary">
<span>{t('diffView.binaryNotRendered')}</span>
{lfs ? (
<span className="diff-lfs-tag" title={t('diffView.lfsManagedTitle')}>
Git LFS{lfs.size != null ? ` · ${formatBytes(lfs.size)}` : ''}
</span>
) : (
<span className="diff-nonlfs-tag" title={t('diffView.notLfsTitle')}>
<span className="diff-lfs-icon" aria-hidden="true">
⚠️
</span>
{t('diffView.notLfs')}
</span>
)}
</div>
);
}
return (
<div className="diff-pane-editor">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { DiffSearchPanel } from './DiffSearchPanel';
import { FileTree } from './FileTree';
import { DiffScopeSelect } from './DiffScopeSelect';
import { DiffPane } from './DiffPane';
import { FileCommentStrip } from './FileCommentStrip';
import { BackendErrorBanner, BackendErrorView, SyncProgress } from './DiffStatus';
import { BlameColumn } from './blame/BlameColumn';
import { fileKey, type PendingCommitView } from './diff-types';
Expand Down Expand Up @@ -396,6 +397,21 @@ export function DiffView({
onDismiss={() => setBlameError(null)}
/>
)}
{selected && (
<FileCommentStrip
pr={pr}
path={selected.path}
oldPath={selected.oldPath}
comments={comments}
capabilities={capabilities}
hardBreaks={commentHardBreaks}
reactionsMode={reactionsMode}
mentionCandidates={mentionCandidates}
attachmentsEnabled={attachmentsEnabled}
userSearchEnabled={userSearchEnabled}
readOnly={readOnly}
/>
)}
{selected && (
<div className="diff-pane-wrapper">
{showBlame && blame && blameLayout && diffEditor && (
Expand Down
Loading
Loading