Skip to content

Commit 02cb20d

Browse files
authored
Merge pull request #85 from watany-dev/claude/plan-split-view-6QduD
2 parents ba819e5 + 3df9f14 commit 02cb20d

8 files changed

Lines changed: 1020 additions & 14 deletions

File tree

src/components/Help.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,11 @@ describe("Help", () => {
162162
const { lastFrame } = render(<Help onClose={vi.fn()} />);
163163
expect(lastFrame()).toContain("File list");
164164
});
165+
166+
it("shows s Split/unified diff keybinding", () => {
167+
const { lastFrame } = render(<Help onClose={vi.fn()} />);
168+
const output = lastFrame();
169+
expect(output).toContain("s");
170+
expect(output).toContain("Split/unified diff");
171+
});
165172
});

src/components/Help.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export function Help({ onClose }: Props) {
5050
<KeyRow keyName="n" description="Next file" />
5151
<KeyRow keyName="N" description="Previous file" />
5252
<KeyRow keyName="f" description="File list" />
53+
<KeyRow keyName="s" description="Split/unified diff" />
5354
<KeyRow keyName="Enter" description="Select / confirm" />
5455
<KeyRow keyName="q / Esc" description="Back / quit" />
5556
<KeyRow keyName="Ctrl+C" description="Exit immediately" />

src/components/PullRequestDetail.test.tsx

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7558,4 +7558,306 @@ describe("PullRequestDetail", () => {
75587558
expect(output).toContain("baz");
75597559
});
75607560
});
7561+
7562+
describe("split view", () => {
7563+
it("toggles to split view with s key", async () => {
7564+
const { stdin, lastFrame } = render(
7565+
<PullRequestDetail
7566+
pullRequest={pullRequest as any}
7567+
differences={differences as any}
7568+
commentThreads={[]}
7569+
diffTexts={diffTexts}
7570+
onBack={vi.fn()}
7571+
onHelp={vi.fn()}
7572+
onShowActivity={vi.fn()}
7573+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7574+
inlineComment={defaultInlineCommentProps}
7575+
reply={defaultReplyProps}
7576+
approval={defaultApprovalProps}
7577+
merge={defaultMergeProps}
7578+
close={defaultCloseProps}
7579+
commitView={defaultCommitProps}
7580+
editComment={defaultEditCommentProps}
7581+
deleteComment={defaultDeleteCommentProps}
7582+
reaction={defaultReactionProps}
7583+
terminalWidth={120}
7584+
/>,
7585+
);
7586+
stdin.write("s");
7587+
await vi.waitFor(() => {
7588+
expect(lastFrame()).toContain("s unified");
7589+
});
7590+
const output = lastFrame() ?? "";
7591+
expect(output).toContain("│");
7592+
});
7593+
7594+
it("toggles back to unified view with s key", async () => {
7595+
const { stdin, lastFrame } = render(
7596+
<PullRequestDetail
7597+
pullRequest={pullRequest as any}
7598+
differences={differences as any}
7599+
commentThreads={[]}
7600+
diffTexts={diffTexts}
7601+
onBack={vi.fn()}
7602+
onHelp={vi.fn()}
7603+
onShowActivity={vi.fn()}
7604+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7605+
inlineComment={defaultInlineCommentProps}
7606+
reply={defaultReplyProps}
7607+
approval={defaultApprovalProps}
7608+
merge={defaultMergeProps}
7609+
close={defaultCloseProps}
7610+
commitView={defaultCommitProps}
7611+
editComment={defaultEditCommentProps}
7612+
deleteComment={defaultDeleteCommentProps}
7613+
reaction={defaultReactionProps}
7614+
terminalWidth={120}
7615+
/>,
7616+
);
7617+
stdin.write("s");
7618+
await vi.waitFor(() => {
7619+
expect(lastFrame()).toContain("s unified");
7620+
});
7621+
stdin.write("s");
7622+
await vi.waitFor(() => {
7623+
expect(lastFrame()).toContain("s split");
7624+
});
7625+
});
7626+
7627+
it("falls back to unified when terminal is too narrow", () => {
7628+
const { stdin, lastFrame } = render(
7629+
<PullRequestDetail
7630+
pullRequest={pullRequest as any}
7631+
differences={differences as any}
7632+
commentThreads={[]}
7633+
diffTexts={diffTexts}
7634+
onBack={vi.fn()}
7635+
onHelp={vi.fn()}
7636+
onShowActivity={vi.fn()}
7637+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7638+
inlineComment={defaultInlineCommentProps}
7639+
reply={defaultReplyProps}
7640+
approval={defaultApprovalProps}
7641+
merge={defaultMergeProps}
7642+
close={defaultCloseProps}
7643+
commitView={defaultCommitProps}
7644+
editComment={defaultEditCommentProps}
7645+
deleteComment={defaultDeleteCommentProps}
7646+
reaction={defaultReactionProps}
7647+
terminalWidth={80}
7648+
/>,
7649+
);
7650+
stdin.write("s");
7651+
const output = lastFrame() ?? "";
7652+
// 80列ではsplit view区切り線が表示されない
7653+
expect(output).not.toContain("s split");
7654+
expect(output).not.toContain("s unified");
7655+
});
7656+
7657+
it("navigates with j/k in split mode", async () => {
7658+
const { stdin, lastFrame } = render(
7659+
<PullRequestDetail
7660+
pullRequest={pullRequest as any}
7661+
differences={differences as any}
7662+
commentThreads={[]}
7663+
diffTexts={diffTexts}
7664+
onBack={vi.fn()}
7665+
onHelp={vi.fn()}
7666+
onShowActivity={vi.fn()}
7667+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7668+
inlineComment={defaultInlineCommentProps}
7669+
reply={defaultReplyProps}
7670+
approval={defaultApprovalProps}
7671+
merge={defaultMergeProps}
7672+
close={defaultCloseProps}
7673+
commitView={defaultCommitProps}
7674+
editComment={defaultEditCommentProps}
7675+
deleteComment={defaultDeleteCommentProps}
7676+
reaction={defaultReactionProps}
7677+
terminalWidth={120}
7678+
/>,
7679+
);
7680+
stdin.write("s");
7681+
await vi.waitFor(() => {
7682+
expect(lastFrame()).toContain("s unified");
7683+
});
7684+
stdin.write("j");
7685+
stdin.write("j");
7686+
const output = lastFrame() ?? "";
7687+
expect(output).toContain("> ");
7688+
});
7689+
7690+
it("navigates with n/N in split mode", async () => {
7691+
const multiFileDiffTexts = new Map([
7692+
["b1:b2", { before: "line1\nline2", after: "line1\nmodified" }],
7693+
["b3:b4", { before: "foo\nbar", after: "foo\nbaz" }],
7694+
]);
7695+
const multiDifferences = [
7696+
{
7697+
beforeBlob: { blobId: "b1", path: "file1.ts" },
7698+
afterBlob: { blobId: "b2", path: "file1.ts" },
7699+
},
7700+
{
7701+
beforeBlob: { blobId: "b3", path: "file2.ts" },
7702+
afterBlob: { blobId: "b4", path: "file2.ts" },
7703+
},
7704+
];
7705+
const { stdin, lastFrame } = render(
7706+
<PullRequestDetail
7707+
pullRequest={pullRequest as any}
7708+
differences={multiDifferences as any}
7709+
commentThreads={[]}
7710+
diffTexts={multiFileDiffTexts}
7711+
onBack={vi.fn()}
7712+
onHelp={vi.fn()}
7713+
onShowActivity={vi.fn()}
7714+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7715+
inlineComment={defaultInlineCommentProps}
7716+
reply={defaultReplyProps}
7717+
approval={defaultApprovalProps}
7718+
merge={defaultMergeProps}
7719+
close={defaultCloseProps}
7720+
commitView={defaultCommitProps}
7721+
editComment={defaultEditCommentProps}
7722+
deleteComment={defaultDeleteCommentProps}
7723+
reaction={defaultReactionProps}
7724+
terminalWidth={120}
7725+
/>,
7726+
);
7727+
stdin.write("s");
7728+
await vi.waitFor(() => {
7729+
expect(lastFrame()).toContain("s unified");
7730+
});
7731+
stdin.write("n");
7732+
const output = lastFrame() ?? "";
7733+
expect(output).toContain("file2.ts");
7734+
});
7735+
7736+
it("scrolls with Ctrl+d in split mode", async () => {
7737+
const { stdin, lastFrame } = render(
7738+
<PullRequestDetail
7739+
pullRequest={pullRequest as any}
7740+
differences={differences as any}
7741+
commentThreads={[]}
7742+
diffTexts={diffTexts}
7743+
onBack={vi.fn()}
7744+
onHelp={vi.fn()}
7745+
onShowActivity={vi.fn()}
7746+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7747+
inlineComment={defaultInlineCommentProps}
7748+
reply={defaultReplyProps}
7749+
approval={defaultApprovalProps}
7750+
merge={defaultMergeProps}
7751+
close={defaultCloseProps}
7752+
commitView={defaultCommitProps}
7753+
editComment={defaultEditCommentProps}
7754+
deleteComment={defaultDeleteCommentProps}
7755+
reaction={defaultReactionProps}
7756+
terminalWidth={120}
7757+
/>,
7758+
);
7759+
stdin.write("s");
7760+
await vi.waitFor(() => {
7761+
expect(lastFrame()).toContain("s unified");
7762+
});
7763+
stdin.write("\x04"); // Ctrl+d
7764+
const output = lastFrame() ?? "";
7765+
expect(output).toBeDefined();
7766+
});
7767+
7768+
it("does not toggle split view during comment input", async () => {
7769+
const { stdin, lastFrame } = render(
7770+
<PullRequestDetail
7771+
pullRequest={pullRequest as any}
7772+
differences={differences as any}
7773+
commentThreads={[]}
7774+
diffTexts={diffTexts}
7775+
onBack={vi.fn()}
7776+
onHelp={vi.fn()}
7777+
onShowActivity={vi.fn()}
7778+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7779+
inlineComment={defaultInlineCommentProps}
7780+
reply={defaultReplyProps}
7781+
approval={defaultApprovalProps}
7782+
merge={defaultMergeProps}
7783+
close={defaultCloseProps}
7784+
commitView={defaultCommitProps}
7785+
editComment={defaultEditCommentProps}
7786+
deleteComment={defaultDeleteCommentProps}
7787+
reaction={defaultReactionProps}
7788+
terminalWidth={120}
7789+
/>,
7790+
);
7791+
stdin.write("c");
7792+
await vi.waitFor(() => {
7793+
expect(lastFrame()).toContain("Comment:");
7794+
});
7795+
stdin.write("s");
7796+
expect(lastFrame()).toContain("Comment:");
7797+
});
7798+
7799+
it("defaults to 120 width when terminalWidth prop is not provided", async () => {
7800+
const { stdin, lastFrame } = render(
7801+
<PullRequestDetail
7802+
pullRequest={pullRequest as any}
7803+
differences={differences as any}
7804+
commentThreads={[]}
7805+
diffTexts={diffTexts}
7806+
onBack={vi.fn()}
7807+
onHelp={vi.fn()}
7808+
onShowActivity={vi.fn()}
7809+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7810+
inlineComment={defaultInlineCommentProps}
7811+
reply={defaultReplyProps}
7812+
approval={defaultApprovalProps}
7813+
merge={defaultMergeProps}
7814+
close={defaultCloseProps}
7815+
commitView={defaultCommitProps}
7816+
editComment={defaultEditCommentProps}
7817+
deleteComment={defaultDeleteCommentProps}
7818+
reaction={defaultReactionProps}
7819+
/>,
7820+
);
7821+
// No terminalWidth prop → falls back to stdout?.columns ?? 120
7822+
// Since ink-testing-library has no real stdout, uses 120 which is >= 100
7823+
expect(lastFrame()).toContain("s split");
7824+
stdin.write("s");
7825+
await vi.waitFor(() => {
7826+
expect(lastFrame()).toContain("s unified");
7827+
});
7828+
});
7829+
});
7830+
7831+
describe("navigation guards with empty diff", () => {
7832+
it("handles Ctrl+d, Ctrl+u, and G when lines are empty", () => {
7833+
const { stdin, lastFrame } = render(
7834+
<PullRequestDetail
7835+
pullRequest={pullRequest as any}
7836+
differences={[]}
7837+
commentThreads={[]}
7838+
diffTexts={new Map()}
7839+
onBack={vi.fn()}
7840+
onHelp={vi.fn()}
7841+
onShowActivity={vi.fn()}
7842+
comment={{ onPost: vi.fn(), isProcessing: false, error: null, onClearError: vi.fn() }}
7843+
inlineComment={defaultInlineCommentProps}
7844+
reply={defaultReplyProps}
7845+
approval={defaultApprovalProps}
7846+
merge={defaultMergeProps}
7847+
close={defaultCloseProps}
7848+
commitView={defaultCommitProps}
7849+
editComment={defaultEditCommentProps}
7850+
deleteComment={defaultDeleteCommentProps}
7851+
reaction={defaultReactionProps}
7852+
terminalWidth={120}
7853+
/>,
7854+
);
7855+
// These should hit early return guards (lines.length === 0)
7856+
stdin.write("\x04"); // Ctrl+d
7857+
stdin.write("\x15"); // Ctrl+u
7858+
stdin.write("G");
7859+
// Component should still render without crash
7860+
expect(lastFrame()).toBeDefined();
7861+
});
7862+
});
75617863
});

0 commit comments

Comments
 (0)