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,481 @@
1
+ import {
2
+ BoxRenderable,
3
+ CliRenderer,
4
+ TextRenderable,
5
+ MouseEvent,
6
+ createTimeline,
7
+ Timeline,
8
+ RGBA,
9
+ parseColor,
10
+ rgbToHex,
11
+ StyledText,
12
+ TextChunk,
13
+ } from "@opentui/core";
14
+ import { SessionData } from "../../types/tasks.js";
15
+ import { THEME } from "../theme.js";
16
+ import { formatDuration, isSessionActive } from "../../utils/index.js";
17
+
18
+ function interpolateColor(color1: string, color2: string, factor: number): string {
19
+ const c1 = parseColor(color1);
20
+ const c2 = parseColor(color2);
21
+ const r = c1.r + (c2.r - c1.r) * factor;
22
+ const g = c1.g + (c2.g - c1.g) * factor;
23
+ const b = c1.b + (c2.b - c1.b) * factor;
24
+ const a = c1.a + (c2.a - c1.a) * factor;
25
+ return rgbToHex(RGBA.fromValues(r, g, b, a));
26
+ }
27
+
28
+ // Colors for log-style display
29
+ const LOG_COLORS = {
30
+ timestamp: THEME.dim,
31
+ gitBranch: THEME.accent,
32
+ gitAhead: THEME.green,
33
+ gitBehind: THEME.error,
34
+ gitModified: THEME.orange,
35
+ path: THEME.dim,
36
+ prompt: THEME.text,
37
+ statusLabel: THEME.orange,
38
+ infoLabel: THEME.blue,
39
+ runLabel: THEME.accent,
40
+ doneLabel: THEME.green,
41
+ errorLabel: THEME.error,
42
+ statusText: THEME.dim,
43
+ runText: THEME.text,
44
+ elapsed: THEME.dim,
45
+ };
46
+
47
+ export class SessionChip extends BoxRenderable {
48
+ public session: SessionData;
49
+ private headerLine: TextRenderable;
50
+ private statusLine: TextRenderable;
51
+ private infoLine: TextRenderable;
52
+ private runLine: TextRenderable;
53
+ private doneLine: TextRenderable;
54
+ private connectorLine: TextRenderable;
55
+ private cancelButton: TextRenderable;
56
+ private reviewButton: TextRenderable;
57
+ private renderer: CliRenderer;
58
+
59
+ public isHovered = false;
60
+ public isPressed = false;
61
+ public isFocused = false;
62
+ public isSelected = false;
63
+ private visualTimeline: Timeline | null = null;
64
+ private currentBg: string = "transparent";
65
+ private onSelectCallback: (session: SessionData) => void;
66
+ private onCancelCallback?: (session: SessionData) => void;
67
+ private onReviewCallback?: (session: SessionData) => void;
68
+
69
+ constructor(
70
+ renderer: CliRenderer,
71
+ session: SessionData,
72
+ onSelect: (session: SessionData) => void,
73
+ onCancel?: (session: SessionData) => void,
74
+ onReview?: (session: SessionData) => void
75
+ ) {
76
+ super(renderer, {
77
+ id: `session-${session.id}`,
78
+ width: "100%",
79
+ flexDirection: "column",
80
+ paddingLeft: 1,
81
+ paddingRight: 1,
82
+ paddingTop: 0,
83
+ paddingBottom: 0,
84
+ alignSelf: "stretch",
85
+ });
86
+
87
+ this.renderer = renderer;
88
+ this.session = session;
89
+ this.onSelectCallback = onSelect;
90
+ this.onCancelCallback = onCancel;
91
+ this.onReviewCallback = onReview;
92
+
93
+ // Set up mouse handlers directly on this component
94
+ this.onMouse = (event: MouseEvent) => {
95
+ switch (event.type) {
96
+ case "up":
97
+ this.isPressed = false;
98
+ this.updateVisuals(150);
99
+ // Only select if the target is not the cancel or review button
100
+ if (event.target !== this.cancelButton && event.target !== this.reviewButton) {
101
+ this.onSelectCallback(this.session);
102
+ }
103
+ break;
104
+ case "over":
105
+ if (!this.isFocused) {
106
+ this.isHovered = true;
107
+ this.updateVisuals(200);
108
+ }
109
+ break;
110
+ case "out":
111
+ this.isHovered = false;
112
+ this.updateVisuals(200);
113
+ break;
114
+ case "down":
115
+ this.isPressed = true;
116
+ this.updateVisuals(50);
117
+ break;
118
+ }
119
+ };
120
+
121
+ // Cancel button [X] - visible when session is active
122
+ this.cancelButton = new TextRenderable(renderer, {
123
+ id: `session-${session.id}-cancel`,
124
+ position: "absolute",
125
+ right: 1,
126
+ top: 0,
127
+ content: "[X]",
128
+ fg: THEME.dim,
129
+ zIndex: 100,
130
+ });
131
+
132
+ this.cancelButton.onMouse = (event: MouseEvent) => {
133
+ if (event.type === "over") {
134
+ this.cancelButton.fg = THEME.error;
135
+ this.renderer.requestRender();
136
+ } else if (event.type === "out") {
137
+ this.cancelButton.fg = THEME.dim;
138
+ this.renderer.requestRender();
139
+ } else if ((event.type as any) === "click" || event.type === "up") {
140
+ if (this.onCancelCallback) {
141
+ this.onCancelCallback(this.session);
142
+ }
143
+ }
144
+ };
145
+
146
+ this.add(this.cancelButton);
147
+
148
+ // Review button [Review] - visible when session is done with worktree
149
+ this.reviewButton = new TextRenderable(renderer, {
150
+ id: `session-${session.id}-review`,
151
+ position: "absolute",
152
+ right: 1,
153
+ top: 0,
154
+ content: " [Review]", // leading space to give breathing room from status text
155
+ fg: THEME.accent,
156
+ zIndex: 100,
157
+ visible: false,
158
+ });
159
+
160
+ this.reviewButton.onMouse = (event: MouseEvent) => {
161
+ if (event.type === "over") {
162
+ this.reviewButton.fg = THEME.white;
163
+ this.renderer.requestRender();
164
+ } else if (event.type === "out") {
165
+ this.reviewButton.fg = THEME.accent;
166
+ this.renderer.requestRender();
167
+ } else if ((event.type as any) === "click" || event.type === "up") {
168
+ if (this.onReviewCallback) {
169
+ this.onReviewCallback(this.session);
170
+ }
171
+ }
172
+ };
173
+
174
+ this.add(this.reviewButton);
175
+
176
+ // Header line: timestamp [branch +ahead -behind ~modified] path > prompt
177
+ this.headerLine = new TextRenderable(renderer, {
178
+ id: `session-${session.id}-header`,
179
+ content: this.buildHeaderContent(),
180
+ truncate: true,
181
+ });
182
+
183
+ // Status line: │ [STATUS] message
184
+ this.statusLine = new TextRenderable(renderer, {
185
+ id: `session-${session.id}-status`,
186
+ content: this.buildStatusContent("Ready."),
187
+ truncate: true,
188
+ });
189
+
190
+ // Info line: │ [INFO] message
191
+ this.infoLine = new TextRenderable(renderer, {
192
+ id: `session-${session.id}-info`,
193
+ content: this.buildInfoContent("Initializing model..."),
194
+ truncate: true,
195
+ });
196
+
197
+ // Run line: │ [RUN ] iteration info
198
+ this.runLine = new TextRenderable(renderer, {
199
+ id: `session-${session.id}-run`,
200
+ content: this.buildRunContent(),
201
+ truncate: true,
202
+ });
203
+
204
+ // Done line: │ └─[Done] Elapsed: XXs
205
+ this.doneLine = new TextRenderable(renderer, {
206
+ id: `session-${session.id}-done`,
207
+ content: this.buildDoneContent(),
208
+ truncate: true,
209
+ });
210
+
211
+ // Connector lines to next chip (two │ lines for spacing)
212
+ this.connectorLine = new TextRenderable(renderer, {
213
+ id: `session-${session.id}-connector`,
214
+ content: "│\n│",
215
+ fg: THEME.dim,
216
+ });
217
+
218
+ this.add(this.headerLine);
219
+ this.add(this.statusLine);
220
+ this.add(this.infoLine);
221
+ this.add(this.runLine);
222
+ this.add(this.doneLine);
223
+ this.add(this.connectorLine);
224
+ }
225
+
226
+ private formatTime(timestamp: number): string {
227
+ const date = new Date(timestamp);
228
+ return date.toLocaleTimeString("en-US", {
229
+ hour12: false,
230
+ hour: "2-digit",
231
+ minute: "2-digit",
232
+ second: "2-digit",
233
+ });
234
+ }
235
+
236
+ private shortenPath(path?: string): string {
237
+ if (!path) return "~/project";
238
+
239
+ // Get the last meaningful directory name
240
+ const segments = path.split("/").filter(s => s.length > 0);
241
+ if (segments.length === 0) return "~/project";
242
+
243
+ // Get last segment (project folder name)
244
+ const lastSegment = segments[segments.length - 1];
245
+
246
+ // If it looks like a common folder name, try to get parent too
247
+ const commonFolders = ["src", "lib", "app", "cli", "dist", "build"];
248
+ if (commonFolders.includes(lastSegment.toLowerCase()) && segments.length > 1) {
249
+ return `~/${segments[segments.length - 2]}/${lastSegment}`;
250
+ }
251
+
252
+ return `~/project/${lastSegment}`;
253
+ }
254
+
255
+ private buildHeaderContent(): StyledText {
256
+ const chunks: TextChunk[] = [];
257
+ const gs = this.session.gitStatus;
258
+ const time = this.formatTime(this.session.startTime);
259
+ const path = this.shortenPath(this.session.workingDir);
260
+ const prompt = this.session.prompt;
261
+
262
+ // Timestamp (no prefix on header)
263
+ chunks.push({ text: time, fg: parseColor(LOG_COLORS.timestamp), __isChunk: true });
264
+ chunks.push({ text: " ", __isChunk: true });
265
+
266
+ // Git status: [branch +ahead -behind ~modified]
267
+ chunks.push({ text: "[", fg: parseColor(LOG_COLORS.gitBranch), __isChunk: true });
268
+ chunks.push({ text: gs?.branch || "main", fg: parseColor(LOG_COLORS.gitBranch), __isChunk: true });
269
+
270
+ if (gs) {
271
+ if (gs.ahead > 0) {
272
+ chunks.push({ text: " +", fg: parseColor(LOG_COLORS.gitAhead), __isChunk: true });
273
+ chunks.push({ text: String(gs.ahead), fg: parseColor(LOG_COLORS.gitAhead), __isChunk: true });
274
+ }
275
+ if (gs.behind > 0) {
276
+ chunks.push({ text: " -", fg: parseColor(LOG_COLORS.gitBehind), __isChunk: true });
277
+ chunks.push({ text: String(gs.behind), fg: parseColor(LOG_COLORS.gitBehind), __isChunk: true });
278
+ }
279
+ if (gs.modified > 0) {
280
+ chunks.push({ text: " ~", fg: parseColor(LOG_COLORS.gitModified), __isChunk: true });
281
+ chunks.push({ text: String(gs.modified), fg: parseColor(LOG_COLORS.gitModified), __isChunk: true });
282
+ }
283
+ }
284
+
285
+ chunks.push({ text: "]", fg: parseColor(LOG_COLORS.gitBranch), __isChunk: true });
286
+ chunks.push({ text: " ", __isChunk: true });
287
+
288
+ // Path
289
+ chunks.push({ text: path, fg: parseColor(LOG_COLORS.path), __isChunk: true });
290
+ chunks.push({ text: " > ", fg: parseColor(LOG_COLORS.path), __isChunk: true });
291
+
292
+ // Prompt
293
+ chunks.push({ text: prompt, fg: parseColor(LOG_COLORS.prompt), __isChunk: true });
294
+
295
+ return new StyledText(chunks);
296
+ }
297
+
298
+ private buildStatusContent(message: string): StyledText {
299
+ const chunks: TextChunk[] = [];
300
+ chunks.push({ text: "│ ", fg: parseColor(THEME.dim), __isChunk: true });
301
+ chunks.push({ text: "[STATUS] ", fg: parseColor(LOG_COLORS.statusLabel), __isChunk: true });
302
+ chunks.push({ text: message, fg: parseColor(LOG_COLORS.statusText), __isChunk: true });
303
+ return new StyledText(chunks);
304
+ }
305
+
306
+ private buildInfoContent(message: string): StyledText {
307
+ const chunks: TextChunk[] = [];
308
+ chunks.push({ text: "│ ", fg: parseColor(THEME.dim), __isChunk: true });
309
+ chunks.push({ text: "[INFO] ", fg: parseColor(LOG_COLORS.infoLabel), __isChunk: true });
310
+ chunks.push({ text: message, fg: parseColor(LOG_COLORS.statusText), __isChunk: true });
311
+ return new StyledText(chunks);
312
+ }
313
+
314
+ private buildRunContent(): StyledText {
315
+ const chunks: TextChunk[] = [];
316
+ const iteration = this.session.iteration || 1;
317
+ const status = this.session.status;
318
+
319
+ // Extract step from status if available (e.g., "Iteration 1: DRAFT PRD (prd)")
320
+ let stepInfo = `Iteration ${iteration}: DRAFT PRD`;
321
+ if (status && !status.toLowerCase().includes("initializing") &&
322
+ !status.toLowerCase().includes("done") &&
323
+ !status.toLowerCase().includes("error")) {
324
+ // Clean up the status to show just the relevant part
325
+ stepInfo = status;
326
+ }
327
+
328
+ chunks.push({ text: "│ ", fg: parseColor(THEME.dim), __isChunk: true });
329
+ chunks.push({ text: "[RUN ] ", fg: parseColor(LOG_COLORS.runLabel), __isChunk: true });
330
+ chunks.push({ text: stepInfo, fg: parseColor(LOG_COLORS.runText), __isChunk: true });
331
+ return new StyledText(chunks);
332
+ }
333
+
334
+ private buildDoneContent(): StyledText {
335
+ const chunks: TextChunk[] = [];
336
+ const durationMs = Date.now() - this.session.startTime;
337
+ const isFinished = !isSessionActive(this.session.status);
338
+ const hasError = this.session.status.toLowerCase().includes("error");
339
+
340
+ // Vertical line with corner: │ └─
341
+ chunks.push({ text: "│ └─", fg: parseColor(THEME.dim), __isChunk: true });
342
+
343
+ if (hasError) {
344
+ chunks.push({ text: "[Error]", fg: parseColor(LOG_COLORS.errorLabel), __isChunk: true });
345
+ } else if (isFinished) {
346
+ chunks.push({ text: "[Done]", fg: parseColor(LOG_COLORS.doneLabel), __isChunk: true });
347
+ } else {
348
+ chunks.push({ text: "[...]", fg: parseColor(LOG_COLORS.elapsed), __isChunk: true });
349
+ }
350
+
351
+ chunks.push({ text: " Elapsed: ", fg: parseColor(LOG_COLORS.elapsed), __isChunk: true });
352
+ chunks.push({ text: formatDuration(durationMs), fg: parseColor(LOG_COLORS.elapsed), __isChunk: true });
353
+
354
+ return new StyledText(chunks);
355
+ }
356
+
357
+ private updateVisuals(duration: number = 0) {
358
+ if (this.visualTimeline) {
359
+ this.visualTimeline.pause();
360
+ this.visualTimeline = null;
361
+ }
362
+
363
+ let targetBg = "transparent";
364
+
365
+ // Hover: green-tinted background
366
+ if (this.isHovered) {
367
+ targetBg = "#0d1a0d";
368
+ }
369
+
370
+ // Pressed: slightly brighter green tint
371
+ if (this.isPressed) {
372
+ targetBg = "#1a2e1a";
373
+ }
374
+
375
+ if (duration === 0) {
376
+ this.currentBg = targetBg;
377
+ this.backgroundColor = targetBg;
378
+ return;
379
+ }
380
+
381
+ const startBg = this.currentBg;
382
+
383
+ this.visualTimeline = createTimeline({ autoplay: false });
384
+ this.visualTimeline.add(this, {
385
+ duration,
386
+ onUpdate: (anim) => {
387
+ if (startBg !== "transparent" && targetBg !== "transparent") {
388
+ this.currentBg = interpolateColor(startBg, targetBg, anim.progress);
389
+ } else {
390
+ this.currentBg = anim.progress > 0.5 ? targetBg : startBg;
391
+ }
392
+ this.backgroundColor = this.currentBg;
393
+ },
394
+ onComplete: () => {
395
+ this.currentBg = targetBg;
396
+ this.backgroundColor = targetBg;
397
+ this.visualTimeline = null;
398
+ },
399
+ });
400
+ this.visualTimeline.play();
401
+ }
402
+
403
+ public focus() {
404
+ this.isFocused = true;
405
+ this.isHovered = false;
406
+ this.updateVisuals(200);
407
+ }
408
+
409
+ public blur() {
410
+ this.isFocused = false;
411
+ this.updateVisuals(200);
412
+ }
413
+
414
+ public resetHover() {
415
+ this.isHovered = false;
416
+ this.updateVisuals();
417
+ }
418
+
419
+ public setSelected(selected: boolean) {
420
+ this.isSelected = selected;
421
+ this.updateVisuals(200);
422
+ }
423
+
424
+ update(session: SessionData) {
425
+ this.session = session;
426
+
427
+ // Update all lines
428
+ this.headerLine.content = this.buildHeaderContent();
429
+
430
+ // Determine status message based on session state
431
+ const statusLower = session.status.toLowerCase();
432
+ let statusMessage = "Ready.";
433
+ let infoMessage = "Waiting...";
434
+
435
+ if (session.gitStatus) {
436
+ const gs = session.gitStatus;
437
+ if (gs.ahead > 0 && gs.isClean) {
438
+ statusMessage = `Branch is ahead by ${gs.ahead} commit${gs.ahead > 1 ? "s" : ""}, working tree clean.`;
439
+ } else if (gs.ahead > 0) {
440
+ statusMessage = `Branch is ahead by ${gs.ahead} commit${gs.ahead > 1 ? "s" : ""}, ${gs.modified} file${gs.modified > 1 ? "s" : ""} modified.`;
441
+ } else if (gs.isClean) {
442
+ statusMessage = "Working tree clean.";
443
+ } else {
444
+ statusMessage = `${gs.modified} file${gs.modified > 1 ? "s" : ""} modified.`;
445
+ }
446
+ }
447
+
448
+ if (statusLower.includes("initializing")) {
449
+ infoMessage = "Initializing model...";
450
+ } else if (statusLower.includes("error")) {
451
+ statusMessage = session.status.replace(/\s+/g, " ").trim();
452
+ infoMessage = "Error occurred";
453
+ } else if (statusLower.includes("done")) {
454
+ infoMessage = "Task completed successfully.";
455
+ } else {
456
+ infoMessage = "Processing...";
457
+ }
458
+
459
+ this.statusLine.content = this.buildStatusContent(statusMessage);
460
+ this.infoLine.content = this.buildInfoContent(infoMessage);
461
+ this.runLine.content = this.buildRunContent();
462
+ this.doneLine.content = this.buildDoneContent();
463
+
464
+ // Update button visibility
465
+ const completedLike = statusLower.includes("done") || statusLower.includes("task completed");
466
+ const isFinished = !isSessionActive(session.status) || completedLike;
467
+ const isDone = completedLike || statusLower === "done";
468
+ const hasWorktree = !!session.worktreeInfo;
469
+
470
+ // Show cancel button only when session is active
471
+ this.cancelButton.visible = !isFinished;
472
+
473
+ // Show review button when session is done and has worktree info
474
+ this.reviewButton.visible = isDone && hasWorktree;
475
+
476
+ // Hide cancel button if review button is shown
477
+ if (this.reviewButton.visible) {
478
+ this.cancelButton.visible = false;
479
+ }
480
+ }
481
+ }
@@ -0,0 +1,36 @@
1
+ import { mock, expect, test, describe, beforeEach, spyOn } from "bun:test";
2
+ import "../test-setup.js";
3
+ import type { CliRenderer } from "@opentui/core";
4
+ import { ToyboxSidebar } from "./ToyboxSidebar.ts";
5
+
6
+ // Note: Due to complexities with Bun's mock.module and class instantiation,
7
+ // we test only the basic structural aspects that work with the mock system.
8
+
9
+ describe("ToyboxSidebar", () => {
10
+ let mockRenderer: CliRenderer;
11
+
12
+ beforeEach(() => {
13
+ mockRenderer = { requestRender: mock(() => {}) } as any;
14
+ });
15
+
16
+ test("should initialize", () => {
17
+ const sidebar = new ToyboxSidebar(mockRenderer);
18
+ expect(sidebar).toBeDefined();
19
+ expect(sidebar.root).toBeDefined();
20
+ });
21
+
22
+ test("should have isOpen return false initially", () => {
23
+ const sidebar = new ToyboxSidebar(mockRenderer);
24
+ expect(sidebar.isOpen()).toBe(false);
25
+ });
26
+
27
+ test.skip("should have destroy method", () => {
28
+ // Skipped: Method access issues with mock classes
29
+ });
30
+
31
+ test("should have hide and show methods", () => {
32
+ const sidebar = new ToyboxSidebar(mockRenderer);
33
+ expect(typeof sidebar.hide).toBe("function");
34
+ expect(typeof sidebar.show).toBe("function");
35
+ });
36
+ });