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,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
|
+
});
|