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,103 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export interface SearchOptions {
5
+ limit?: number;
6
+ timeoutMs?: number;
7
+ ignore?: string[];
8
+ }
9
+
10
+ export interface SearchResult {
11
+ files: string[];
12
+ truncated: boolean;
13
+ }
14
+
15
+ export function fuzzyScore(text: string, query: string): number {
16
+ if (!query) return 1;
17
+ const target = text.toLowerCase();
18
+ const pattern = query.toLowerCase();
19
+
20
+ let score = 0;
21
+ let queryIdx = 0;
22
+
23
+ // Check for exact substring match first (highest priority)
24
+ if (target.includes(pattern)) {
25
+ score += 100;
26
+ // Bonus for matching at start of path segment
27
+ if (target.startsWith(pattern) || target.includes("/" + pattern)) {
28
+ score += 50;
29
+ }
30
+ }
31
+
32
+ // Fallback to subsequence matching
33
+ for (let i = 0; i < target.length && queryIdx < pattern.length; i++) {
34
+ if (target[i] === pattern[queryIdx]) {
35
+ queryIdx++;
36
+ if (queryIdx === 1) {
37
+ // Bonus for matching first char at start of segment
38
+ if (i === 0 || target[i-1] === "/" || target[i-1] === "_" || target[i-1] === "-") {
39
+ score += 10;
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ return queryIdx === pattern.length ? score + 1 : 0;
46
+ }
47
+
48
+ export function fuzzyMatch(text: string, query: string): boolean {
49
+ return fuzzyScore(text, query) > 0;
50
+ }
51
+
52
+ export async function recursiveSearch(
53
+ dir: string,
54
+ query: string,
55
+ options: SearchOptions = {}
56
+ ): Promise<SearchResult> {
57
+ const { limit = 20000, timeoutMs = 250, ignore = ["node_modules", ".git", "dist", ".DS_Store"] } = options;
58
+ const start = Date.now();
59
+ const results: { path: string, score: number }[] = [];
60
+ let fileCount = 0;
61
+ let truncated = false;
62
+
63
+ async function walk(currentDir: string) {
64
+ if (truncated) return;
65
+ if (Date.now() - start >= timeoutMs) {
66
+ truncated = true;
67
+ return;
68
+ }
69
+
70
+ try {
71
+ const entries = await readdir(currentDir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ if (truncated) return;
74
+ if (ignore.includes(entry.name)) continue;
75
+
76
+ const fullPath = join(currentDir, entry.name);
77
+ if (entry.isDirectory()) {
78
+ await walk(fullPath);
79
+ } else {
80
+ fileCount++;
81
+ const relativePath = fullPath.replace(dir + "/", "");
82
+ const score = fuzzyScore(relativePath, query);
83
+ if (score > 0) {
84
+ results.push({ path: fullPath, score });
85
+ }
86
+ if (fileCount >= limit) {
87
+ truncated = true;
88
+ return;
89
+ }
90
+ }
91
+ }
92
+ } catch (err) {
93
+ // Silently ignore
94
+ }
95
+ }
96
+
97
+ await walk(dir);
98
+
99
+ // Sort by score descending
100
+ results.sort((a, b) => b.score - a.score);
101
+
102
+ return { files: results.map(r => r.path), truncated };
103
+ }
@@ -0,0 +1,46 @@
1
+ import { expect, test, describe, beforeEach } from "bun:test";
2
+ import { sessionTracker, type TrackedSession } from "./session-tracker.js";
3
+
4
+ describe("SessionTracker", () => {
5
+ beforeEach(() => {
6
+ sessionTracker.clear();
7
+ });
8
+
9
+ const mockSession: TrackedSession = {
10
+ id: "session-1",
11
+ prompt: "Test Prompt",
12
+ createdAt: 1000,
13
+ startedAt: "2026-02-03T00:00:00Z",
14
+ sessionDir: "/tmp/session-1"
15
+ };
16
+
17
+ test("should add and track a session", () => {
18
+ sessionTracker.addSession(mockSession);
19
+ expect(sessionTracker.isTracked("session-1")).toBe(true);
20
+ expect(sessionTracker.getTrackedSessions()).toHaveLength(1);
21
+ });
22
+
23
+ test("should update a session", () => {
24
+ sessionTracker.addSession(mockSession);
25
+ sessionTracker.updateSession("session-1", { status: "running", iteration: 5 });
26
+
27
+ const sessions = sessionTracker.getTrackedSessions();
28
+ expect(sessions[0].status).toBe("running");
29
+ expect(sessions[0].iteration).toBe(5);
30
+ });
31
+
32
+ test("should remove a session", () => {
33
+ sessionTracker.addSession(mockSession);
34
+ sessionTracker.removeSession("session-1");
35
+ expect(sessionTracker.isTracked("session-1")).toBe(false);
36
+ });
37
+
38
+ test("should sort sessions by createdAt descending", () => {
39
+ sessionTracker.addSession({ ...mockSession, id: "old", createdAt: 1000 });
40
+ sessionTracker.addSession({ ...mockSession, id: "new", createdAt: 2000 });
41
+
42
+ const sessions = sessionTracker.getTrackedSessions();
43
+ expect(sessions[0].id).toBe("new");
44
+ expect(sessions[1].id).toBe("old");
45
+ });
46
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Global session tracking utility
3
+ * Tracks sessions created from the current UI instance (LandingView/Dashboard)
4
+ */
5
+
6
+ export interface TrackedSession {
7
+ id: string;
8
+ prompt: string;
9
+ createdAt: number;
10
+ startedAt: string;
11
+ sessionDir: string;
12
+ status?: string;
13
+ iteration?: number;
14
+ }
15
+
16
+ class SessionTracker {
17
+ private sessions: Map<string, TrackedSession> = new Map();
18
+
19
+ /**
20
+ * Add a session to the tracking list
21
+ */
22
+ public addSession(session: TrackedSession): void {
23
+ this.sessions.set(session.id, session);
24
+ }
25
+
26
+ /**
27
+ * Update status/iteration metadata for a tracked session
28
+ */
29
+ public updateSession(sessionId: string, partial: Partial<TrackedSession>): void {
30
+ const existing = this.sessions.get(sessionId);
31
+ if (!existing) return;
32
+ this.sessions.set(sessionId, { ...existing, ...partial });
33
+ }
34
+
35
+ /**
36
+ * Remove a session from tracking
37
+ */
38
+ public removeSession(sessionId: string): void {
39
+ this.sessions.delete(sessionId);
40
+ }
41
+
42
+ /**
43
+ * Get all tracked sessions
44
+ */
45
+ public getTrackedSessions(): TrackedSession[] {
46
+ return Array.from(this.sessions.values()).sort(
47
+ (a, b) => b.createdAt - a.createdAt
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Clear all tracked sessions (for testing/cleanup)
53
+ */
54
+ public clear(): void {
55
+ this.sessions.clear();
56
+ }
57
+
58
+ /**
59
+ * Check if a session is being tracked
60
+ */
61
+ public isTracked(sessionId: string): boolean {
62
+ return this.sessions.has(sessionId);
63
+ }
64
+ }
65
+
66
+ // Export singleton instance
67
+ export const sessionTracker = new SessionTracker();
@@ -0,0 +1,54 @@
1
+ import { expect, test, describe, beforeEach, afterEach, mock } from "bun:test";
2
+ import { Spinner } from "./spinner.js";
3
+
4
+ describe("Spinner Utility", () => {
5
+ let spinner: Spinner;
6
+ let originalWrite: any;
7
+ let mockWrite: any;
8
+
9
+ beforeEach(() => {
10
+ originalWrite = process.stdout.write;
11
+ mockWrite = mock(() => true);
12
+ process.stdout.write = mockWrite;
13
+ spinner = new Spinner();
14
+ });
15
+
16
+ afterEach(() => {
17
+ if (spinner['timer']) {
18
+ clearInterval(spinner['timer']);
19
+ }
20
+ process.stdout.write = originalWrite;
21
+ });
22
+
23
+ test("should start and create newline", () => {
24
+ spinner.start("Thinking...");
25
+ const calls = mockWrite.mock.calls.flat();
26
+ expect(calls).toContain("\x1B[?25l");
27
+ expect(calls).toContain("\n"); // Check for the initial newline
28
+ expect(calls.some((c: string) => c.includes("Thinking..."))).toBe(true);
29
+ });
30
+
31
+ test("should stop and show cursor", () => {
32
+ spinner.start("Work");
33
+ mockWrite.mockClear();
34
+ spinner.stop("Done");
35
+
36
+ const calls = mockWrite.mock.calls.flat();
37
+ expect(calls).toContain("\x1B[?25h");
38
+ });
39
+
40
+ test("should use move-up logic in printAbove", () => {
41
+ spinner.start("Spinning");
42
+ mockWrite.mockClear();
43
+
44
+ spinner.printAbove("Log message");
45
+
46
+ const calls = mockWrite.mock.calls.flat();
47
+ // Sequence: Clear -> Up -> Content -> Newline -> Render
48
+ expect(calls[0]).toBe('\r\x1B[K');
49
+ expect(calls[1]).toBe('\x1B[1A'); // The key fix
50
+ expect(calls[2]).toBe("Log message");
51
+ expect(calls[3]).toBe("\n");
52
+ expect(calls[4]).toContain("Spinning");
53
+ });
54
+ });
@@ -0,0 +1,87 @@
1
+ import pc from "picocolors";
2
+
3
+ export class Spinner {
4
+ private timer: Timer | null = null;
5
+ private text: string = "";
6
+ private frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
+ private frameIndex = 0;
8
+ private isSpinning = false;
9
+
10
+ start(text: string) {
11
+ if (this.isSpinning) {
12
+ this.update(text);
13
+ return;
14
+ }
15
+ this.isSpinning = true;
16
+ this.text = text;
17
+ process.stdout.write("\x1B[?25l"); // Hide cursor
18
+
19
+ // Create a new line for the spinner to live on
20
+ process.stdout.write("\n");
21
+
22
+ this.render();
23
+ this.timer = setInterval(() => {
24
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
25
+ this.render();
26
+ }, 80);
27
+ }
28
+
29
+ stop(message?: string, type: 'success' | 'error' = 'success') {
30
+ if (!this.isSpinning) return;
31
+ clearInterval(this.timer!);
32
+ this.timer = null;
33
+ this.isSpinning = false;
34
+
35
+ this.clearLine();
36
+
37
+ if (message) {
38
+ const symbol = type === 'success' ? pc.green('✔') : pc.red('✖');
39
+ process.stdout.write(`\r${symbol} ${message}\n`);
40
+ }
41
+ process.stdout.write("\x1B[?25h"); // Show cursor
42
+ }
43
+
44
+ success(text: string) {
45
+ this.stop(text, 'success');
46
+ }
47
+
48
+ error(text: string) {
49
+ this.stop(text, 'error');
50
+ }
51
+
52
+ update(text: string) {
53
+ this.text = text;
54
+ }
55
+
56
+ printAbove(content: string) {
57
+ if (this.isSpinning) {
58
+ // 1. Clear the spinner line
59
+ process.stdout.write('\r\x1B[K');
60
+ // 2. Move cursor up to the previous line (the log line)
61
+ process.stdout.write('\x1B[1A');
62
+ // 3. Print content (this appends to the log line)
63
+ process.stdout.write(content);
64
+
65
+ // 4. Ensure we return to the spinner line
66
+ if (!content.endsWith('\n')) {
67
+ process.stdout.write('\n');
68
+ }
69
+
70
+ // 5. Redraw spinner
71
+ this.render();
72
+ } else {
73
+ // Fallback if not spinning
74
+ process.stdout.write(content);
75
+ if (!content.endsWith('\n')) process.stdout.write('\n');
76
+ }
77
+ }
78
+
79
+ private clearLine() {
80
+ process.stdout.write('\r\x1B[K');
81
+ }
82
+
83
+ private render() {
84
+ const frame = pc.cyan(this.frames[this.frameIndex]);
85
+ process.stdout.write(`\r${frame} ${this.text}`);
86
+ }
87
+ }