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