Skip to content

Commit 9eb8b26

Browse files
committed
Merge branch 'main' into feat/opentui-solid-export
2 parents 35e73be + 9b01f12 commit 9eb8b26

49 files changed

Lines changed: 2700 additions & 487 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-prebuilt-npm.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ jobs:
184184
- name: Download platform artifacts
185185
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
186186
with:
187+
pattern: hunkdiff-*
187188
path: dist/release/artifacts
188189

189190
- name: Archive release assets
@@ -205,7 +206,7 @@ jobs:
205206
fi
206207
chmod 0755 "$binary"
207208
tar -C "$(dirname "$directory")" -czf "dist/release/github/${package_name}.tar.gz" "$package_name"
208-
done < <(find dist/release/artifacts -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)
209+
done < <(find dist/release/artifacts -mindepth 1 -maxdepth 1 -type d -name 'hunkdiff-*' -print0 | sort -z)
209210
find dist/release/github -maxdepth 1 -type f | sort
210211
211212
- name: Create or update GitHub release

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,27 @@ All notable user-visible changes to Hunk are documented in this file.
66

77
### Added
88

9+
### Changed
10+
11+
### Fixed
12+
13+
## [0.13.0-beta.0] - 2026-05-16
14+
15+
### Added
16+
17+
- Added an `e` shortcut to open the selected diff file in `$EDITOR`.
918
- Added `g` and `G` keyboard aliases for jump-to-top and jump-to-bottom review navigation.
19+
- Added session-persistent user-authored inline notes with `c` to draft/save notes.
20+
- Added `hunk session comment list --type <live|all|ai|agent|user>` so agents can read human-authored notes through the comment workflow.
1021

1122
### Changed
1223

24+
- Clarified inline note draft actions by labeling buttons as `Save (^S)` and `Cancel (Esc)`.
25+
1326
### Fixed
1427

28+
- Fixed draft note focus handling so app shortcuts resume after the note textarea blurs without discarding the draft.
29+
- Preserved the resolved auto theme across `--watch` refreshes instead of falling back to the default dark theme.
1530
- Included the bundled Hunk review skill in standalone prebuilt release archives so `hunk skill path` works after extracting a tarball or installing via Homebrew.
1631

1732
## [0.12.0] - 2026-05-12
@@ -316,7 +331,8 @@ All notable user-visible changes to Hunk are documented in this file.
316331

317332
- Stabilized diff repainting, active-hunk scrolling, syntax highlighting, pager stdin patch handling, and terminal cleanup on exit.
318333

319-
[Unreleased]: https://github.com/modem-dev/hunk/compare/v0.12.0...HEAD
334+
[Unreleased]: https://github.com/modem-dev/hunk/compare/v0.13.0-beta.0...HEAD
335+
[0.13.0-beta.0]: https://github.com/modem-dev/hunk/compare/v0.12.0...v0.13.0-beta.0
320336
[0.12.0]: https://github.com/modem-dev/hunk/compare/v0.11.1...v0.12.0
321337
[0.11.1]: https://github.com/modem-dev/hunk/compare/v0.11.0...v0.11.1
322338
[0.11.0]: https://github.com/modem-dev/hunk/compare/v0.10.0...v0.11.0

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hunkdiff",
3-
"version": "0.12.0",
3+
"version": "0.13.0-beta.0",
44
"description": "Desktop-inspired terminal diff viewer for understanding agent-authored changesets.",
55
"keywords": [
66
"ai",

packages/session-broker-core/src/brokerState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export interface SessionBrokerViewAdapter<
5353
buildSelectedContext: (session: ListedSession) => SelectedContext;
5454
buildSessionReview: (
5555
entry: SessionBrokerEntry<Info, State>,
56-
options: { includePatch?: boolean },
56+
options: { includePatch?: boolean; includeNotes?: boolean },
5757
) => SessionReview;
5858
listComments: (session: ListedSession, filter: { filePath?: string }) => SessionCommentSummary[];
5959
}
@@ -174,7 +174,7 @@ export class SessionBrokerState<
174174
/** Return the live session's loaded review model, with raw patch text included only on demand. */
175175
getSessionReview(
176176
selector: SessionTargetSelector,
177-
options: { includePatch?: boolean } = {},
177+
options: { includePatch?: boolean; includeNotes?: boolean } = {},
178178
): SessionReview {
179179
return this.view.buildSessionReview(this.getSessionEntry(selector), options);
180180
}

skills/hunk-review/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,12 @@ hunk session reload --session-path /path/to/live-window --source /path/to/other-
9898
```bash
9999
hunk session comment add --repo . --file README.md --new-line 103 --summary "Tighten this wording" [--rationale "..."] [--author "agent"] [--focus]
100100
printf '%s\n' '{"comments":[{"filePath":"README.md","newLine":103,"summary":"Tighten this wording"}]}' | hunk session comment apply --repo . --stdin [--focus]
101-
hunk session comment list --repo . [--file README.md]
101+
hunk session comment list --repo . [--file README.md] [--type live|all|ai|agent|user]
102102
hunk session comment rm --repo . <comment-id>
103103
hunk session comment clear --repo . --yes [--file README.md]
104104
```
105105

106+
- `comment list --type user` shows human-authored inline notes; without `--type`, `comment list` preserves the legacy live-agent-comment view
106107
- `comment add` is best for one note; `comment apply` is best when an agent already has several notes ready
107108
- `comment add` requires `--file`, `--summary`, and exactly one of `--old-line` or `--new-line`
108109
- `comment apply` payload items require `filePath`, `summary`, and exactly one target such as `hunk`, `hunkNumber`, `oldLine`, or `newLine`

src/core/cli.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,27 @@ describe("parseCli", () => {
301301
});
302302
});
303303

304+
test("parses session review with live notes included", async () => {
305+
const parsed = await parseCli([
306+
"bun",
307+
"hunk",
308+
"session",
309+
"review",
310+
"session-1",
311+
"--include-notes",
312+
"--json",
313+
]);
314+
315+
expect(parsed).toMatchObject({
316+
kind: "session",
317+
action: "review",
318+
selector: { sessionId: "session-1" },
319+
output: "json",
320+
includePatch: false,
321+
includeNotes: true,
322+
});
323+
});
324+
304325
test("parses session navigate by hunk number", async () => {
305326
const parsed = await parseCli([
306327
"bun",
@@ -563,6 +584,39 @@ describe("parseCli", () => {
563584
});
564585
});
565586

587+
test("rejects the removed session note namespace", async () => {
588+
await expect(parseCli(["bun", "hunk", "session", "note", "list", "session-1"])).rejects.toThrow(
589+
"Unknown session command: note",
590+
);
591+
});
592+
593+
test("parses session comment list with review-note type filter", async () => {
594+
const parsed = await parseCli([
595+
"bun",
596+
"hunk",
597+
"session",
598+
"comment",
599+
"list",
600+
"session-1",
601+
"--type",
602+
"user",
603+
]);
604+
605+
expect(parsed).toEqual({
606+
kind: "session",
607+
action: "comment-list",
608+
selector: { sessionId: "session-1" },
609+
type: "user",
610+
output: "text",
611+
});
612+
});
613+
614+
test("rejects session comment list with an unsupported type", async () => {
615+
await expect(
616+
parseCli(["bun", "hunk", "session", "comment", "list", "session-1", "--type", "robot"]),
617+
).rejects.toThrow("Comment type must be one of live, all, ai, agent, or user.");
618+
});
619+
566620
test("parses session comment rm", async () => {
567621
const parsed = await parseCli([
568622
"bun",

src/core/cli.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
LayoutMode,
99
PagerCommandInput,
1010
ParsedCliInput,
11+
SessionCommentListType,
1112
SessionCommentApplyItemInput,
1213
} from "./types";
1314
import { resolveBundledHunkReviewSkillPath } from "./paths";
@@ -596,15 +597,15 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
596597
" hunk session get --repo <path>",
597598
" hunk session context <session-id>",
598599
" hunk session context --repo <path>",
599-
" hunk session review <session-id> [--include-patch]",
600-
" hunk session review --repo <path> [--include-patch]",
600+
" hunk session review <session-id> [--include-patch] [--include-notes]",
601+
" hunk session review --repo <path> [--include-patch] [--include-notes]",
601602
" hunk session navigate (<session-id> | --repo <path>) --file <path> (--hunk <n> | --old-line <n> | --new-line <n>)",
602603
" hunk session navigate (<session-id> | --repo <path>) (--next-comment | --prev-comment)",
603604
" hunk session reload (<session-id> | --repo <path> | --session-path <path>) [--source <path>] -- diff [ref] [-- <pathspec...>]",
604605
" hunk session reload (<session-id> | --repo <path> | --session-path <path>) [--source <path>] -- show [ref] [-- <pathspec...>]",
605606
" hunk session comment add (<session-id> | --repo <path>) --file <path> (--old-line <n> | --new-line <n>) --summary <text> [--focus]",
606607
" hunk session comment apply (<session-id> | --repo <path>) --stdin [--focus]",
607-
" hunk session comment list (<session-id> | --repo <path>)",
608+
" hunk session comment list (<session-id> | --repo <path>) [--type <live|all|ai|agent|user>]",
608609
" hunk session comment rm (<session-id> | --repo <path>) <comment-id>",
609610
" hunk session comment clear (<session-id> | --repo <path>) --yes",
610611
].join("\n") + "\n",
@@ -647,19 +648,23 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
647648
.option("--json", "emit structured JSON");
648649

649650
if (subcommand === "review") {
650-
command.option(
651-
"--include-patch",
652-
"include raw unified diff text for each file in review output",
653-
);
651+
command
652+
.option("--include-patch", "include raw unified diff text for each file in review output")
653+
.option("--include-notes", "include live review notes in review output");
654654
}
655655

656656
let parsedSessionId: string | undefined;
657-
let parsedOptions: { repo?: string; includePatch?: boolean; json?: boolean } = {};
657+
let parsedOptions: {
658+
repo?: string;
659+
includePatch?: boolean;
660+
includeNotes?: boolean;
661+
json?: boolean;
662+
} = {};
658663

659664
command.action(
660665
(
661666
sessionId: string | undefined,
662-
options: { repo?: string; includePatch?: boolean; json?: boolean },
667+
options: { repo?: string; includePatch?: boolean; includeNotes?: boolean; json?: boolean },
663668
) => {
664669
parsedSessionId = sessionId;
665670
parsedOptions = options;
@@ -678,6 +683,7 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
678683
output: resolveJsonOutput(parsedOptions),
679684
selector: resolveExplicitSessionSelector(parsedSessionId, parsedOptions.repo),
680685
includePatch: parsedOptions.includePatch ?? false,
686+
includeNotes: parsedOptions.includeNotes ?? false,
681687
};
682688
}
683689

@@ -873,7 +879,7 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
873879
"Usage:",
874880
" hunk session comment add (<session-id> | --repo <path>) --file <path> (--old-line <n> | --new-line <n>) --summary <text> [--focus]",
875881
" hunk session comment apply (<session-id> | --repo <path>) --stdin [--focus]",
876-
" hunk session comment list (<session-id> | --repo <path>) [--file <path>]",
882+
" hunk session comment list (<session-id> | --repo <path>) [--file <path>] [--type <live|all|ai|agent|user>]",
877883
" hunk session comment rm (<session-id> | --repo <path>) <comment-id>",
878884
" hunk session comment clear (<session-id> | --repo <path>) [--file <path>] --yes",
879885
].join("\n") + "\n",
@@ -1039,15 +1045,16 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
10391045
.argument("[sessionId]")
10401046
.option("--repo <path>", "target the live session whose repo root matches this path")
10411047
.option("--file <path>", "filter comments to one diff file")
1048+
.option("--type <type>", "filter to live, all, ai, agent, or user comments")
10421049
.option("--json", "emit structured JSON");
10431050

10441051
let parsedSessionId: string | undefined;
1045-
let parsedOptions: { repo?: string; file?: string; json?: boolean } = {};
1052+
let parsedOptions: { repo?: string; file?: string; type?: string; json?: boolean } = {};
10461053

10471054
command.action(
10481055
(
10491056
sessionId: string | undefined,
1050-
options: { repo?: string; file?: string; json?: boolean },
1057+
options: { repo?: string; file?: string; type?: string; json?: boolean },
10511058
) => {
10521059
parsedSessionId = sessionId;
10531060
parsedOptions = options;
@@ -1059,13 +1066,24 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
10591066
}
10601067

10611068
await parseStandaloneCommand(command, commentRest);
1069+
if (
1070+
parsedOptions.type !== undefined &&
1071+
parsedOptions.type !== "live" &&
1072+
parsedOptions.type !== "all" &&
1073+
parsedOptions.type !== "ai" &&
1074+
parsedOptions.type !== "agent" &&
1075+
parsedOptions.type !== "user"
1076+
) {
1077+
throw new Error("Comment type must be one of live, all, ai, agent, or user.");
1078+
}
10621079

10631080
return {
10641081
kind: "session",
10651082
action: "comment-list",
10661083
output: resolveJsonOutput(parsedOptions),
10671084
selector: resolveExplicitSessionSelector(parsedSessionId, parsedOptions.repo),
10681085
filePath: parsedOptions.file,
1086+
...(parsedOptions.type ? { type: parsedOptions.type as SessionCommentListType } : {}),
10691087
};
10701088
}
10711089

src/core/jj.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ afterEach(() => {
8282
cleanupTempDirs();
8383
});
8484

85+
// Keep jj-backed integration checks opt-in on machines that have the external CLI installed.
86+
const jjTest = Bun.which("jj") ? test : test.skip;
87+
8588
describe("jj command helpers", () => {
8689
test("reports a friendly error when jj is not installed or not on PATH", () => {
8790
expect(() =>
@@ -99,7 +102,7 @@ describe("jj command helpers", () => {
99102
);
100103
});
101104

102-
test("reports a friendly error outside a jj repository", () => {
105+
jjTest("reports a friendly error outside a jj repository", () => {
103106
const dir = createTempDir("hunk-jj-nonrepo-");
104107

105108
expect(() =>
@@ -115,7 +118,7 @@ describe("jj command helpers", () => {
115118
).toThrow('`hunk diff` must be run inside a Jujutsu repository when `vcs = "jj"`.');
116119
});
117120

118-
test("reports a friendly error for invalid revsets", () => {
121+
jjTest("reports a friendly error for invalid revsets", () => {
119122
const dir = createTempJjRepo("hunk-jj-invalid-revset-");
120123
const input = {
121124
kind: "vcs" as const,
@@ -133,7 +136,7 @@ describe("jj command helpers", () => {
133136
).toThrow("`hunk diff missing_revision` could not resolve Jujutsu revset `missing_revision`.");
134137
});
135138

136-
test(
139+
jjTest(
137140
"reports a friendly error for ambiguous change id prefixes",
138141
() => {
139142
const dir = createTempJjRepo("hunk-jj-ambiguous-prefix-");

src/core/loaders.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ function createTempJjRepo(prefix: string) {
9494
return dir;
9595
}
9696

97+
// Keep jj-backed loader coverage opt-in on machines that have the external CLI installed.
98+
const jjTest = Bun.which("jj") ? test : test.skip;
99+
97100
async function runWithHome<T>(home: string, task: () => Promise<T>) {
98101
const previousHome = process.env.HOME;
99102
process.env.HOME = home;
@@ -773,7 +776,7 @@ describe("loadAppBootstrap", () => {
773776
expect(bootstrap.changeset.files.map((file) => file.path)).toEqual(["beta.ts"]);
774777
});
775778

776-
test(
779+
jjTest(
777780
"loads jj diff output for a configured revset",
778781
async () => {
779782
const home = createTempDir("hunk-jj-home-");
@@ -802,7 +805,7 @@ describe("loadAppBootstrap", () => {
802805
JjLoaderIntegrationTestTimeoutMs,
803806
);
804807

805-
test(
808+
jjTest(
806809
"loads jj show output for a configured revset",
807810
async () => {
808811
const home = createTempDir("hunk-jj-home-");

src/core/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ export type LayoutMode = "auto" | "split" | "stack";
44
export type VcsMode = "git" | "jj";
55
export type TerminalThemeMode = "light" | "dark";
66

7+
export type ReviewNoteSource = "ai" | "agent" | "user";
8+
export type SessionCommentListType = "live" | "all" | ReviewNoteSource;
9+
10+
export interface UserNoteLineTarget {
11+
side: "old" | "new";
12+
line: number;
13+
}
14+
715
export interface AgentAnnotation {
816
id?: string;
917
oldRange?: [number, number];
@@ -13,8 +21,11 @@ export interface AgentAnnotation {
1321
tags?: string[];
1422
confidence?: "low" | "medium" | "high";
1523
source?: string;
24+
title?: string;
1625
author?: string;
1726
createdAt?: string;
27+
updatedAt?: string;
28+
editable?: boolean;
1829
}
1930

2031
export interface AgentFileContext {
@@ -120,6 +131,7 @@ export interface SessionReviewCommandInput {
120131
output: SessionCommandOutput;
121132
selector: SessionSelectorInput;
122133
includePatch: boolean;
134+
includeNotes?: boolean;
123135
}
124136

125137
export interface SessionNavigateCommandInput {
@@ -182,6 +194,7 @@ export interface SessionCommentListCommandInput {
182194
output: SessionCommandOutput;
183195
selector: SessionSelectorInput;
184196
filePath?: string;
197+
type?: SessionCommentListType;
185198
}
186199

187200
export interface SessionCommentRemoveCommandInput {

0 commit comments

Comments
 (0)