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.
Files changed (128) hide show
  1. package/README.md +242 -0
  2. package/bin.js +3 -0
  3. package/dist/pickle +0 -0
  4. package/dist/worker-executor.js +207 -0
  5. package/package.json +53 -0
  6. package/src/games/GameSidebarManager.test.ts +64 -0
  7. package/src/games/GameSidebarManager.ts +78 -0
  8. package/src/games/gameboy/GameboyView.test.ts +25 -0
  9. package/src/games/gameboy/GameboyView.ts +100 -0
  10. package/src/games/gameboy/gameboy-polyfills.ts +313 -0
  11. package/src/games/index.test.ts +9 -0
  12. package/src/games/index.ts +4 -0
  13. package/src/games/snake/SnakeGame.test.ts +35 -0
  14. package/src/games/snake/SnakeGame.ts +145 -0
  15. package/src/games/snake/SnakeView.test.ts +25 -0
  16. package/src/games/snake/SnakeView.ts +290 -0
  17. package/src/index.test.ts +24 -0
  18. package/src/index.ts +141 -0
  19. package/src/services/commands/worker.test.ts +14 -0
  20. package/src/services/commands/worker.ts +262 -0
  21. package/src/services/config/index.ts +2 -0
  22. package/src/services/config/settings.test.ts +42 -0
  23. package/src/services/config/settings.ts +220 -0
  24. package/src/services/config/state.test.ts +88 -0
  25. package/src/services/config/state.ts +130 -0
  26. package/src/services/config/types.ts +39 -0
  27. package/src/services/execution/index.ts +1 -0
  28. package/src/services/execution/pickle-source.test.ts +88 -0
  29. package/src/services/execution/pickle-source.ts +264 -0
  30. package/src/services/execution/prompt.test.ts +93 -0
  31. package/src/services/execution/prompt.ts +322 -0
  32. package/src/services/execution/sequential.test.ts +91 -0
  33. package/src/services/execution/sequential.ts +422 -0
  34. package/src/services/execution/worker-client.ts +94 -0
  35. package/src/services/execution/worker-executor.ts +41 -0
  36. package/src/services/execution/worker.test.ts +73 -0
  37. package/src/services/git/branch.test.ts +147 -0
  38. package/src/services/git/branch.ts +128 -0
  39. package/src/services/git/diff.test.ts +113 -0
  40. package/src/services/git/diff.ts +323 -0
  41. package/src/services/git/index.ts +4 -0
  42. package/src/services/git/pr.test.ts +104 -0
  43. package/src/services/git/pr.ts +192 -0
  44. package/src/services/git/worktree.test.ts +99 -0
  45. package/src/services/git/worktree.ts +141 -0
  46. package/src/services/providers/base.test.ts +86 -0
  47. package/src/services/providers/base.ts +438 -0
  48. package/src/services/providers/codex.test.ts +39 -0
  49. package/src/services/providers/codex.ts +208 -0
  50. package/src/services/providers/gemini.test.ts +40 -0
  51. package/src/services/providers/gemini.ts +169 -0
  52. package/src/services/providers/index.test.ts +28 -0
  53. package/src/services/providers/index.ts +41 -0
  54. package/src/services/providers/opencode.test.ts +64 -0
  55. package/src/services/providers/opencode.ts +228 -0
  56. package/src/services/providers/types.ts +44 -0
  57. package/src/skills/code-implementer.md +105 -0
  58. package/src/skills/code-researcher.md +78 -0
  59. package/src/skills/implementation-planner.md +105 -0
  60. package/src/skills/plan-reviewer.md +100 -0
  61. package/src/skills/prd-drafter.md +123 -0
  62. package/src/skills/research-reviewer.md +79 -0
  63. package/src/skills/ruthless-refactorer.md +52 -0
  64. package/src/skills/ticket-manager.md +135 -0
  65. package/src/types/index.ts +2 -0
  66. package/src/types/rpc.ts +14 -0
  67. package/src/types/tasks.ts +50 -0
  68. package/src/types.d.ts +9 -0
  69. package/src/ui/common.ts +28 -0
  70. package/src/ui/components/FilePickerView.test.ts +79 -0
  71. package/src/ui/components/FilePickerView.ts +161 -0
  72. package/src/ui/components/MultiLineInput.test.ts +27 -0
  73. package/src/ui/components/MultiLineInput.ts +233 -0
  74. package/src/ui/components/SessionChip.test.ts +69 -0
  75. package/src/ui/components/SessionChip.ts +481 -0
  76. package/src/ui/components/ToyboxSidebar.test.ts +36 -0
  77. package/src/ui/components/ToyboxSidebar.ts +329 -0
  78. package/src/ui/components/refactor_plan.md +35 -0
  79. package/src/ui/controllers/DashboardController.integration.test.ts +43 -0
  80. package/src/ui/controllers/DashboardController.ts +650 -0
  81. package/src/ui/dashboard.test.ts +43 -0
  82. package/src/ui/dashboard.ts +309 -0
  83. package/src/ui/dialogs/DashboardDialog.test.ts +146 -0
  84. package/src/ui/dialogs/DashboardDialog.ts +399 -0
  85. package/src/ui/dialogs/Dialog.test.ts +50 -0
  86. package/src/ui/dialogs/Dialog.ts +241 -0
  87. package/src/ui/dialogs/DialogSidebar.test.ts +60 -0
  88. package/src/ui/dialogs/DialogSidebar.ts +71 -0
  89. package/src/ui/dialogs/DiffViewDialog.test.ts +57 -0
  90. package/src/ui/dialogs/DiffViewDialog.ts +510 -0
  91. package/src/ui/dialogs/PRPreviewDialog.test.ts +50 -0
  92. package/src/ui/dialogs/PRPreviewDialog.ts +346 -0
  93. package/src/ui/dialogs/test-utils.ts +232 -0
  94. package/src/ui/file-picker-utils.test.ts +71 -0
  95. package/src/ui/file-picker-utils.ts +200 -0
  96. package/src/ui/input-chrome.test.ts +62 -0
  97. package/src/ui/input-chrome.ts +172 -0
  98. package/src/ui/logger.test.ts +68 -0
  99. package/src/ui/logger.ts +45 -0
  100. package/src/ui/mock-factory.ts +6 -0
  101. package/src/ui/spinner.test.ts +65 -0
  102. package/src/ui/spinner.ts +41 -0
  103. package/src/ui/test-setup.ts +300 -0
  104. package/src/ui/theme.test.ts +23 -0
  105. package/src/ui/theme.ts +16 -0
  106. package/src/ui/views/LandingView.integration.test.ts +21 -0
  107. package/src/ui/views/LandingView.test.ts +24 -0
  108. package/src/ui/views/LandingView.ts +221 -0
  109. package/src/ui/views/LogView.test.ts +24 -0
  110. package/src/ui/views/LogView.ts +277 -0
  111. package/src/ui/views/ToyboxView.test.ts +46 -0
  112. package/src/ui/views/ToyboxView.ts +323 -0
  113. package/src/utils/clipboard.test.ts +86 -0
  114. package/src/utils/clipboard.ts +100 -0
  115. package/src/utils/index.test.ts +68 -0
  116. package/src/utils/index.ts +95 -0
  117. package/src/utils/persona.test.ts +12 -0
  118. package/src/utils/persona.ts +8 -0
  119. package/src/utils/project-root.test.ts +38 -0
  120. package/src/utils/project-root.ts +22 -0
  121. package/src/utils/resources.test.ts +64 -0
  122. package/src/utils/resources.ts +92 -0
  123. package/src/utils/search.test.ts +48 -0
  124. package/src/utils/search.ts +103 -0
  125. package/src/utils/session-tracker.test.ts +46 -0
  126. package/src/utils/session-tracker.ts +67 -0
  127. package/src/utils/spinner.test.ts +54 -0
  128. package/src/utils/spinner.ts +87 -0
@@ -0,0 +1,50 @@
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 { PRPreviewDialog } from "./PRPreviewDialog.ts";
5
+
6
+ describe("PRPreviewDialog", () => {
7
+ let mockRenderer: any;
8
+ let events: any;
9
+ let spies: any[] = [];
10
+
11
+ beforeEach(() => {
12
+ mockRenderer = createMockRenderer();
13
+ events = {
14
+ onConfirm: mock(async () => {}),
15
+ onCancel: mock(() => {}),
16
+ };
17
+
18
+ // Use spyOn instead of global mock.module to avoid polluting other tests
19
+ spies = [
20
+ spyOn(git, "generatePRDescription").mockResolvedValue({ title: "Test Title", body: "Test Body" }),
21
+ ];
22
+ });
23
+
24
+ afterEach(() => {
25
+ spies.forEach(spy => spy.mockRestore());
26
+ });
27
+
28
+ test("should initialize and setup UI", () => {
29
+ const dialog = new PRPreviewDialog(mockRenderer, events);
30
+ expect(dialog).toBeDefined();
31
+ expect(dialog.isOpen()).toBe(false);
32
+ });
33
+
34
+ test("should show and load PR description", async () => {
35
+ const dialog = new PRPreviewDialog(mockRenderer, events);
36
+
37
+ const mockSession = {
38
+ id: "test-session",
39
+ worktreeInfo: {
40
+ branchName: "feature",
41
+ baseBranch: "main",
42
+ }
43
+ };
44
+
45
+ await dialog.show(mockSession as any);
46
+ expect(dialog.isOpen()).toBe(true);
47
+ // titleInput.value should be set to the title from generatePRDescription
48
+ expect((dialog as any).titleInput.value).toBe("Test Title");
49
+ });
50
+ });
@@ -0,0 +1,346 @@
1
+ import {
2
+ BoxRenderable,
3
+ TextRenderable,
4
+ TextAttributes,
5
+ ScrollBoxRenderable,
6
+ CliRenderer,
7
+ KeyEvent,
8
+ InputRenderable,
9
+ MarkdownRenderable,
10
+ SyntaxStyle,
11
+ } from "@opentui/core";
12
+ import { THEME } from "../theme.js";
13
+ import type { SessionData } from "../../types/tasks.js";
14
+ import { generatePRDescription } from "../../services/git/index.js";
15
+
16
+ export interface PRPreviewDialogEvents {
17
+ onConfirm: (session: SessionData, title: string, body: string) => Promise<void>;
18
+ onCancel: () => void;
19
+ }
20
+
21
+ export class PRPreviewDialog {
22
+ public root: BoxRenderable;
23
+ private renderer: CliRenderer;
24
+ private isVisible = false;
25
+ private session: SessionData | null = null;
26
+ private events: PRPreviewDialogEvents;
27
+ private keyHandler: ((key: KeyEvent) => void) | null = null;
28
+
29
+ // UI Elements
30
+ private overlay: BoxRenderable;
31
+ private mainPanel: BoxRenderable;
32
+ private headerBar: BoxRenderable;
33
+ private titleText: TextRenderable;
34
+ private contentArea: BoxRenderable;
35
+ private titleInput: InputRenderable;
36
+ private bodyScrollContainer: ScrollBoxRenderable;
37
+ private bodyPreview: MarkdownRenderable;
38
+ private footerBar: BoxRenderable;
39
+ private statusText: TextRenderable;
40
+
41
+ // State
42
+ private prTitle: string = "";
43
+ private prBody: string = "";
44
+ private isEditingTitle = false;
45
+ private isLoading = false;
46
+
47
+ constructor(renderer: CliRenderer, events: PRPreviewDialogEvents) {
48
+ this.renderer = renderer;
49
+ this.events = events;
50
+
51
+ // Background overlay
52
+ this.overlay = new BoxRenderable(renderer, {
53
+ id: "pr-preview-overlay",
54
+ width: "100%",
55
+ height: "100%",
56
+ position: "absolute",
57
+ left: 0,
58
+ top: 0,
59
+ backgroundColor: THEME.bg,
60
+ visible: false,
61
+ zIndex: 31000,
62
+ flexDirection: "column",
63
+ });
64
+
65
+ // Main panel
66
+ this.mainPanel = new BoxRenderable(renderer, {
67
+ id: "pr-preview-main",
68
+ width: "100%",
69
+ height: "100%",
70
+ flexDirection: "column",
71
+ padding: 2,
72
+ });
73
+ this.overlay.add(this.mainPanel);
74
+
75
+ // Header bar
76
+ this.headerBar = new BoxRenderable(renderer, {
77
+ id: "pr-preview-header",
78
+ width: "100%",
79
+ height: 3,
80
+ flexDirection: "row",
81
+ alignItems: "center",
82
+ marginBottom: 1,
83
+ flexShrink: 0,
84
+ });
85
+ this.mainPanel.add(this.headerBar);
86
+
87
+ this.titleText = new TextRenderable(renderer, {
88
+ id: "pr-preview-title",
89
+ content: "Create Pull Request",
90
+ fg: THEME.accent,
91
+ attributes: TextAttributes.BOLD,
92
+ });
93
+ this.headerBar.add(this.titleText);
94
+
95
+ // Content area
96
+ this.contentArea = new BoxRenderable(renderer, {
97
+ id: "pr-preview-content",
98
+ width: "100%",
99
+ flexGrow: 1,
100
+ flexDirection: "column",
101
+ });
102
+ this.mainPanel.add(this.contentArea);
103
+
104
+ // Title section
105
+ const titleSection = new BoxRenderable(renderer, {
106
+ id: "pr-preview-title-section",
107
+ width: "100%",
108
+ flexDirection: "column",
109
+ marginBottom: 2,
110
+ flexShrink: 0,
111
+ });
112
+ this.contentArea.add(titleSection);
113
+
114
+ const titleLabel = new TextRenderable(renderer, {
115
+ id: "pr-preview-title-label",
116
+ content: "Title:",
117
+ fg: THEME.white,
118
+ attributes: TextAttributes.BOLD,
119
+ marginBottom: 1,
120
+ });
121
+ titleSection.add(titleLabel);
122
+
123
+ this.titleInput = new InputRenderable(renderer, {
124
+ id: "pr-preview-title-input",
125
+ width: "100%",
126
+ placeholder: "Enter PR title...",
127
+ backgroundColor: THEME.surface,
128
+ cursorColor: THEME.accent,
129
+ textColor: THEME.text,
130
+ placeholderColor: THEME.dim,
131
+ });
132
+ titleSection.add(this.titleInput);
133
+
134
+ // Body section
135
+ const bodySection = new BoxRenderable(renderer, {
136
+ id: "pr-preview-body-section",
137
+ width: "100%",
138
+ flexGrow: 1,
139
+ flexDirection: "column",
140
+ });
141
+ this.contentArea.add(bodySection);
142
+
143
+ const bodyLabel = new TextRenderable(renderer, {
144
+ id: "pr-preview-body-label",
145
+ content: "Description (Preview):",
146
+ fg: THEME.white,
147
+ attributes: TextAttributes.BOLD,
148
+ marginBottom: 1,
149
+ });
150
+ bodySection.add(bodyLabel);
151
+
152
+ this.bodyScrollContainer = new ScrollBoxRenderable(renderer, {
153
+ id: "pr-preview-body-scroll",
154
+ width: "100%",
155
+ flexGrow: 1,
156
+ scrollY: true,
157
+ backgroundColor: THEME.surface,
158
+ border: true,
159
+ borderColor: THEME.darkAccent,
160
+ contentOptions: {
161
+ padding: 1,
162
+ },
163
+ });
164
+ bodySection.add(this.bodyScrollContainer);
165
+
166
+ this.bodyPreview = new MarkdownRenderable(renderer, {
167
+ id: "pr-preview-body-content",
168
+ content: "",
169
+ width: "100%",
170
+ syntaxStyle: SyntaxStyle.fromStyles({}),
171
+ });
172
+ this.bodyScrollContainer.add(this.bodyPreview);
173
+
174
+ // Footer bar
175
+ this.footerBar = new BoxRenderable(renderer, {
176
+ id: "pr-preview-footer",
177
+ width: "100%",
178
+ height: 3,
179
+ flexDirection: "row",
180
+ alignItems: "center",
181
+ justifyContent: "space-between",
182
+ marginTop: 1,
183
+ flexShrink: 0,
184
+ });
185
+ this.mainPanel.add(this.footerBar);
186
+
187
+ const footerLeft = new TextRenderable(renderer, {
188
+ id: "pr-preview-footer-left",
189
+ content: "[Enter] Create PR | [ESC] Cancel | [Tab] Edit Title",
190
+ fg: THEME.dim,
191
+ });
192
+ this.footerBar.add(footerLeft);
193
+
194
+ this.statusText = new TextRenderable(renderer, {
195
+ id: "pr-preview-status",
196
+ content: "",
197
+ fg: THEME.accent,
198
+ });
199
+ this.footerBar.add(this.statusText);
200
+
201
+ this.root = this.overlay;
202
+ }
203
+
204
+ private setupKeyboard() {
205
+ if (this.keyHandler) return;
206
+
207
+ this.keyHandler = (key: KeyEvent) => {
208
+ if (!this.isVisible || this.isLoading) return;
209
+
210
+ // If title input is focused, let it handle keys
211
+ if (this.titleInput.focused) {
212
+ if (key.name === "escape") {
213
+ this.titleInput.blur();
214
+ this.isEditingTitle = false;
215
+ } else if (key.name === "return" || key.name === "enter") {
216
+ this.prTitle = this.titleInput.value;
217
+ this.titleInput.blur();
218
+ this.isEditingTitle = false;
219
+ }
220
+ return;
221
+ }
222
+
223
+ // Tab to edit title
224
+ if (key.name === "tab") {
225
+ this.isEditingTitle = true;
226
+ this.titleInput.focus();
227
+ }
228
+ // Enter to confirm
229
+ else if (key.name === "return" || key.name === "enter") {
230
+ this.prTitle = this.titleInput.value;
231
+ this.confirmCreate();
232
+ }
233
+ // Escape to cancel
234
+ else if (key.name === "escape") {
235
+ this.hide();
236
+ this.events.onCancel();
237
+ }
238
+ };
239
+
240
+ this.renderer.keyInput.on("keypress", this.keyHandler);
241
+ }
242
+
243
+ private cleanupKeyboard() {
244
+ if (this.keyHandler) {
245
+ this.renderer.keyInput.off("keypress", this.keyHandler);
246
+ this.keyHandler = null;
247
+ }
248
+ }
249
+
250
+ private async loadPRDescription() {
251
+ if (!this.session?.worktreeInfo) return;
252
+
253
+ this.isLoading = true;
254
+ this.statusText.content = "Generating PR description...";
255
+ this.renderer.requestRender();
256
+
257
+ try {
258
+ const { branchName, baseBranch } = this.session.worktreeInfo;
259
+ const sessionDir = this.session.id;
260
+
261
+ const prDesc = await generatePRDescription(
262
+ sessionDir,
263
+ branchName,
264
+ baseBranch
265
+ );
266
+
267
+ this.prTitle = prDesc.title;
268
+ this.prBody = prDesc.body;
269
+
270
+ this.titleInput.value = this.prTitle;
271
+ this.bodyPreview.content = this.prBody;
272
+ this.statusText.content = "";
273
+ } catch (error) {
274
+ this.statusText.content = `Error: ${error instanceof Error ? error.message : "Failed to generate description"}`;
275
+ } finally {
276
+ this.isLoading = false;
277
+ this.renderer.requestRender();
278
+ }
279
+ }
280
+
281
+ private async confirmCreate() {
282
+ if (!this.session || this.isLoading) return;
283
+
284
+ this.isLoading = true;
285
+ this.statusText.content = "Creating PR...";
286
+ this.renderer.requestRender();
287
+
288
+ try {
289
+ await this.events.onConfirm(this.session, this.prTitle, this.prBody);
290
+ this.hide();
291
+ } catch (error) {
292
+ this.statusText.content = `Error: ${error instanceof Error ? error.message : "Failed to create PR"}`;
293
+ this.isLoading = false;
294
+ this.renderer.requestRender();
295
+ }
296
+ }
297
+
298
+ public async show(session: SessionData) {
299
+ if (this.isVisible) return;
300
+ if (!session.worktreeInfo) return;
301
+
302
+ this.session = session;
303
+ this.isVisible = true;
304
+ this.overlay.visible = true;
305
+
306
+ // Update title
307
+ const { branchName, baseBranch } = session.worktreeInfo;
308
+ this.titleText.content = `Create Pull Request: ${branchName} -> ${baseBranch}`;
309
+
310
+ // Load PR description
311
+ await this.loadPRDescription();
312
+
313
+ // Setup keyboard
314
+ this.setupKeyboard();
315
+
316
+ this.renderer.requestRender();
317
+ }
318
+
319
+ public hide() {
320
+ if (!this.isVisible) return;
321
+
322
+ this.isVisible = false;
323
+ this.overlay.visible = false;
324
+ this.cleanupKeyboard();
325
+
326
+ // Reset state
327
+ this.prTitle = "";
328
+ this.prBody = "";
329
+ this.isEditingTitle = false;
330
+ this.isLoading = false;
331
+ this.titleInput.value = "";
332
+ this.titleInput.blur();
333
+ this.bodyPreview.content = "";
334
+ this.statusText.content = "";
335
+
336
+ this.renderer.requestRender();
337
+ }
338
+
339
+ public isOpen(): boolean {
340
+ return this.isVisible;
341
+ }
342
+
343
+ public destroy() {
344
+ this.cleanupKeyboard();
345
+ }
346
+ }
@@ -0,0 +1,232 @@
1
+ import { mock } from "bun:test";
2
+ import type { SessionData } from "../../types/tasks.ts";
3
+
4
+ export interface MockComponent {
5
+ id: string;
6
+ visible: boolean;
7
+ onMouse?: ((e: { type: string }) => void) | null;
8
+ add?: ReturnType<typeof mock>;
9
+ remove?: ReturnType<typeof mock>;
10
+ }
11
+
12
+ export interface MockBox extends MockComponent {
13
+ backgroundColor?: string;
14
+ opacity: number;
15
+ right: number;
16
+ getChildren: ReturnType<typeof mock>;
17
+ }
18
+
19
+ export interface MockScrollBox extends MockComponent {
20
+ getChildren: ReturnType<typeof mock>;
21
+ }
22
+
23
+ export interface MockText extends MockComponent {
24
+ fg: string;
25
+ content: string;
26
+ }
27
+
28
+ export interface MockSelect extends MockComponent {
29
+ options: Array<{ title: string; value: string; description?: string }>;
30
+ on: ReturnType<typeof mock>;
31
+ emit: (event: string, ...args: Array<string | number | boolean | object>) => void;
32
+ moveUp: ReturnType<typeof mock>;
33
+ moveDown: ReturnType<typeof mock>;
34
+ selectCurrent: ReturnType<typeof mock>;
35
+ setSelectedIndex: ReturnType<typeof mock>;
36
+ }
37
+
38
+ export interface MockInput extends MockComponent {
39
+ focused: boolean;
40
+ value: string;
41
+ focus: ReturnType<typeof mock>;
42
+ blur: ReturnType<typeof mock>;
43
+ }
44
+
45
+ interface MockRenderableOptions {
46
+ id?: string;
47
+ visible?: boolean;
48
+ opacity?: number;
49
+ backgroundColor?: string;
50
+ content?: string;
51
+ fg?: string;
52
+ view?: string;
53
+ diff?: string;
54
+ }
55
+
56
+ export interface MockRenderer {
57
+ requestRender: ReturnType<typeof mock>;
58
+ root: { add: ReturnType<typeof mock> };
59
+ keyInput: {
60
+ on: ReturnType<typeof mock>;
61
+ off: ReturnType<typeof mock>;
62
+ emit: (event: string, ...args: Array<{ name: string; ctrl?: boolean; meta?: boolean } | number | string>) => void;
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Standard mocking strategy for @opentui/core components.
68
+ */
69
+ export const createOpentuiMocks = () => {
70
+ const mocks = {
71
+ BoxRenderable: class implements MockBox {
72
+ public visible = true;
73
+ public opacity = 1;
74
+ public id: string;
75
+ public backgroundColor: string | undefined;
76
+ public right: number = 0;
77
+ constructor(public renderer: MockRenderer, public options: MockRenderableOptions) {
78
+ this.id = options?.id || "mock-box";
79
+ if (options?.visible !== undefined) this.visible = options.visible;
80
+ if (options?.opacity !== undefined) this.opacity = options.opacity;
81
+ if (options?.backgroundColor !== undefined) this.backgroundColor = options.backgroundColor;
82
+ }
83
+ add = mock(() => {});
84
+ remove = mock(() => {});
85
+ getChildren = mock(() => [] as MockComponent[]);
86
+ onMouse = null;
87
+ },
88
+ ScrollBoxRenderable: class implements MockScrollBox {
89
+ public id: string;
90
+ public visible = true;
91
+ private children: MockComponent[] = [];
92
+ constructor(public renderer: MockRenderer, public options: MockRenderableOptions) {
93
+ this.id = options?.id || "mock-scrollbox";
94
+ this.getChildren = mock(() => this.children);
95
+ this.add = mock((child: MockComponent) => this.children.push(child));
96
+ this.remove = mock((id: string) => {
97
+ this.children = this.children.filter(c => c.id !== id);
98
+ });
99
+ }
100
+ add: ReturnType<typeof mock> = mock(() => {});
101
+ getChildren: ReturnType<typeof mock> = mock(() => []);
102
+ remove: ReturnType<typeof mock> = mock(() => {});
103
+ },
104
+ TextRenderable: class implements MockText {
105
+ public id: string;
106
+ public visible = true;
107
+ public fg = "";
108
+ public content: string = "";
109
+ constructor(public renderer: MockRenderer, public options: MockRenderableOptions) {
110
+ this.id = options?.id || "mock-text";
111
+ this.content = options?.content || "";
112
+ this.fg = options?.fg || "";
113
+ }
114
+ onMouse = null;
115
+ },
116
+ DiffRenderable: class implements MockComponent {
117
+ public id: string;
118
+ public visible = true;
119
+ public diff: string = "";
120
+ public view: string = "";
121
+ public filetype: string = "";
122
+ public wrapMode: string = "";
123
+ constructor(public renderer: MockRenderer, public options: MockRenderableOptions) {
124
+ this.id = options?.id || "mock-diff";
125
+ this.diff = options?.diff || "";
126
+ this.view = options?.view || "unified";
127
+ }
128
+ destroy = mock(() => {});
129
+ },
130
+ SelectRenderable: class implements MockSelect {
131
+ public id: string;
132
+ public visible = true;
133
+ public options: Array<{ title: string; value: string; description?: string }> = [];
134
+ private handlers: Record<string, Function[]> = {};
135
+ constructor(public renderer: MockRenderer, public options_init: MockRenderableOptions) {
136
+ this.id = options_init?.id || "mock-select";
137
+ }
138
+ on = mock((event: string, handler: Function) => {
139
+ if (!this.handlers[event]) this.handlers[event] = [];
140
+ this.handlers[event].push(handler);
141
+ });
142
+ emit = (event: string, ...args: Array<string | number | boolean | object>) => {
143
+ this.handlers[event]?.forEach(h => h(...args));
144
+ };
145
+ moveUp = mock(() => {});
146
+ moveDown = mock(() => {});
147
+ selectCurrent = mock(() => {});
148
+ setSelectedIndex = mock(() => {});
149
+ },
150
+ InputRenderable: class implements MockInput {
151
+ public id: string;
152
+ public visible = true;
153
+ public focused = false;
154
+ public value = "";
155
+ constructor(public renderer: MockRenderer, public options: MockRenderableOptions) {
156
+ this.id = options?.id || "mock-input";
157
+ }
158
+ focus = mock(() => { this.focused = true; });
159
+ blur = mock(() => { this.focused = false; });
160
+ },
161
+ MarkdownRenderable: class implements MockComponent {
162
+ public id: string;
163
+ public visible = true;
164
+ public content = "";
165
+ constructor(public renderer: MockRenderer, public options: MockRenderableOptions) {
166
+ this.id = options?.id || "mock-markdown";
167
+ }
168
+ },
169
+ createTimeline: mock(() => ({
170
+ add: mock((target: { opacity: number }, options: { opacity?: number; onComplete?: () => void }) => {
171
+ if (options.opacity !== undefined) target.opacity = options.opacity;
172
+ if (options.onComplete) options.onComplete();
173
+ return { play: mock(() => {}) };
174
+ }),
175
+ play: mock(() => {}),
176
+ })),
177
+ TextAttributes: { BOLD: 1 },
178
+ RGBA: { fromInts: mock(() => ({})), fromValues: mock(() => ({})) },
179
+ parseColor: mock(() => ({})),
180
+ SyntaxStyle: { fromStyles: mock(() => ({ destroy: mock(() => {}) })) },
181
+ SelectRenderableEvents: { SELECTION_CHANGED: "selection_changed", ITEM_SELECTED: "item_selected" },
182
+ };
183
+ return mocks;
184
+ };
185
+
186
+ export const gitMock = {
187
+ getChangedFiles: mock(async () => [] as Array<{ path: string; status: string; additions: number; deletions: number }>),
188
+ getFileDiff: mock(async () => ""),
189
+ getFileType: mock(() => "typescript"),
190
+ getStatusIndicator: mock(() => "M"),
191
+ getStatusColor: mock(() => "#ffffff"),
192
+ generatePRDescription: mock(async () => ({ title: "", body: "" })),
193
+ };
194
+
195
+ export const createMockRenderer = (): MockRenderer => {
196
+ const handlers: Record<string, Function[]> = {};
197
+ return {
198
+ requestRender: mock(() => {}),
199
+ root: { add: mock(() => {}) },
200
+ keyInput: {
201
+ on: mock((event: string, handler: Function) => {
202
+ if (!handlers[event]) handlers[event] = [];
203
+ handlers[event].push(handler);
204
+ }),
205
+ off: mock((event: string, handler: Function) => {
206
+ if (handlers[event]) {
207
+ handlers[event] = handlers[event].filter(h => h !== handler);
208
+ }
209
+ }),
210
+ emit: (event: string, ...args: Array<{ name: string; ctrl?: boolean; meta?: boolean } | number | string>) => {
211
+ handlers[event]?.forEach(h => h(...args));
212
+ }
213
+ }
214
+ };
215
+ };
216
+
217
+ /**
218
+ * Creates a valid SessionData mock for tests.
219
+ */
220
+ export const createMockSession = (overrides: Partial<SessionData> = {}): SessionData => {
221
+ return {
222
+ id: "sessions/test",
223
+ prompt: "test prompt",
224
+ startTime: Date.now(),
225
+ status: "Running",
226
+ engine: "gemini",
227
+ isPrdMode: false,
228
+ iteration: 1,
229
+ history: [],
230
+ ...overrides,
231
+ } as SessionData;
232
+ };
@@ -0,0 +1,71 @@
1
+ import { expect, test, describe, mock, beforeEach } from "bun:test";
2
+ import { createMockRenderer } from "./mock-factory.ts";
3
+ import type { InputRenderable, BoxRenderable } from "@opentui/core";
4
+ import type { FilePickerState } from "./file-picker-utils.js";
5
+
6
+ // Mock search utility
7
+ const mockRecursiveSearch = mock(async () => ({ files: ["test.ts", "other.ts"] }));
8
+ mock.module("../utils/search.js", () => ({
9
+ recursiveSearch: mockRecursiveSearch,
10
+ }));
11
+
12
+ // Now import the target
13
+ import { setupFilePicker } from "./file-picker-utils.js";
14
+
15
+ describe("File Picker Utils", () => {
16
+ let mockRenderer: any;
17
+ let mockInput: InputRenderable;
18
+ let mockContainer: BoxRenderable;
19
+ let state: FilePickerState;
20
+
21
+ beforeEach(() => {
22
+ mockRenderer = createMockRenderer();
23
+
24
+ mockInput = {
25
+ value: "",
26
+ syntaxStyle: null,
27
+ removeHighlightsByRef: mock(() => {}),
28
+ addHighlightByCharRange: mock(() => {}),
29
+ focus: mock(() => {}),
30
+ on: mock(() => {}),
31
+ deleteCharBackward: mock(() => false),
32
+ } as any;
33
+
34
+ mockContainer = {
35
+ add: mock(() => {}),
36
+ remove: mock(() => {}),
37
+ } as any;
38
+
39
+ state = {
40
+ activePicker: null,
41
+ };
42
+ });
43
+
44
+ test("setupFilePicker should register syntax highlighting and override delete", () => {
45
+ const cleanup = setupFilePicker(mockRenderer, mockInput, mockContainer, state);
46
+ expect(mockInput.syntaxStyle).not.toBeNull();
47
+ expect(typeof mockInput.deleteCharBackward).toBe("function");
48
+ cleanup();
49
+ });
50
+
51
+ test("should trigger search when @ is typed", async () => {
52
+ setupFilePicker(mockRenderer, mockInput, mockContainer, state);
53
+
54
+ // @ts-ignore - accessing mock properties
55
+ const inputCalls = mockInput.on.mock.calls;
56
+ const onInput = inputCalls.find((c: [string, Function]) => c[0] === "input")[1];
57
+
58
+ await onInput("Checking @");
59
+ expect(mockRecursiveSearch).toHaveBeenCalled();
60
+ });
61
+
62
+ test("atomic deletion logic", () => {
63
+ setupFilePicker(mockRenderer, mockInput, mockContainer, state);
64
+
65
+ mockInput.value = "File is @test.ts";
66
+ // Should delete @test.ts atomically
67
+ const deleted = mockInput.deleteCharBackward();
68
+ expect(deleted).toBe(true);
69
+ expect(mockInput.value).toBe("File is ");
70
+ });
71
+ });