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,309 @@
1
+ import {
2
+ CliRenderer,
3
+ createCliRenderer,
4
+ BoxRenderable,
5
+ TextRenderable,
6
+ ScrollBoxRenderable,
7
+ KeyEvent,
8
+ engine,
9
+ MouseParser,
10
+ } from "@opentui/core";
11
+ import { DashboardController } from "./controllers/DashboardController.js";
12
+ import { createLandingView } from "./views/LandingView.js";
13
+ import { THEME } from "./theme.js";
14
+ import { isGameboyActive } from "../games/gameboy/GameboyView.js";
15
+ import { MultiLineInputRenderable, MultiLineInputEvents } from "./components/MultiLineInput.js";
16
+ import { buildVerticalBar, createInputContainerMouseHandler, createProviderMetadataRow, createCtrlCExitHandler } from "./input-chrome.js";
17
+
18
+ export async function createDashboard(renderer: CliRenderer, initialPrompt?: string) {
19
+ const INPUT_CHROME_LINES = 4;
20
+
21
+ const root = new BoxRenderable(renderer, {
22
+ id: "dashboard-root",
23
+ width: "100%",
24
+ height: "100%",
25
+ flexDirection: "column",
26
+ backgroundColor: THEME.bg,
27
+ paddingLeft: 4,
28
+ paddingRight: 4,
29
+ });
30
+
31
+ const mouseParser = new MouseParser();
32
+ renderer.addInputHandler((seq) => {
33
+ const mouse = mouseParser.parseMouseEvent(Buffer.from(seq));
34
+ if (mouse) {
35
+ // Mouse events handled here if needed
36
+ }
37
+ return false;
38
+ });
39
+
40
+ const mainContent = new BoxRenderable(renderer, {
41
+ id: "mainContent",
42
+ width: "100%",
43
+ flexGrow: 1,
44
+ flexDirection: "column",
45
+ justifyContent: "flex-start",
46
+ alignItems: "center",
47
+ paddingTop: 1,
48
+ backgroundColor: THEME.bg,
49
+ });
50
+
51
+ const separator = new BoxRenderable(renderer, {
52
+ id: "separator",
53
+ width: "96%",
54
+ height: 1,
55
+ border: ["bottom"],
56
+ borderColor: THEME.darkAccent,
57
+ marginBottom: 1,
58
+ flexShrink: 0,
59
+ alignSelf: "center",
60
+ visible: false,
61
+ });
62
+
63
+ const sessionContainer = new BoxRenderable(renderer, {
64
+ id: "sessionContainer",
65
+ width: "100%",
66
+ flexGrow: 1,
67
+ flexDirection: "column",
68
+ alignItems: "stretch",
69
+ gap: 0,
70
+ });
71
+
72
+
73
+
74
+ const dashboardView = new ScrollBoxRenderable(renderer, {
75
+ id: "dashboardView",
76
+ width: "100%",
77
+ flexGrow: 1,
78
+ flexDirection: "column",
79
+ scrollY: true,
80
+ scrollX: false,
81
+ scrollbarOptions: {
82
+ trackOptions: {
83
+ backgroundColor: THEME.darkAccent,
84
+ foregroundColor: THEME.accent,
85
+ },
86
+ },
87
+ });
88
+
89
+ const toyboxView = new BoxRenderable(renderer, {
90
+ id: "toyboxView",
91
+ width: "100%",
92
+ flexGrow: 1,
93
+ flexDirection: "column",
94
+ visible: false,
95
+ backgroundColor: THEME.bg,
96
+ });
97
+
98
+ const inputGroup = new BoxRenderable(renderer, {
99
+ id: "inputGroup",
100
+ width: "100%",
101
+ flexDirection: "column",
102
+ flexShrink: 0,
103
+ border: ["top"],
104
+ borderColor: THEME.darkAccent,
105
+ paddingTop: 1,
106
+ marginBottom: 1,
107
+ });
108
+
109
+ const inputContainer = new BoxRenderable(renderer, {
110
+ id: "inputContainer",
111
+ width: "100%",
112
+ minHeight: 5,
113
+ flexDirection: "column",
114
+ backgroundColor: THEME.surface,
115
+ paddingLeft: 1,
116
+ paddingRight: 1,
117
+ });
118
+
119
+ const input = new MultiLineInputRenderable(renderer, {
120
+ id: "input",
121
+ flexGrow: 1,
122
+ placeholder: 'Try "Write unit tests for this file"',
123
+ textColor: THEME.text,
124
+ focusedTextColor: THEME.text,
125
+ minHeight: 1,
126
+ maxHeight: 10,
127
+ });
128
+
129
+ const inputRow = new BoxRenderable(renderer, {
130
+ id: "inputRow",
131
+ width: "100%",
132
+ flexDirection: "row",
133
+ alignItems: "center",
134
+ });
135
+
136
+ inputRow.add(input);
137
+
138
+ inputContainer.onMouse = createInputContainerMouseHandler(inputContainer, input);
139
+
140
+ const { row: metadataRow, pickleLabel: metadataRowL, modelLabel } = createProviderMetadataRow(renderer, "metadataRow");
141
+
142
+ inputContainer.add(new BoxRenderable(renderer, { id: "spacer1", height: 1 }));
143
+ inputContainer.add(inputRow);
144
+ inputContainer.add(new BoxRenderable(renderer, { id: "spacer2", height: 1 }));
145
+ inputContainer.add(metadataRow);
146
+ inputContainer.add(new BoxRenderable(renderer, { id: "spacer3", height: 1 }));
147
+
148
+ const inputDecorativeBar = new TextRenderable(renderer, {
149
+ id: "inputDecorativeBar",
150
+ content: buildVerticalBar(inputContainer.minHeight ?? 5),
151
+ fg: THEME.accent,
152
+ position: "absolute",
153
+ left: 0,
154
+ top: 0,
155
+ });
156
+ inputContainer.add(inputDecorativeBar);
157
+
158
+ inputGroup.add(inputContainer);
159
+
160
+ const globalFooter = new BoxRenderable(renderer, {
161
+ id: "globalFooter",
162
+ width: "100%",
163
+ height: 1,
164
+ backgroundColor: THEME.bg,
165
+ flexDirection: "row",
166
+ justifyContent: "space-between",
167
+ paddingLeft: 1,
168
+ paddingRight: 1,
169
+ flexShrink: 0,
170
+ });
171
+
172
+ const footerLeft = new TextRenderable(renderer, {
173
+ id: "footerLeft",
174
+ content: "CTRL+T: Toybox",
175
+ fg: THEME.dim,
176
+ });
177
+
178
+ const footerRight = new TextRenderable(renderer, {
179
+ id: "footerRight",
180
+ content: "",
181
+ fg: THEME.dim,
182
+ });
183
+
184
+ globalFooter.add(footerLeft);
185
+ globalFooter.add(footerRight);
186
+
187
+ const controller = new DashboardController(renderer, sessionContainer);
188
+
189
+ const landingView = await createLandingView(renderer, (prompt, mode) => {
190
+ controller.startDashboardSession(prompt, mode);
191
+ });
192
+
193
+ controller.ui = {
194
+ tabs: undefined,
195
+ separator: separator,
196
+ dashboardView: dashboardView,
197
+ toyboxView: toyboxView,
198
+ inputGroup: inputGroup,
199
+ landingView: landingView.root,
200
+ mainContent: mainContent,
201
+ globalFooter: globalFooter,
202
+ input: input,
203
+ inputContainer: inputContainer,
204
+
205
+ metadataLabel: metadataRowL,
206
+ modelLabel: modelLabel,
207
+ footerLeft: footerLeft,
208
+ footerRight: footerRight,
209
+ };
210
+
211
+ input.focus();
212
+
213
+ const syncInputChrome = () => {
214
+ const minHeight = typeof inputContainer.minHeight === "number" ? inputContainer.minHeight : 5;
215
+ const inputHeight = typeof input.height === "number" ? input.height : 1;
216
+ const nextHeight = Math.max(minHeight, inputHeight + INPUT_CHROME_LINES);
217
+ if (inputContainer.height !== nextHeight) {
218
+ inputContainer.height = nextHeight;
219
+ inputDecorativeBar.content = buildVerticalBar(nextHeight);
220
+ renderer.requestRender();
221
+ }
222
+ };
223
+
224
+ syncInputChrome();
225
+
226
+
227
+ // Handle form submission on Enter
228
+ input.on(MultiLineInputEvents.SUBMIT, (value: string) => {
229
+ if (controller.hasActivePicker() || isGameboyActive()) return;
230
+ controller.spawnSession(value);
231
+ input.value = "";
232
+ syncInputChrome();
233
+ });
234
+
235
+ input.on(MultiLineInputEvents.INPUT, () => {
236
+ syncInputChrome();
237
+ });
238
+
239
+ renderer.keyInput.on("keypress", (key: KeyEvent) => {
240
+ if (controller.hasActivePicker() || isGameboyActive()) return false;
241
+
242
+ // Handle Ctrl+T to toggle toybox
243
+ if (key.ctrl && key.name === "t") {
244
+ controller.toggleToybox?.();
245
+ return true;
246
+ }
247
+
248
+ // Let other keys propagate to the focused input
249
+ return false;
250
+ });
251
+
252
+ dashboardView.add(separator);
253
+ dashboardView.add(sessionContainer);
254
+
255
+ mainContent.add(dashboardView);
256
+ mainContent.add(toyboxView);
257
+ mainContent.add(inputGroup);
258
+
259
+ root.add(landingView.root);
260
+ root.add(mainContent);
261
+ root.add(globalFooter);
262
+
263
+ if (initialPrompt) {
264
+ landingView.root.visible = false;
265
+ mainContent.visible = true;
266
+ globalFooter.visible = true;
267
+ controller.spawnSession(initialPrompt);
268
+ input.focus();
269
+ } else {
270
+ landingView.root.visible = true;
271
+ mainContent.visible = false;
272
+ globalFooter.visible = false;
273
+ landingView.input.focus();
274
+ }
275
+
276
+ return { root, sessionContainer, input };
277
+ }
278
+
279
+ export async function startDashboard(initialPrompt?: string) {
280
+ try {
281
+ const renderer = await createCliRenderer({ exitOnCtrlC: false, targetFps: 100 });
282
+ renderer.useMouse = true;
283
+
284
+ engine.attach(renderer);
285
+
286
+ const dashboard = await createDashboard(renderer, initialPrompt);
287
+ renderer.root.add(dashboard.root);
288
+
289
+ // Get footer reference for hints
290
+ const footerLeft = dashboard.root.getChildren()
291
+ .find(c => c.id === "globalFooter")
292
+ ?.getChildren()
293
+ .find(c => c.id === "footerLeft") as TextRenderable | undefined;
294
+
295
+ if (footerLeft) {
296
+ const exitHandler = createCtrlCExitHandler({
297
+ renderer,
298
+ hintText: footerLeft,
299
+ originalContent: footerLeft.content || "",
300
+ });
301
+ renderer.keyInput.on("keypress", exitHandler);
302
+ }
303
+
304
+ renderer.start();
305
+ } catch (error) {
306
+ console.error("❌ Failed to start Pickle Rick Dashboard:", error);
307
+ process.exit(1);
308
+ }
309
+ }
@@ -0,0 +1,146 @@
1
+ import { mock, expect, test, describe, beforeEach, afterEach } from "bun:test";
2
+ import { createMockRenderer, createMockSession, type MockRenderer, type MockSelect } from "./test-utils.ts";
3
+ import type { CliRenderer } from "@opentui/core";
4
+
5
+ mock.module("../theme.js", () => ({
6
+ THEME: {
7
+ bg: "#000000",
8
+ dim: "#555555",
9
+ accent: "#00ff00",
10
+ darkAccent: "#003300",
11
+ text: "#ffffff",
12
+ white: "#ffffff",
13
+ surface: "#111111",
14
+ green: "#00ff00",
15
+ }
16
+ }));
17
+
18
+ const mockLogView = {
19
+ root: { id: "mock-log-view-root", add: mock(() => {}) },
20
+ destroy: mock(() => {}),
21
+ };
22
+
23
+ mock.module("../views/LogView.js", () => ({
24
+ LogView: class {
25
+ constructor() { return mockLogView; }
26
+ }
27
+ }));
28
+
29
+ const utilsMock = {
30
+ formatDuration: mock((ms: number) => "10s"),
31
+ Clipboard: { copy: mock(async () => {}) },
32
+ };
33
+ mock.module("../../utils/index.js", () => utilsMock);
34
+
35
+ const fsMock = {
36
+ readFile: mock(async () => "mock log content"),
37
+ };
38
+ mock.module("node:fs/promises", () => fsMock);
39
+
40
+ interface MockInterval {
41
+ fn: Function;
42
+ ms: number;
43
+ }
44
+
45
+ describe("DashboardDialog", () => {
46
+ let mockRenderer: MockRenderer;
47
+ let originalSetInterval: typeof setInterval;
48
+ let originalClearInterval: typeof clearInterval;
49
+ let intervals: MockInterval[] = [];
50
+
51
+ beforeEach(() => {
52
+ mockRenderer = createMockRenderer();
53
+ intervals = [];
54
+ originalSetInterval = global.setInterval;
55
+ originalClearInterval = global.clearInterval;
56
+
57
+ // @ts-ignore - Mocking global timer functions
58
+ global.setInterval = mock((fn: Function, ms: number) => {
59
+ const id = { fn, ms };
60
+ intervals.push(id);
61
+ return id as unknown as Timer;
62
+ });
63
+
64
+ // @ts-ignore - Mocking global timer functions
65
+ global.clearInterval = mock((id: Timer) => {
66
+ intervals = intervals.filter(i => (i as unknown as Timer) !== id);
67
+ });
68
+ });
69
+
70
+ afterEach(() => {
71
+ global.setInterval = originalSetInterval;
72
+ global.clearInterval = originalClearInterval;
73
+ });
74
+
75
+ test("should initialize and setup UI", async () => {
76
+ const { DashboardDialog } = await import("./DashboardDialog.ts");
77
+ const dashboard = new DashboardDialog(mockRenderer as unknown as CliRenderer);
78
+ expect(dashboard).toBeDefined();
79
+ expect(dashboard.isOpen()).toBe(false);
80
+ });
81
+
82
+ test("should update with session data", async () => {
83
+ const { DashboardDialog } = await import("./DashboardDialog.ts");
84
+ const dashboard = new DashboardDialog(mockRenderer as unknown as CliRenderer);
85
+
86
+ const mockSession = createMockSession({
87
+ id: "sessions/test-id",
88
+ prompt: "test prompt",
89
+ startTime: Date.now() - 10000,
90
+ status: "Running",
91
+ engine: "gemini",
92
+ isPrdMode: false,
93
+ });
94
+
95
+ dashboard.update(mockSession);
96
+
97
+ expect(global.setInterval).toHaveBeenCalled();
98
+ expect(mockRenderer.requestRender).toHaveBeenCalled();
99
+ });
100
+
101
+ test("should clear ticker on hide", async () => {
102
+ const { DashboardDialog } = await import("./DashboardDialog.ts");
103
+ const dashboard = new DashboardDialog(mockRenderer as unknown as CliRenderer);
104
+
105
+ const mockSession = createMockSession({
106
+ id: "sessions/test-id",
107
+ prompt: "test prompt",
108
+ startTime: Date.now(),
109
+ status: "Running",
110
+ });
111
+
112
+ dashboard.update(mockSession);
113
+ expect(intervals.length).toBe(1);
114
+
115
+ dashboard.hide();
116
+ expect(global.clearInterval).toHaveBeenCalled();
117
+ expect(intervals.length).toBe(0);
118
+ });
119
+
120
+ test("should handle copy logs to clipboard", async () => {
121
+ const { DashboardDialog } = await import("./DashboardDialog.ts");
122
+ const dashboard = new DashboardDialog(mockRenderer as unknown as CliRenderer);
123
+
124
+ const mockSession = createMockSession({
125
+ id: "sessions/test-id",
126
+ prompt: "test prompt",
127
+ startTime: Date.now(),
128
+ status: "Running",
129
+ });
130
+
131
+ dashboard.update(mockSession);
132
+
133
+ const internalDialog = (dashboard as any).dialog as MockSelect;
134
+ const copyOption = internalDialog.options.find((o) => o.title === "Copy");
135
+ expect(copyOption).toBeDefined();
136
+
137
+ // We need to cast copyOption to include onSelect which is not in MockSelect options but is in the real DialogOption
138
+ interface DialogOptionWithSelect {
139
+ onSelect: (dialog: any) => Promise<void>;
140
+ }
141
+
142
+ await (copyOption as unknown as DialogOptionWithSelect).onSelect(internalDialog);
143
+ expect(fsMock.readFile).toHaveBeenCalled();
144
+ expect(utilsMock.Clipboard.copy).toHaveBeenCalledWith("mock log content");
145
+ });
146
+ });