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,329 @@
1
+ import {
2
+ BoxRenderable,
3
+ MouseEvent,
4
+ CliRenderer,
5
+ TextRenderable,
6
+ TextAttributes,
7
+ ScrollBoxRenderable,
8
+ createTimeline,
9
+ } from "@opentui/core";
10
+ import { THEME } from "../theme.js";
11
+ import { formatDuration } from "../../utils/index.js";
12
+ import { launchSnake } from "../../games/snake/SnakeView.js";
13
+ import { sessionTracker, type TrackedSession } from "../../utils/session-tracker.js";
14
+ import { loadState } from "../../services/config/state.js";
15
+
16
+ export class ToyboxSidebar {
17
+ public root: BoxRenderable;
18
+ private content: ScrollBoxRenderable;
19
+ private renderer: CliRenderer;
20
+ private isVisible = false;
21
+ public onHide?: () => void;
22
+ private ticker: ReturnType<typeof setInterval> | null = null;
23
+
24
+ private titleText: TextRenderable;
25
+
26
+ constructor(renderer: CliRenderer) {
27
+ this.renderer = renderer;
28
+ this.root = new BoxRenderable(renderer, {
29
+ id: "toybox-sidebar-root",
30
+ width: 45,
31
+ height: "100%",
32
+ position: "absolute",
33
+ right: -45, // Start off-screen
34
+ flexDirection: "column",
35
+ backgroundColor: THEME.bg,
36
+ visible: false,
37
+ zIndex: 25000,
38
+ border: ["left"],
39
+ borderColor: THEME.darkAccent,
40
+ });
41
+
42
+ this.content = new ScrollBoxRenderable(renderer, {
43
+ id: "toybox-sidebar-content",
44
+ width: "100%",
45
+ flexGrow: 1,
46
+ scrollY: true,
47
+ scrollX: false,
48
+ scrollbarOptions: {
49
+ trackOptions: {
50
+ backgroundColor: THEME.darkAccent,
51
+ foregroundColor: THEME.accent,
52
+ },
53
+ },
54
+ });
55
+
56
+ // Header
57
+ const header = new BoxRenderable(renderer, {
58
+ id: "toybox-sidebar-header",
59
+ width: "100%",
60
+ height: 3,
61
+ flexDirection: "column",
62
+ justifyContent: "center",
63
+ paddingLeft: 2,
64
+ paddingRight: 2,
65
+ border: ["bottom"],
66
+ borderColor: THEME.darkAccent,
67
+ flexShrink: 0,
68
+ });
69
+
70
+ this.titleText = new TextRenderable(renderer, {
71
+ id: "toybox-sidebar-title",
72
+ content: "Sessions",
73
+ fg: THEME.accent,
74
+ attributes: TextAttributes.BOLD,
75
+ });
76
+
77
+ header.add(this.titleText);
78
+
79
+ this.root.add(header);
80
+ this.root.add(this.content);
81
+
82
+ this.setupMouseHandlers();
83
+ }
84
+
85
+ private setupMouseHandlers() {
86
+ this.root.onMouse = (event: MouseEvent) => {
87
+ if ((event.type as any) === "click") {
88
+ // Allow clicking through to content
89
+ return;
90
+ }
91
+ };
92
+ }
93
+
94
+ public async show() {
95
+ if (this.isVisible) return;
96
+
97
+ this.isVisible = true;
98
+ this.root.visible = true;
99
+
100
+ // Animate in
101
+ createTimeline().add(this.root, {
102
+ right: 0,
103
+ duration: 200,
104
+ ease: "outQuad",
105
+ onUpdate: () => this.renderer.requestRender(),
106
+ });
107
+
108
+ await this.refreshContent();
109
+ this.startTicker();
110
+ }
111
+
112
+ public hide() {
113
+ if (!this.isVisible) return;
114
+
115
+ this.stopTicker();
116
+
117
+ // Animate out
118
+ createTimeline().add(this.root, {
119
+ right: -45,
120
+ duration: 200,
121
+ ease: "inQuad",
122
+ onUpdate: () => this.renderer.requestRender(),
123
+ onComplete: () => {
124
+ this.isVisible = false;
125
+ this.root.visible = false;
126
+ this.onHide?.();
127
+ },
128
+ });
129
+ }
130
+
131
+ public destroy() {
132
+ this.stopTicker();
133
+ }
134
+
135
+ public isOpen(): boolean {
136
+ return this.isVisible;
137
+ }
138
+
139
+ private async refreshContent() {
140
+ // Clear all children - collect IDs first, then remove in reverse order
141
+ const children = this.content.getChildren();
142
+ for (let i = children.length - 1; i >= 0; i--) {
143
+ this.content.remove(children[i].id);
144
+ }
145
+
146
+ // Add sessions section
147
+ await this.addSessionsSection();
148
+
149
+ this.renderer.requestRender();
150
+ }
151
+
152
+ private startTicker() {
153
+ if (this.ticker) return;
154
+ this.ticker = setInterval(async () => {
155
+ if (!this.isVisible) return;
156
+ try {
157
+ await this.refreshContent();
158
+ } catch (e) {
159
+ // Silently ignore refresh errors
160
+ }
161
+ }, 2000);
162
+ }
163
+
164
+ private stopTicker() {
165
+ if (this.ticker) {
166
+ clearInterval(this.ticker);
167
+ this.ticker = null;
168
+ }
169
+ }
170
+
171
+ private async addSessionsSection() {
172
+ try {
173
+ // Get tracked sessions (created from this UI instance)
174
+ const trackedSessions = sessionTracker.getTrackedSessions();
175
+
176
+ if (trackedSessions.length === 0) {
177
+ const emptyState = new TextRenderable(this.renderer, {
178
+ id: "empty-sessions",
179
+ content: "No active sessions",
180
+ fg: THEME.dim,
181
+ alignSelf: "center",
182
+ marginTop: 1,
183
+ });
184
+ this.content.add(emptyState);
185
+ return;
186
+ }
187
+
188
+ // Get current status for each tracked session
189
+ const sessionsWithStatus = await Promise.all(
190
+ trackedSessions.map(async (trackedSession) => {
191
+ const state = await loadState(trackedSession.sessionDir);
192
+ // Always prefer fresh state from disk over cached status
193
+ const status = state && state.active && state.step !== "done"
194
+ ? `${state.step.toUpperCase()} (Iteration ${state.iteration})`
195
+ : state?.step === "done"
196
+ ? "Done"
197
+ : trackedSession.status ?? "Starting...";
198
+ const iteration = state?.iteration ?? trackedSession.iteration ?? 0;
199
+
200
+ return {
201
+ ...trackedSession,
202
+ status,
203
+ iteration,
204
+ };
205
+ })
206
+ );
207
+
208
+ // Create session cards
209
+ for (let i = 0; i < sessionsWithStatus.length; i++) {
210
+ const session = sessionsWithStatus[i];
211
+ const sessionCard = this.createSessionCard(session, i);
212
+ this.content.add(sessionCard);
213
+ }
214
+
215
+ } catch (error) {
216
+ const errorText = new TextRenderable(this.renderer, {
217
+ id: "error-text",
218
+ content: "Failed to load sessions",
219
+ fg: "#ff6b6b",
220
+ alignSelf: "center",
221
+ marginTop: 2,
222
+ });
223
+ this.content.add(errorText);
224
+ }
225
+ }
226
+
227
+ private createSessionCard(session: TrackedSession & { status: string }, index: number): BoxRenderable {
228
+ const shortId = session.id.substring(0, 8);
229
+ const cardId = `${index}-${shortId}`;
230
+
231
+ const card = new BoxRenderable(this.renderer, {
232
+ id: `session-card-${cardId}`,
233
+ width: "100%",
234
+ flexDirection: "column",
235
+ paddingLeft: 2,
236
+ paddingRight: 2,
237
+ paddingTop: 1,
238
+ paddingBottom: 1,
239
+ border: ["bottom"],
240
+ borderColor: THEME.darkAccent,
241
+ });
242
+
243
+ // Status indicator and title
244
+ const statusRow = new BoxRenderable(this.renderer, {
245
+ id: `status-row-${cardId}`,
246
+ width: "100%",
247
+ flexDirection: "row",
248
+ justifyContent: "space-between",
249
+ marginBottom: 1,
250
+ });
251
+
252
+ const isActive = session.status.toLowerCase() !== "done";
253
+ const statusColor = isActive ? THEME.green : THEME.accent;
254
+
255
+ const statusIndicator = new TextRenderable(this.renderer, {
256
+ id: `status-indicator-${cardId}`,
257
+ content: isActive ? "🟢" : "✅",
258
+ fg: statusColor,
259
+ });
260
+
261
+ const statusText = new TextRenderable(this.renderer, {
262
+ id: `status-text-${cardId}`,
263
+ content: session.status.toUpperCase(),
264
+ fg: statusColor,
265
+ attributes: TextAttributes.BOLD,
266
+
267
+ });
268
+
269
+ statusRow.add(statusIndicator);
270
+ statusRow.add(statusText);
271
+
272
+ // Session prompt (truncated)
273
+ const promptText = session.prompt.length > 50
274
+ ? session.prompt.substring(0, 47) + "..."
275
+ : session.prompt;
276
+
277
+ const promptRenderable = new TextRenderable(this.renderer, {
278
+ id: `prompt-${cardId}`,
279
+ content: promptText,
280
+ fg: THEME.text,
281
+ marginBottom: 1,
282
+
283
+ });
284
+
285
+ // Session metadata
286
+ const metaRow = new BoxRenderable(this.renderer, {
287
+ id: `meta-row-${cardId}`,
288
+ width: "100%",
289
+ flexDirection: "row",
290
+ justifyContent: "space-between",
291
+ });
292
+
293
+ const timeAgo = formatDuration(Date.now() - session.createdAt);
294
+
295
+ const timeText = new TextRenderable(this.renderer, {
296
+ id: `time-${cardId}`,
297
+ content: `Started ${timeAgo} ago`,
298
+ fg: THEME.dim,
299
+
300
+ });
301
+
302
+ const sessionIdText = new TextRenderable(this.renderer, {
303
+ id: `session-id-${cardId}`,
304
+ content: shortId,
305
+ fg: THEME.darkAccent,
306
+ });
307
+
308
+ metaRow.add(timeText);
309
+ metaRow.add(sessionIdText);
310
+
311
+ card.add(statusRow);
312
+ card.add(promptRenderable);
313
+ card.add(metaRow);
314
+
315
+ // Add hover effect
316
+ card.onMouse = (event: MouseEvent) => {
317
+ switch (event.type) {
318
+ case "over":
319
+ card.backgroundColor = "#2d372d";
320
+ break;
321
+ case "out":
322
+ card.backgroundColor = THEME.bg;
323
+ break;
324
+ }
325
+ };
326
+
327
+ return card;
328
+ }
329
+ }
@@ -0,0 +1,35 @@
1
+ # Refactor Plan: Clean up MultiLineInput Tests
2
+
3
+ ## Overview
4
+ Remove `any` types and redundant comments from `MultiLineInput.test.ts`.
5
+
6
+ ## Current State Analysis
7
+ - `mockCtx` is typed as `any`.
8
+ - Multiple casts to `any` for private property access.
9
+ - `keyEvent` is `any`.
10
+
11
+ ## The Kill List
12
+ - `mockCtx: any`
13
+ - `(input as any)`
14
+ - `keyEvent: any`
15
+
16
+ ## Consolidation Map
17
+ - Use a partial of `RenderContext` for `mockCtx`.
18
+ - Create an `InternalMultiLineInput` interface for private access.
19
+ - Use `KeyEvent` from `@opentui/core` for `keyEvent`.
20
+
21
+ ## Changes Required:
22
+ #### cli/src/ui/components/MultiLineInput.test.ts
23
+ **Changes**: Add `InternalMultiLineInput` interface and fix types.
24
+ ```typescript
25
+ interface InternalMultiLineInput {
26
+ virtualLineCount: number;
27
+ onContentChange: () => void;
28
+ adjustHeight: () => void;
29
+ emit: (event: any, ...args: any[]) => void;
30
+ _placeholder: any;
31
+ }
32
+ ```
33
+
34
+ ## Verification
35
+ - [ ] `bun test cli/src/ui/components/MultiLineInput.test.ts`
@@ -0,0 +1,43 @@
1
+ import { expect, test, describe, beforeEach, afterEach, mock, spyOn } from "bun:test";
2
+ import "../test-setup.js";
3
+ import { createMockRenderer } from "../mock-factory.js";
4
+ import { DashboardController } from "./DashboardController.js";
5
+ import * as search from "../../utils/search.js";
6
+ import * as state from "../../services/config/state.js";
7
+
8
+ describe("DashboardController Integration", () => {
9
+ let mockRenderer: any;
10
+ let mockSessionContainer: any;
11
+ let mockSidebar: any;
12
+ let spies: any[] = [];
13
+
14
+ beforeEach(() => {
15
+ mockRenderer = createMockRenderer();
16
+ mockSessionContainer = { add: mock(() => {}), getChildren: mock(() => []) };
17
+ mockSidebar = { onHide: null, root: { visible: false }, isOpen: () => false, hide: mock(() => {}), update: mock(() => {}) };
18
+
19
+ spies = [
20
+ spyOn(search, "recursiveSearch").mockImplementation(async (dir, query) => {
21
+ if (query === "test") {
22
+ return { files: ["/root/test.ts"], truncated: false };
23
+ }
24
+ return { files: [], truncated: false };
25
+ }),
26
+ spyOn(state, "listSessions").mockResolvedValue([]),
27
+ spyOn(state, "createSession").mockResolvedValue({ session_dir: "/tmp/session" } as any),
28
+ ];
29
+ });
30
+
31
+ afterEach(() => {
32
+ spies.forEach(s => s.mockRestore());
33
+ });
34
+
35
+ test("should initialize controller", () => {
36
+ const controller = new DashboardController(
37
+ mockRenderer,
38
+ mockSessionContainer
39
+ );
40
+
41
+ expect(controller).toBeDefined();
42
+ });
43
+ });