im-pickle-rick 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -0
- package/bin.js +3 -0
- package/dist/pickle +0 -0
- package/dist/worker-executor.js +207 -0
- package/package.json +53 -0
- package/src/games/GameSidebarManager.test.ts +64 -0
- package/src/games/GameSidebarManager.ts +78 -0
- package/src/games/gameboy/GameboyView.test.ts +25 -0
- package/src/games/gameboy/GameboyView.ts +100 -0
- package/src/games/gameboy/gameboy-polyfills.ts +313 -0
- package/src/games/index.test.ts +9 -0
- package/src/games/index.ts +4 -0
- package/src/games/snake/SnakeGame.test.ts +35 -0
- package/src/games/snake/SnakeGame.ts +145 -0
- package/src/games/snake/SnakeView.test.ts +25 -0
- package/src/games/snake/SnakeView.ts +290 -0
- package/src/index.test.ts +24 -0
- package/src/index.ts +141 -0
- package/src/services/commands/worker.test.ts +14 -0
- package/src/services/commands/worker.ts +262 -0
- package/src/services/config/index.ts +2 -0
- package/src/services/config/settings.test.ts +42 -0
- package/src/services/config/settings.ts +220 -0
- package/src/services/config/state.test.ts +88 -0
- package/src/services/config/state.ts +130 -0
- package/src/services/config/types.ts +39 -0
- package/src/services/execution/index.ts +1 -0
- package/src/services/execution/pickle-source.test.ts +88 -0
- package/src/services/execution/pickle-source.ts +264 -0
- package/src/services/execution/prompt.test.ts +93 -0
- package/src/services/execution/prompt.ts +322 -0
- package/src/services/execution/sequential.test.ts +91 -0
- package/src/services/execution/sequential.ts +422 -0
- package/src/services/execution/worker-client.ts +94 -0
- package/src/services/execution/worker-executor.ts +41 -0
- package/src/services/execution/worker.test.ts +73 -0
- package/src/services/git/branch.test.ts +147 -0
- package/src/services/git/branch.ts +128 -0
- package/src/services/git/diff.test.ts +113 -0
- package/src/services/git/diff.ts +323 -0
- package/src/services/git/index.ts +4 -0
- package/src/services/git/pr.test.ts +104 -0
- package/src/services/git/pr.ts +192 -0
- package/src/services/git/worktree.test.ts +99 -0
- package/src/services/git/worktree.ts +141 -0
- package/src/services/providers/base.test.ts +86 -0
- package/src/services/providers/base.ts +438 -0
- package/src/services/providers/codex.test.ts +39 -0
- package/src/services/providers/codex.ts +208 -0
- package/src/services/providers/gemini.test.ts +40 -0
- package/src/services/providers/gemini.ts +169 -0
- package/src/services/providers/index.test.ts +28 -0
- package/src/services/providers/index.ts +41 -0
- package/src/services/providers/opencode.test.ts +64 -0
- package/src/services/providers/opencode.ts +228 -0
- package/src/services/providers/types.ts +44 -0
- package/src/skills/code-implementer.md +105 -0
- package/src/skills/code-researcher.md +78 -0
- package/src/skills/implementation-planner.md +105 -0
- package/src/skills/plan-reviewer.md +100 -0
- package/src/skills/prd-drafter.md +123 -0
- package/src/skills/research-reviewer.md +79 -0
- package/src/skills/ruthless-refactorer.md +52 -0
- package/src/skills/ticket-manager.md +135 -0
- package/src/types/index.ts +2 -0
- package/src/types/rpc.ts +14 -0
- package/src/types/tasks.ts +50 -0
- package/src/types.d.ts +9 -0
- package/src/ui/common.ts +28 -0
- package/src/ui/components/FilePickerView.test.ts +79 -0
- package/src/ui/components/FilePickerView.ts +161 -0
- package/src/ui/components/MultiLineInput.test.ts +27 -0
- package/src/ui/components/MultiLineInput.ts +233 -0
- package/src/ui/components/SessionChip.test.ts +69 -0
- package/src/ui/components/SessionChip.ts +481 -0
- package/src/ui/components/ToyboxSidebar.test.ts +36 -0
- package/src/ui/components/ToyboxSidebar.ts +329 -0
- package/src/ui/components/refactor_plan.md +35 -0
- package/src/ui/controllers/DashboardController.integration.test.ts +43 -0
- package/src/ui/controllers/DashboardController.ts +650 -0
- package/src/ui/dashboard.test.ts +43 -0
- package/src/ui/dashboard.ts +309 -0
- package/src/ui/dialogs/DashboardDialog.test.ts +146 -0
- package/src/ui/dialogs/DashboardDialog.ts +399 -0
- package/src/ui/dialogs/Dialog.test.ts +50 -0
- package/src/ui/dialogs/Dialog.ts +241 -0
- package/src/ui/dialogs/DialogSidebar.test.ts +60 -0
- package/src/ui/dialogs/DialogSidebar.ts +71 -0
- package/src/ui/dialogs/DiffViewDialog.test.ts +57 -0
- package/src/ui/dialogs/DiffViewDialog.ts +510 -0
- package/src/ui/dialogs/PRPreviewDialog.test.ts +50 -0
- package/src/ui/dialogs/PRPreviewDialog.ts +346 -0
- package/src/ui/dialogs/test-utils.ts +232 -0
- package/src/ui/file-picker-utils.test.ts +71 -0
- package/src/ui/file-picker-utils.ts +200 -0
- package/src/ui/input-chrome.test.ts +62 -0
- package/src/ui/input-chrome.ts +172 -0
- package/src/ui/logger.test.ts +68 -0
- package/src/ui/logger.ts +45 -0
- package/src/ui/mock-factory.ts +6 -0
- package/src/ui/spinner.test.ts +65 -0
- package/src/ui/spinner.ts +41 -0
- package/src/ui/test-setup.ts +300 -0
- package/src/ui/theme.test.ts +23 -0
- package/src/ui/theme.ts +16 -0
- package/src/ui/views/LandingView.integration.test.ts +21 -0
- package/src/ui/views/LandingView.test.ts +24 -0
- package/src/ui/views/LandingView.ts +221 -0
- package/src/ui/views/LogView.test.ts +24 -0
- package/src/ui/views/LogView.ts +277 -0
- package/src/ui/views/ToyboxView.test.ts +46 -0
- package/src/ui/views/ToyboxView.ts +323 -0
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +100 -0
- package/src/utils/index.test.ts +68 -0
- package/src/utils/index.ts +95 -0
- package/src/utils/persona.test.ts +12 -0
- package/src/utils/persona.ts +8 -0
- package/src/utils/project-root.test.ts +38 -0
- package/src/utils/project-root.ts +22 -0
- package/src/utils/resources.test.ts +64 -0
- package/src/utils/resources.ts +92 -0
- package/src/utils/search.test.ts +48 -0
- package/src/utils/search.ts +103 -0
- package/src/utils/session-tracker.test.ts +46 -0
- package/src/utils/session-tracker.ts +67 -0
- package/src/utils/spinner.test.ts +54 -0
- package/src/utils/spinner.ts +87 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mock, expect, test, describe, beforeEach } from "bun:test";
|
|
2
|
+
import { createMockRenderer, createMockSession, type MockRenderer } from "./test-utils.ts";
|
|
3
|
+
import type { CliRenderer } from "@opentui/core";
|
|
4
|
+
|
|
5
|
+
const mockDashboardDialog = {
|
|
6
|
+
update: mock(() => {}),
|
|
7
|
+
show: mock(() => {}),
|
|
8
|
+
hide: mock(() => {}),
|
|
9
|
+
isOpen: mock(() => false),
|
|
10
|
+
root: { visible: false, add: mock(() => {}) },
|
|
11
|
+
destroy: mock(() => {}),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
mock.module("./DashboardDialog.js", () => ({
|
|
15
|
+
DashboardDialog: class {
|
|
16
|
+
constructor() { return mockDashboardDialog; }
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe("DialogSidebar", () => {
|
|
21
|
+
let mockRenderer: MockRenderer;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mockRenderer = createMockRenderer();
|
|
25
|
+
mockDashboardDialog.isOpen.mockReturnValue(false);
|
|
26
|
+
// Clear mocks
|
|
27
|
+
mockDashboardDialog.update.mockClear();
|
|
28
|
+
mockDashboardDialog.show.mockClear();
|
|
29
|
+
mockDashboardDialog.hide.mockClear();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("should delegate to DashboardDialog", async () => {
|
|
33
|
+
const { DialogSidebar } = await import("./DialogSidebar.ts");
|
|
34
|
+
const ds = new DialogSidebar(mockRenderer as unknown as CliRenderer);
|
|
35
|
+
|
|
36
|
+
const mockSession = createMockSession({ id: "test" });
|
|
37
|
+
ds.update(mockSession);
|
|
38
|
+
|
|
39
|
+
expect(mockDashboardDialog.update).toHaveBeenCalledWith(mockSession);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should handle show/hide", async () => {
|
|
43
|
+
const { DialogSidebar } = await import("./DialogSidebar.ts");
|
|
44
|
+
const ds = new DialogSidebar(mockRenderer as unknown as CliRenderer);
|
|
45
|
+
|
|
46
|
+
ds.show();
|
|
47
|
+
expect(mockDashboardDialog.show).toHaveBeenCalled();
|
|
48
|
+
|
|
49
|
+
ds.hide();
|
|
50
|
+
expect(mockDashboardDialog.hide).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("should check isOpen", async () => {
|
|
54
|
+
const { DialogSidebar } = await import("./DialogSidebar.ts");
|
|
55
|
+
const ds = new DialogSidebar(mockRenderer as unknown as CliRenderer);
|
|
56
|
+
|
|
57
|
+
mockDashboardDialog.isOpen.mockReturnValue(true);
|
|
58
|
+
expect(ds.isOpen()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DashboardDialog } from "./DashboardDialog.js";
|
|
2
|
+
import { SessionData } from "../../types/tasks.js";
|
|
3
|
+
import { CliRenderer } from "@opentui/core";
|
|
4
|
+
|
|
5
|
+
export class DialogSidebar {
|
|
6
|
+
private dashboardDialog: DashboardDialog;
|
|
7
|
+
private useDialog = true;
|
|
8
|
+
|
|
9
|
+
constructor(renderer: CliRenderer) {
|
|
10
|
+
this.dashboardDialog = new DashboardDialog(renderer);
|
|
11
|
+
|
|
12
|
+
// Add the dialog to the renderer root
|
|
13
|
+
renderer.root.add(this.dashboardDialog.root);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public setUseDialog(use: boolean) {
|
|
17
|
+
this.useDialog = use;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public update(session: SessionData, silent: boolean = false) {
|
|
21
|
+
this.dashboardDialog.update(session);
|
|
22
|
+
if (!silent && !this.dashboardDialog.isOpen()) {
|
|
23
|
+
this.dashboardDialog.show();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public show() {
|
|
28
|
+
this.dashboardDialog.show();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public hide() {
|
|
32
|
+
this.dashboardDialog.hide();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public isOpen(): boolean {
|
|
36
|
+
return this.dashboardDialog.isOpen();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public showInput(placeholder?: string) {
|
|
40
|
+
// Dialog doesn't support input
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public hideInput() {
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public focusInput() {
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public get onHide() {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public set onHide(callback: (() => void) | undefined) {
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public get input() {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public get root() {
|
|
61
|
+
return this.dashboardDialog.root;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public get dialogComponent() {
|
|
65
|
+
return this.dashboardDialog;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public destroy() {
|
|
69
|
+
this.dashboardDialog.destroy();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mock, expect, test, describe, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
+
import { createMockRenderer } from "../mock-factory.ts";
|
|
3
|
+
import * as git from "../../services/git/index.js";
|
|
4
|
+
import { DiffViewDialog } from "./DiffViewDialog.ts";
|
|
5
|
+
|
|
6
|
+
describe("DiffViewDialog", () => {
|
|
7
|
+
let mockRenderer: any;
|
|
8
|
+
let events: any;
|
|
9
|
+
let spies: any[] = [];
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockRenderer = createMockRenderer();
|
|
13
|
+
events = {
|
|
14
|
+
onMerge: mock(async () => {}),
|
|
15
|
+
onCreatePR: mock(async () => {}),
|
|
16
|
+
onReject: mock(async () => {}),
|
|
17
|
+
onClose: mock(() => {}),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Use spyOn instead of global mock.module to avoid polluting other tests
|
|
21
|
+
spies = [
|
|
22
|
+
spyOn(git, "getChangedFiles").mockImplementation(async () => [
|
|
23
|
+
{ path: "file1.ts", status: "modified", additions: 10, deletions: 5 },
|
|
24
|
+
]),
|
|
25
|
+
spyOn(git, "getFileDiff").mockResolvedValue("diff"),
|
|
26
|
+
spyOn(git, "getStatusIndicator").mockReturnValue("M"),
|
|
27
|
+
spyOn(git, "getStatusColor").mockReturnValue("#ffffff"),
|
|
28
|
+
spyOn(git, "getFileType").mockReturnValue("typescript"),
|
|
29
|
+
];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
spies.forEach(spy => spy.mockRestore());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("should initialize and setup UI", () => {
|
|
37
|
+
const dialog = new DiffViewDialog(mockRenderer, events);
|
|
38
|
+
expect(dialog).toBeDefined();
|
|
39
|
+
expect(dialog.isOpen()).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should show and load changed files", async () => {
|
|
43
|
+
const dialog = new DiffViewDialog(mockRenderer, events);
|
|
44
|
+
|
|
45
|
+
const mockSession = {
|
|
46
|
+
worktreeInfo: {
|
|
47
|
+
worktreeDir: "/tmp/wt",
|
|
48
|
+
branchName: "feature",
|
|
49
|
+
baseBranch: "main",
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await dialog.show(mockSession as any);
|
|
54
|
+
expect(dialog.isOpen()).toBe(true);
|
|
55
|
+
expect(git.getChangedFiles).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
TextRenderable,
|
|
4
|
+
TextAttributes,
|
|
5
|
+
ScrollBoxRenderable,
|
|
6
|
+
CliRenderer,
|
|
7
|
+
KeyEvent,
|
|
8
|
+
DiffRenderable,
|
|
9
|
+
SelectRenderable,
|
|
10
|
+
SelectRenderableEvents,
|
|
11
|
+
type SelectOption,
|
|
12
|
+
parseColor,
|
|
13
|
+
SyntaxStyle,
|
|
14
|
+
} from "@opentui/core";
|
|
15
|
+
import { THEME } from "../theme.js";
|
|
16
|
+
import type { SessionData, WorktreeInfo } from "../../types/tasks.js";
|
|
17
|
+
import {
|
|
18
|
+
getChangedFiles,
|
|
19
|
+
getFileDiff,
|
|
20
|
+
getFileType,
|
|
21
|
+
getStatusIndicator,
|
|
22
|
+
getStatusColor,
|
|
23
|
+
type ChangedFile,
|
|
24
|
+
} from "../../services/git/index.js";
|
|
25
|
+
|
|
26
|
+
export interface DiffViewDialogEvents {
|
|
27
|
+
onMerge: (session: SessionData) => Promise<void>;
|
|
28
|
+
onCreatePR: (session: SessionData) => Promise<void>;
|
|
29
|
+
onReject: (session: SessionData) => Promise<void>;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class DiffViewDialog {
|
|
34
|
+
public root: BoxRenderable;
|
|
35
|
+
private renderer: CliRenderer;
|
|
36
|
+
private isVisible = false;
|
|
37
|
+
private session: SessionData | null = null;
|
|
38
|
+
private events: DiffViewDialogEvents;
|
|
39
|
+
private keyHandler: ((key: KeyEvent) => void) | null = null;
|
|
40
|
+
|
|
41
|
+
// UI Elements
|
|
42
|
+
private overlay: BoxRenderable;
|
|
43
|
+
private mainPanel: BoxRenderable;
|
|
44
|
+
private headerBar: BoxRenderable;
|
|
45
|
+
private titleText: TextRenderable;
|
|
46
|
+
private leftPanel: BoxRenderable;
|
|
47
|
+
private rightPanel: BoxRenderable;
|
|
48
|
+
private footerBar: BoxRenderable;
|
|
49
|
+
private fileSelect: SelectRenderable;
|
|
50
|
+
private diffContainer: ScrollBoxRenderable;
|
|
51
|
+
private diffView: DiffRenderable | null = null;
|
|
52
|
+
private noDiffText: TextRenderable;
|
|
53
|
+
private syntaxStyle: SyntaxStyle;
|
|
54
|
+
|
|
55
|
+
// State
|
|
56
|
+
private changedFiles: ChangedFile[] = [];
|
|
57
|
+
private currentFileIndex = 0;
|
|
58
|
+
private viewMode: "unified" | "split" = "unified";
|
|
59
|
+
private wrapMode: "none" | "word" = "none";
|
|
60
|
+
|
|
61
|
+
constructor(renderer: CliRenderer, events: DiffViewDialogEvents) {
|
|
62
|
+
this.renderer = renderer;
|
|
63
|
+
this.events = events;
|
|
64
|
+
|
|
65
|
+
// Create syntax style for diff view
|
|
66
|
+
this.syntaxStyle = SyntaxStyle.fromStyles({
|
|
67
|
+
keyword: { fg: parseColor("#FF7B72"), bold: true },
|
|
68
|
+
"keyword.import": { fg: parseColor("#FF7B72"), bold: true },
|
|
69
|
+
string: { fg: parseColor("#A5D6FF") },
|
|
70
|
+
comment: { fg: parseColor("#8B949E"), italic: true },
|
|
71
|
+
number: { fg: parseColor("#79C0FF") },
|
|
72
|
+
boolean: { fg: parseColor("#79C0FF") },
|
|
73
|
+
constant: { fg: parseColor("#79C0FF") },
|
|
74
|
+
function: { fg: parseColor("#D2A8FF") },
|
|
75
|
+
"function.call": { fg: parseColor("#D2A8FF") },
|
|
76
|
+
constructor: { fg: parseColor("#FFA657") },
|
|
77
|
+
type: { fg: parseColor("#FFA657") },
|
|
78
|
+
operator: { fg: parseColor("#FF7B72") },
|
|
79
|
+
variable: { fg: parseColor("#E6EDF3") },
|
|
80
|
+
property: { fg: parseColor("#79C0FF") },
|
|
81
|
+
bracket: { fg: parseColor("#F0F6FC") },
|
|
82
|
+
punctuation: { fg: parseColor("#F0F6FC") },
|
|
83
|
+
default: { fg: parseColor("#E6EDF3") },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Background overlay
|
|
87
|
+
this.overlay = new BoxRenderable(renderer, {
|
|
88
|
+
id: "diff-view-overlay",
|
|
89
|
+
width: "100%",
|
|
90
|
+
height: "100%",
|
|
91
|
+
position: "absolute",
|
|
92
|
+
left: 0,
|
|
93
|
+
top: 0,
|
|
94
|
+
backgroundColor: THEME.bg,
|
|
95
|
+
visible: false,
|
|
96
|
+
zIndex: 30000,
|
|
97
|
+
flexDirection: "column",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Main panel
|
|
101
|
+
this.mainPanel = new BoxRenderable(renderer, {
|
|
102
|
+
id: "diff-view-main",
|
|
103
|
+
width: "100%",
|
|
104
|
+
height: "100%",
|
|
105
|
+
flexDirection: "column",
|
|
106
|
+
});
|
|
107
|
+
this.overlay.add(this.mainPanel);
|
|
108
|
+
|
|
109
|
+
// Header bar
|
|
110
|
+
this.headerBar = new BoxRenderable(renderer, {
|
|
111
|
+
id: "diff-view-header",
|
|
112
|
+
width: "100%",
|
|
113
|
+
height: 3,
|
|
114
|
+
flexDirection: "row",
|
|
115
|
+
alignItems: "center",
|
|
116
|
+
paddingLeft: 2,
|
|
117
|
+
paddingRight: 2,
|
|
118
|
+
backgroundColor: THEME.surface,
|
|
119
|
+
borderColor: THEME.accent,
|
|
120
|
+
border: ["bottom"],
|
|
121
|
+
flexShrink: 0,
|
|
122
|
+
});
|
|
123
|
+
this.mainPanel.add(this.headerBar);
|
|
124
|
+
|
|
125
|
+
this.titleText = new TextRenderable(renderer, {
|
|
126
|
+
id: "diff-view-title",
|
|
127
|
+
content: "Review Changes",
|
|
128
|
+
fg: THEME.accent,
|
|
129
|
+
attributes: TextAttributes.BOLD,
|
|
130
|
+
});
|
|
131
|
+
this.headerBar.add(this.titleText);
|
|
132
|
+
|
|
133
|
+
// Content area (file picker + diff view)
|
|
134
|
+
const contentArea = new BoxRenderable(renderer, {
|
|
135
|
+
id: "diff-view-content",
|
|
136
|
+
width: "100%",
|
|
137
|
+
flexGrow: 1,
|
|
138
|
+
flexDirection: "row",
|
|
139
|
+
});
|
|
140
|
+
this.mainPanel.add(contentArea);
|
|
141
|
+
|
|
142
|
+
// Left panel - File picker (30% width)
|
|
143
|
+
this.leftPanel = new BoxRenderable(renderer, {
|
|
144
|
+
id: "diff-view-left",
|
|
145
|
+
width: "30%",
|
|
146
|
+
height: "100%",
|
|
147
|
+
flexDirection: "column",
|
|
148
|
+
backgroundColor: THEME.surface,
|
|
149
|
+
borderColor: THEME.darkAccent,
|
|
150
|
+
border: ["right"],
|
|
151
|
+
padding: 1,
|
|
152
|
+
});
|
|
153
|
+
contentArea.add(this.leftPanel);
|
|
154
|
+
|
|
155
|
+
const fileListHeader = new TextRenderable(renderer, {
|
|
156
|
+
id: "diff-view-file-header",
|
|
157
|
+
content: "Changed Files",
|
|
158
|
+
fg: THEME.white,
|
|
159
|
+
attributes: TextAttributes.BOLD,
|
|
160
|
+
marginBottom: 1,
|
|
161
|
+
});
|
|
162
|
+
this.leftPanel.add(fileListHeader);
|
|
163
|
+
|
|
164
|
+
this.fileSelect = new SelectRenderable(renderer, {
|
|
165
|
+
id: "diff-view-file-select",
|
|
166
|
+
width: "100%",
|
|
167
|
+
flexGrow: 1,
|
|
168
|
+
options: [],
|
|
169
|
+
backgroundColor: THEME.surface,
|
|
170
|
+
textColor: THEME.text,
|
|
171
|
+
selectedBackgroundColor: THEME.darkAccent,
|
|
172
|
+
selectedTextColor: THEME.accent,
|
|
173
|
+
focusedBackgroundColor: THEME.surface,
|
|
174
|
+
focusedTextColor: THEME.text,
|
|
175
|
+
descriptionColor: THEME.dim,
|
|
176
|
+
selectedDescriptionColor: THEME.dim,
|
|
177
|
+
showScrollIndicator: true,
|
|
178
|
+
wrapSelection: true,
|
|
179
|
+
showDescription: true,
|
|
180
|
+
});
|
|
181
|
+
this.leftPanel.add(this.fileSelect);
|
|
182
|
+
|
|
183
|
+
// Right panel - Diff view (70% width)
|
|
184
|
+
this.rightPanel = new BoxRenderable(renderer, {
|
|
185
|
+
id: "diff-view-right",
|
|
186
|
+
width: "70%",
|
|
187
|
+
height: "100%",
|
|
188
|
+
flexDirection: "column",
|
|
189
|
+
backgroundColor: THEME.bg,
|
|
190
|
+
padding: 1,
|
|
191
|
+
});
|
|
192
|
+
contentArea.add(this.rightPanel);
|
|
193
|
+
|
|
194
|
+
this.diffContainer = new ScrollBoxRenderable(renderer, {
|
|
195
|
+
id: "diff-view-scroll",
|
|
196
|
+
width: "100%",
|
|
197
|
+
flexGrow: 1,
|
|
198
|
+
scrollY: true,
|
|
199
|
+
scrollX: true,
|
|
200
|
+
backgroundColor: THEME.bg,
|
|
201
|
+
});
|
|
202
|
+
this.rightPanel.add(this.diffContainer);
|
|
203
|
+
|
|
204
|
+
this.noDiffText = new TextRenderable(renderer, {
|
|
205
|
+
id: "diff-view-no-diff",
|
|
206
|
+
content: "Select a file to view diff",
|
|
207
|
+
fg: THEME.dim,
|
|
208
|
+
});
|
|
209
|
+
this.diffContainer.add(this.noDiffText);
|
|
210
|
+
|
|
211
|
+
// Footer bar
|
|
212
|
+
this.footerBar = new BoxRenderable(renderer, {
|
|
213
|
+
id: "diff-view-footer",
|
|
214
|
+
width: "100%",
|
|
215
|
+
height: 3,
|
|
216
|
+
flexDirection: "row",
|
|
217
|
+
alignItems: "center",
|
|
218
|
+
justifyContent: "space-between",
|
|
219
|
+
paddingLeft: 2,
|
|
220
|
+
paddingRight: 2,
|
|
221
|
+
backgroundColor: THEME.surface,
|
|
222
|
+
borderColor: THEME.darkAccent,
|
|
223
|
+
border: ["top"],
|
|
224
|
+
flexShrink: 0,
|
|
225
|
+
});
|
|
226
|
+
this.mainPanel.add(this.footerBar);
|
|
227
|
+
|
|
228
|
+
const footerLeft = new TextRenderable(renderer, {
|
|
229
|
+
id: "diff-view-footer-left",
|
|
230
|
+
content: "[M] Merge | [P] Create PR | [R] Reject | [ESC] Cancel",
|
|
231
|
+
fg: THEME.dim,
|
|
232
|
+
});
|
|
233
|
+
this.footerBar.add(footerLeft);
|
|
234
|
+
|
|
235
|
+
const footerRight = new TextRenderable(renderer, {
|
|
236
|
+
id: "diff-view-footer-right",
|
|
237
|
+
content: "[V] View Mode | [W] Wrap | [j/k] Navigate",
|
|
238
|
+
fg: THEME.dim,
|
|
239
|
+
});
|
|
240
|
+
this.footerBar.add(footerRight);
|
|
241
|
+
|
|
242
|
+
this.root = this.overlay;
|
|
243
|
+
|
|
244
|
+
// Setup file selection event
|
|
245
|
+
this.fileSelect.on(
|
|
246
|
+
SelectRenderableEvents.SELECTION_CHANGED,
|
|
247
|
+
(index: number) => {
|
|
248
|
+
this.currentFileIndex = index;
|
|
249
|
+
this.loadFileDiff();
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
this.fileSelect.on(
|
|
254
|
+
SelectRenderableEvents.ITEM_SELECTED,
|
|
255
|
+
(index: number) => {
|
|
256
|
+
this.currentFileIndex = index;
|
|
257
|
+
this.loadFileDiff();
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private setupKeyboard() {
|
|
263
|
+
if (this.keyHandler) return;
|
|
264
|
+
|
|
265
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
266
|
+
if (!this.isVisible) return;
|
|
267
|
+
|
|
268
|
+
// File navigation
|
|
269
|
+
if (key.name === "up" || key.name === "k") {
|
|
270
|
+
this.fileSelect.moveUp();
|
|
271
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
272
|
+
this.fileSelect.moveDown();
|
|
273
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
274
|
+
this.fileSelect.selectCurrent();
|
|
275
|
+
}
|
|
276
|
+
// View mode toggle
|
|
277
|
+
else if (key.name === "v" && !key.ctrl && !key.meta) {
|
|
278
|
+
this.viewMode = this.viewMode === "unified" ? "split" : "unified";
|
|
279
|
+
if (this.diffView) {
|
|
280
|
+
this.diffView.view = this.viewMode;
|
|
281
|
+
}
|
|
282
|
+
this.renderer.requestRender();
|
|
283
|
+
}
|
|
284
|
+
// Wrap mode toggle
|
|
285
|
+
else if (key.name === "w" && !key.ctrl && !key.meta) {
|
|
286
|
+
this.wrapMode = this.wrapMode === "none" ? "word" : "none";
|
|
287
|
+
if (this.diffView) {
|
|
288
|
+
this.diffView.wrapMode = this.wrapMode;
|
|
289
|
+
}
|
|
290
|
+
this.renderer.requestRender();
|
|
291
|
+
}
|
|
292
|
+
// Merge action
|
|
293
|
+
else if (key.name === "m" && !key.ctrl && !key.meta) {
|
|
294
|
+
if (this.session) {
|
|
295
|
+
this.events.onMerge(this.session);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Reject / cleanup worktree
|
|
299
|
+
else if (key.name === "r" && !key.ctrl && !key.meta) {
|
|
300
|
+
if (this.session) {
|
|
301
|
+
this.events.onReject(this.session);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Create PR action
|
|
305
|
+
else if (key.name === "p" && !key.ctrl && !key.meta) {
|
|
306
|
+
if (this.session) {
|
|
307
|
+
this.events.onCreatePR(this.session);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Close
|
|
311
|
+
else if (key.name === "escape") {
|
|
312
|
+
this.hide();
|
|
313
|
+
this.events.onClose();
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
this.renderer.keyInput.on("keypress", this.keyHandler);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private cleanupKeyboard() {
|
|
321
|
+
if (this.keyHandler) {
|
|
322
|
+
this.renderer.keyInput.off("keypress", this.keyHandler);
|
|
323
|
+
this.keyHandler = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async loadChangedFiles() {
|
|
328
|
+
if (!this.session?.worktreeInfo) return;
|
|
329
|
+
|
|
330
|
+
const { worktreeDir, baseBranch } = this.session.worktreeInfo;
|
|
331
|
+
|
|
332
|
+
const disallowedExtensions = new Set([
|
|
333
|
+
"png",
|
|
334
|
+
"jpg",
|
|
335
|
+
"jpeg",
|
|
336
|
+
"gif",
|
|
337
|
+
"webp",
|
|
338
|
+
"bmp",
|
|
339
|
+
"tiff",
|
|
340
|
+
"ico",
|
|
341
|
+
"svg",
|
|
342
|
+
"pdf",
|
|
343
|
+
"zip",
|
|
344
|
+
"tar",
|
|
345
|
+
"gz",
|
|
346
|
+
"tgz",
|
|
347
|
+
"bz2",
|
|
348
|
+
"xz",
|
|
349
|
+
"7z",
|
|
350
|
+
"mov",
|
|
351
|
+
"mp4",
|
|
352
|
+
"mkv",
|
|
353
|
+
"avi",
|
|
354
|
+
"mp3",
|
|
355
|
+
"wav",
|
|
356
|
+
"ogg",
|
|
357
|
+
"flac",
|
|
358
|
+
"woff",
|
|
359
|
+
"woff2",
|
|
360
|
+
"ttf",
|
|
361
|
+
"otf",
|
|
362
|
+
"psd",
|
|
363
|
+
"ai",
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
const isRenderableFile = (path: string): boolean => {
|
|
367
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
368
|
+
return !disallowedExtensions.has(ext);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const allFiles = await getChangedFiles(worktreeDir, baseBranch);
|
|
373
|
+
this.changedFiles = allFiles.filter((file) => isRenderableFile(file.path));
|
|
374
|
+
|
|
375
|
+
const options: SelectOption[] = this.changedFiles.map((file) => {
|
|
376
|
+
const indicator = getStatusIndicator(file.status);
|
|
377
|
+
const stats =
|
|
378
|
+
file.additions > 0 || file.deletions > 0
|
|
379
|
+
? `+${file.additions} -${file.deletions}`
|
|
380
|
+
: "";
|
|
381
|
+
return {
|
|
382
|
+
name: `${indicator} ${file.path}`,
|
|
383
|
+
description: stats,
|
|
384
|
+
value: file.path,
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
this.fileSelect.options = options;
|
|
389
|
+
|
|
390
|
+
if (this.changedFiles.length > 0) {
|
|
391
|
+
this.currentFileIndex = 0;
|
|
392
|
+
this.fileSelect.setSelectedIndex(0);
|
|
393
|
+
await this.loadFileDiff();
|
|
394
|
+
} else {
|
|
395
|
+
this.noDiffText.content = "No renderable text changes (images/binaries hidden)";
|
|
396
|
+
this.noDiffText.visible = true;
|
|
397
|
+
this.diffView = null;
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error("Failed to load changed files:", error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private async loadFileDiff() {
|
|
405
|
+
if (!this.session?.worktreeInfo || this.changedFiles.length === 0) return;
|
|
406
|
+
|
|
407
|
+
const { worktreeDir, baseBranch } = this.session.worktreeInfo;
|
|
408
|
+
const file = this.changedFiles[this.currentFileIndex];
|
|
409
|
+
|
|
410
|
+
if (!file) return;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const diff = await getFileDiff(worktreeDir, baseBranch, file.path, file.status);
|
|
414
|
+
const filetype = getFileType(file.path);
|
|
415
|
+
const syntaxStyle = this.syntaxStyle ?? SyntaxStyle.fromStyles({});
|
|
416
|
+
|
|
417
|
+
// Remove the "no diff" text
|
|
418
|
+
this.noDiffText.visible = false;
|
|
419
|
+
|
|
420
|
+
// Create or update diff view
|
|
421
|
+
if (!this.diffView) {
|
|
422
|
+
this.diffView = new DiffRenderable(this.renderer, {
|
|
423
|
+
id: "diff-view-renderable",
|
|
424
|
+
diff,
|
|
425
|
+
view: this.viewMode,
|
|
426
|
+
filetype,
|
|
427
|
+
syntaxStyle,
|
|
428
|
+
showLineNumbers: true,
|
|
429
|
+
wrapMode: this.wrapMode,
|
|
430
|
+
addedBg: "#1a4d1a",
|
|
431
|
+
removedBg: "#4d1a1a",
|
|
432
|
+
contextBg: "transparent",
|
|
433
|
+
addedSignColor: "#22c55e",
|
|
434
|
+
removedSignColor: "#ef4444",
|
|
435
|
+
lineNumberFg: "#6b7280",
|
|
436
|
+
lineNumberBg: "#161b22",
|
|
437
|
+
addedLineNumberBg: "#0d3a0d",
|
|
438
|
+
removedLineNumberBg: "#3a0d0d",
|
|
439
|
+
width: "100%",
|
|
440
|
+
flexGrow: 1,
|
|
441
|
+
});
|
|
442
|
+
this.diffContainer.add(this.diffView);
|
|
443
|
+
} else {
|
|
444
|
+
this.diffView.diff = diff;
|
|
445
|
+
this.diffView.filetype = filetype;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.renderer.requestRender();
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error("Failed to load file diff:", error);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public async show(session: SessionData) {
|
|
455
|
+
if (this.isVisible) return;
|
|
456
|
+
if (!session.worktreeInfo) return;
|
|
457
|
+
|
|
458
|
+
this.session = session;
|
|
459
|
+
this.isVisible = true;
|
|
460
|
+
this.overlay.visible = true;
|
|
461
|
+
|
|
462
|
+
// Update title
|
|
463
|
+
const { branchName, baseBranch } = session.worktreeInfo;
|
|
464
|
+
this.titleText.content = `Review Changes: ${branchName} -> ${baseBranch}`;
|
|
465
|
+
|
|
466
|
+
// Load changed files
|
|
467
|
+
await this.loadChangedFiles();
|
|
468
|
+
|
|
469
|
+
// Setup keyboard
|
|
470
|
+
this.setupKeyboard();
|
|
471
|
+
|
|
472
|
+
this.renderer.requestRender();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
public hide() {
|
|
476
|
+
if (!this.isVisible) return;
|
|
477
|
+
|
|
478
|
+
this.isVisible = false;
|
|
479
|
+
this.overlay.visible = false;
|
|
480
|
+
this.cleanupKeyboard();
|
|
481
|
+
|
|
482
|
+
// Reset state
|
|
483
|
+
this.changedFiles = [];
|
|
484
|
+
this.currentFileIndex = 0;
|
|
485
|
+
if (this.diffView) {
|
|
486
|
+
this.diffContainer.remove(this.diffView.id);
|
|
487
|
+
this.diffView = null;
|
|
488
|
+
}
|
|
489
|
+
this.noDiffText.visible = true;
|
|
490
|
+
this.fileSelect.options = [];
|
|
491
|
+
|
|
492
|
+
this.renderer.requestRender();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
public isOpen(): boolean {
|
|
496
|
+
return this.isVisible;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
public getSession(): SessionData | null {
|
|
500
|
+
return this.session;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
public destroy() {
|
|
504
|
+
this.cleanupKeyboard();
|
|
505
|
+
if (this.diffView) {
|
|
506
|
+
this.diffView.destroy();
|
|
507
|
+
}
|
|
508
|
+
this.syntaxStyle.destroy();
|
|
509
|
+
}
|
|
510
|
+
}
|