Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2b7b85f
chore(release): start 0.11.1-dev development
huhamhire Jul 7, 2026
0c3e3f3
Merge branch 'master' into dev: align 0.11.0 release back into dev
huhamhire Jul 7, 2026
1eb2dbb
docs(release): make post-release back-merge to dev a standard step
huhamhire Jul 7, 2026
2f4d14d
feat(diff): show Git LFS status on binary-file diffs
huhamhire Jul 8, 2026
9f338c0
feat(review): support file-level comments (whole-file, not line-ancho…
huhamhire Jul 8, 2026
3d4d834
docs(changelog): add Git LFS status + file-level comments under Unrel…
huhamhire Jul 8, 2026
5c6d268
style(diff): match file breadcrumb font size to the file tree
huhamhire Jul 8, 2026
7951463
Merge pull request #211 from huhamhire/feat/diff-lfs-status
huhamhire Jul 8, 2026
5c0f748
fix(diff): align the file breadcrumb bar to the file tree (font size …
huhamhire Jul 8, 2026
4f484b4
feat(agent): support repo_context_files on LocalGitProvider via shim
huhamhire Jul 9, 2026
2d4bb52
docs(changelog): note repo-context guidance-file injection under Unre…
huhamhire Jul 10, 2026
f15edd5
Merge pull request #212 from huhamhire/feat/pragent-repo-context-local
huhamhire Jul 10, 2026
6be1c60
fix(diff): preserve in-progress inline comment across comment refresh
huhamhire Jul 14, 2026
f114074
feat(review): make comment replies deferred drafts, consistent with n…
huhamhire Jul 14, 2026
21a5b70
merge: bring in inline-comment-zone teardown fix (preserve in-progres…
huhamhire Jul 14, 2026
c3a1992
Merge pull request #213 from huhamhire/feat/inline-reply-drafts
huhamhire Jul 14, 2026
f2cea75
chore(release): 0.11.1
huhamhire Jul 14, 2026
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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ After changing code, run these four steps locally before wrapping up (this is ex

## Release flow

Release: merge `dev` into `master` → tag `v*` on `master`, which triggers [release.yml](.github/workflows/release.yml) (produces Windows / macOS installers + CLI binaries + a GitHub Release).
Release: merge `dev` into `master` → tag `v*` on `master`, which triggers [release.yml](.github/workflows/release.yml) (produces Windows / macOS installers + CLI binaries + a GitHub Release) → **back-merge `master` into `dev`** + bump `dev` to the next `-dev`.

⚠️ **Before tagging, complete the three prerequisites (version / CHANGELOG / proofread) in the same batch of changes, flowing through `dev` → `master` with the release** — miss any step and CI won't error (only `::warning::`) but will produce a wrong Release. **The full prerequisite checklist, the `-dev` version-number rule, and CHANGELOG writing style are in [Packaging & release](docs/development/packaging-release.md)**. The tag name must equal the package.json version (`v<version>`); a prerelease tag with a `-` in the name is automatically marked prerelease and does not claim Latest.

**Post-release back-merge (mandatory)**: after the `dev → master` release PR merges, `master` gains a merge commit that `dev` does not contain, so the branches immediately diverge — leave it and every subsequent `dev → master` PR shows phantom "behind" commits and risks messy merges. Right after tagging, **merge `master` back into `dev`** (`git checkout dev && git merge master && git push`) so `master` becomes an ancestor of `dev` again (verify with `git log dev..master` = empty). The `-dev` version bump on `dev` may land before or after this back-merge; the version line resolves to `dev`'s newer `-dev` automatically (no conflict), since `dev` changed it last relative to the merge base.

## CLI sub-project (cli/)

`cli/` is the independently distributed cross-platform command-line client `meebox` (for external agents / scripts to integrate via the [local API service](docs/arch/04-integration/01-service-api.md)). Design in [docs/arch/04-integration/02-cli.md](docs/arch/04-integration/02-cli.md), usage in [docs/guide/06-cli.md](docs/guide/06-cli.md).
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@
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/).

## [0.11.1] - 2026-07-14

> Highlights of this release:
>
> - **File-level comments**: comment on a whole file, not just a single line, where the platform supports it (Bitbucket / GitHub).
> - **Replies as drafts**: replying to a comment now creates a deferred draft — consistent with a new comment, persisted across switches, and published with the review batch.
> - **Project-aware reviews**: the review now reads the reviewed repository's own `AGENTS.md` as context.
> - **Git LFS status in the diff**: binary files show whether they're Git LFS-managed.
>
> Plus a fix so an in-progress inline comment isn't discarded when the comment list refreshes.

### ✨ Added

- Binary files in the diff (images, office documents, PDFs, …) now show their Git LFS status — a "Git LFS · &lt;size&gt;" 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.
- Reviews now pick up the reviewed repository's own guidance file (`AGENTS.md`) as project context, so the review, description, and suggestions follow the project's stated conventions.
- Replying to a comment now creates a **reply draft** instead of posting immediately — consistent with adding a new inline comment. The reply is persisted (so an unsubmitted reply survives switching PRs/files/tabs), shown below its parent comment on every surface (activity timeline + inline diff), and published together with your other drafts via "Publish comments".

### 🔧 Fixed

- An in-progress inline comment reply or edit in the diff is no longer discarded when the comment list refreshes (e.g. the poller pulls a new remote comment while you're typing) — the open editor keeps its text.

## [0.11.0] - 2026-07-07

> Highlights of this release:
Expand Down Expand Up @@ -465,6 +487,8 @@ 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.1...HEAD
[0.11.1]: https://github.com/huhamhire/code-meeseeks/compare/v0.11.0...v0.11.1
[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
24 changes: 24 additions & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@
本项目所有重要变更记录于此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## [0.11.1] - 2026-07-14

> 本次发布要点:
>
> - **文件级评论**:可对整个文件(而非仅某一行)添加评论(平台支持时,Bitbucket / GitHub)。
> - **回复即草稿**:回复评论现会生成延迟草稿——与新建评论一致,跨切换保留,随评审批量发布。
> - **项目感知的评审**:评审现会读取被审仓库自身的 `AGENTS.md` 作为上下文。
> - **diff 中的 Git LFS 状态**:二进制文件会显示其是否由 Git LFS 管理。
>
> 另修复:评论列表刷新时,正在编写的内联评论不再被丢弃。

### ✨ 新增

- diff 中的二进制文件(图片、Office 文档、PDF 等)现会显示其 Git LFS 状态——受 LFS 管理的文件显示「Git LFS · &lt;大小&gt;」标记,直接内联存入 git 的文件显示「⚠ 非 LFS」标记。
- 现可对整个文件(而不仅是某一行)添加评论(平台支持时,Bitbucket / GitHub):diff 中新增「对文件评论」入口,且已存在的文件级评论现能正确归属显示,不再被当作 PR 通用评论。
- 评审现会读取被审仓库自身的规范文件(`AGENTS.md`)作为项目上下文,使评审、描述与建议遵循该项目既定的约定。
- 回复评论现会生成一条**回复草稿**,而非立即提交——与新增内联评论的行为一致。回复被持久化(未提交的回复可跨 PR / 文件 / 标签页切换保留),在每个界面(活动时间线 + 内联 diff)都显示在其父评论下方,并随其他草稿一起通过「发布评论」批量发布。

### 🔧 修复

- 在 diff 中正在编写的内联评论回复 / 编辑,不再因评论列表刷新(如轮询在你输入时拉到新的远端评论)而被丢弃——打开的编辑器会保留已输入的内容。

## [0.11.0] - 2026-07-07

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

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

[Unreleased]: https://github.com/huhamhire/code-meeseeks/compare/v0.11.1...HEAD
[0.11.1]: https://github.com/huhamhire/code-meeseeks/compare/v0.11.0...v0.11.1
[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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@meebox/desktop",
"version": "0.11.0",
"version": "0.11.1",
"private": true,
"description": "meebox Electron desktop app",
"author": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""LocalGitProvider patch (version-guarded): binary-safe get_diff_files + get_line_link anchor."""
"""LocalGitProvider patch (version-guarded): binary-safe get_diff_files + get_line_link anchor + repo-context file fetch."""
from ..runtime import _EXPECTED_PRAGENT_VERSION, _pragent_version, _warn


Expand Down Expand Up @@ -122,3 +122,29 @@ def get_pr_labels(self, update=False):
return []

module.LocalGitProvider.get_pr_labels = get_pr_labels

# get_repo_file_content: pr-agent 0.39.0's configuration.toml ships a new default
# `repo_context_files = ["AGENTS.md"]` — build_repo_context() fetches those files from the reviewed
# repo and injects them as <instruction_files> so /review /describe /improve follow the project's own
# conventions. The base class returns "" (no-op), so LocalGitProvider is judged "does not support
# repository file fetching" and logs a WARNING each run while silently skipping the feature. Implement it
# by reading the blob straight from the base branch's tree object (not the working tree): the review's diff
# is head.commit vs merge-base(target_branch_name), and target_branch_name is the branch the PR merges
# into — the trusted "default/base branch" content the feature wants (repo_context_from_default_branch=true).
# Reading the tree object (never the working tree) also keeps this independent of _prepare_repo's working-tree
# sanitizing of agent instruction files (that guards the /ask CLI subprocess; repo_context serves the other
# tools). A missing file / any git error degrades to "" (no context) rather than raising, so
# build_repo_context treats it as "no context" and never caches a fetch error.
def get_repo_file_content(self, file_path, from_default_branch=False):
rel = (file_path or "").lstrip("/")
if not rel:
return ""
try:
# For a local provider there is no remote "default branch" distinct from the PR base, so both the
# default-branch and target-branch cases collapse to target_branch_name (guaranteed to exist by
# _prepare_repo). `git show <ref>:<path>` returns the file text, or errors if the path is absent.
return self.repo.git.show(f"{self.target_branch_name}:{rel}")
except Exception:
return ""

module.LocalGitProvider.get_repo_file_content = get_repo_file_content
79 changes: 65 additions & 14 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 Expand Up @@ -571,6 +590,18 @@ export const addDraft: IpcController<'drafts:create'> = async (_event, req) => {
if (draft.origin === 'manual' && draft.source) {
throw new Error('drafts:create: origin=manual must not pass source');
}
// Kind constraints: a reply must target a parent comment and is always a manually-authored draft (no source);
// a comment (default) must anchor to a line. Enforced here so malformed drafts never reach disk.
if (draft.kind === 'reply') {
if (!draft.replyTo?.parentCommentId) {
throw new Error('drafts:create: kind=reply requires replyTo.parentCommentId');
}
if (draft.origin !== 'manual') {
throw new Error('drafts:create: kind=reply must be origin=manual');
}
} else if (!draft.anchor) {
throw new Error('drafts:create: a comment draft requires an anchor');
}
const created = await createDraft(await ctx.pr.storeForPr(localId), localId, draft);
ctx.broadcast('drafts:changed', { localId });
return created;
Expand Down Expand Up @@ -655,20 +686,40 @@ export const publishDraftBatch: IpcController<'drafts:publishBatch'> = async (_e
continue;
}
try {
// ReviewDraftAnchor → PrCommentAnchor: side maps conservatively new→added / old→removed;
// multi-line lands on endLine (the comment appears below the annotated range, not interrupting top-down reading). Hitting a context line
// makes Bitbucket return 400; the error is collected into results for the user to see.
const posted = await adapter.comments.publishInlineComment(
{ projectKey: pr.repo.projectKey, repoSlug: pr.repo.repoSlug },
pr.remoteId,
{
path: draft.anchor.path,
line: draft.anchor.endLine,
side: draft.anchor.side,
lineType: draft.anchor.side === 'old' ? 'removed' : 'added',
},
draft.body,
);
let posted: { remoteId: string };
if (draft.kind === 'reply') {
// Reply-draft: publish against the parent comment via the reply API (not a fresh inline comment). The parent
// already exists remotely, so ordering within the batch doesn't matter.
if (!draft.replyTo?.parentCommentId) {
results.push({ draftId, ok: false, error: 'reply draft missing replyTo.parentCommentId' });
continue;
}
posted = await adapter.comments.replyToComment(
{ projectKey: pr.repo.projectKey, repoSlug: pr.repo.repoSlug },
pr.remoteId,
draft.replyTo.parentCommentId,
draft.body,
);
} else {
if (!draft.anchor) {
results.push({ draftId, ok: false, error: 'comment draft missing anchor' });
continue;
}
// ReviewDraftAnchor → PrCommentAnchor: side maps conservatively new→added / old→removed;
// multi-line lands on endLine (the comment appears below the annotated range, not interrupting top-down reading). Hitting a context line
// makes Bitbucket return 400; the error is collected into results for the user to see.
posted = await adapter.comments.publishInlineComment(
{ projectKey: pr.repo.projectKey, repoSlug: pr.repo.repoSlug },
pr.remoteId,
{
path: draft.anchor.path,
line: draft.anchor.endLine,
side: draft.anchor.side,
lineType: draft.anchor.side === 'old' ? 'removed' : 'added',
},
draft.body,
);
}
// Successful publish = the local draft's mission is done, delete it directly (the remote comment is pulled back and takes over display via the force-refresh below).
await deleteDraft(store, req.localId, draftId);
anyPublished = true;
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 All @@ -243,7 +245,8 @@ export function PrPanel({
readOnly={readOnly}
onJumpToAnchor={(draftId) => {
const d = (drafts ?? []).find((x) => x.id === draftId);
if (!d) return;
// Reply-drafts with no anchor (reply to a summary comment) aren't line-navigable.
if (!d?.anchor) return;
onRequestDiffNav?.({
anchor: {
path: d.anchor.path,
Expand All @@ -270,7 +273,8 @@ export function PrPanel({
// Click anchor → close modal + turn into pendingDiffNav bubbled up to App. runId/findingId omitted →
// DiffView only navigates, doesn't enter edit (the user wants to see the code context, not necessarily edit the draft).
const d = (drafts ?? []).find((x) => x.id === draftId);
if (!d) return;
// Reply-drafts with no anchor (reply to a summary comment) aren't line-navigable.
if (!d?.anchor) return;
setPublishModalOpen(false);
onRequestDiffNav?.({
anchor: {
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
Loading
Loading