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,399 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TextRenderable,
|
|
3
|
+
TextAttributes,
|
|
4
|
+
ScrollBoxRenderable,
|
|
5
|
+
BoxRenderable,
|
|
6
|
+
CliRenderer,
|
|
7
|
+
} from "@opentui/core";
|
|
8
|
+
import { THEME } from "../theme.js";
|
|
9
|
+
import { Dialog } from "./Dialog.js";
|
|
10
|
+
import { SessionData } from "../../types/tasks.js";
|
|
11
|
+
import { LogView } from "../views/LogView.js";
|
|
12
|
+
import { formatDuration, Clipboard } from "../../utils/index.js";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
|
|
15
|
+
export class DashboardDialog {
|
|
16
|
+
private dialog: Dialog;
|
|
17
|
+
private renderer: CliRenderer;
|
|
18
|
+
private session: SessionData | null = null;
|
|
19
|
+
private logView: LogView | null = null;
|
|
20
|
+
private currentSessionId: string | null = null;
|
|
21
|
+
|
|
22
|
+
private promptText!: TextRenderable;
|
|
23
|
+
private timeText!: TextRenderable;
|
|
24
|
+
private statusText!: TextRenderable;
|
|
25
|
+
private sessionMetaText!: TextRenderable;
|
|
26
|
+
private logContainer!: BoxRenderable;
|
|
27
|
+
private logScrollContainer!: BoxRenderable;
|
|
28
|
+
private logHeaderRight!: TextRenderable;
|
|
29
|
+
private ticker: ReturnType<typeof setInterval> | null = null;
|
|
30
|
+
|
|
31
|
+
constructor(renderer: CliRenderer) {
|
|
32
|
+
this.renderer = renderer;
|
|
33
|
+
this.dialog = new Dialog(renderer, "Session Dashboard");
|
|
34
|
+
|
|
35
|
+
this.setupContent();
|
|
36
|
+
|
|
37
|
+
// Initialize with a message to ensure content is visible
|
|
38
|
+
this.promptText.content = "No session selected";
|
|
39
|
+
this.timeText.content = "--:--s";
|
|
40
|
+
this.statusText.content = "Idle";
|
|
41
|
+
this.statusText.fg = THEME.dim;
|
|
42
|
+
this.sessionMetaText.content = "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private setupContent() {
|
|
46
|
+
const childIds = this.dialog.content.getChildren().map(c => c.id);
|
|
47
|
+
childIds.forEach(id => this.dialog.content.remove(id));
|
|
48
|
+
|
|
49
|
+
const sessionCard = new BoxRenderable(this.renderer, {
|
|
50
|
+
id: "dashboard-session-card",
|
|
51
|
+
width: "100%",
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
marginBottom: 1,
|
|
54
|
+
backgroundColor: THEME.surface,
|
|
55
|
+
border: true,
|
|
56
|
+
borderColor: THEME.darkAccent,
|
|
57
|
+
paddingLeft: 1,
|
|
58
|
+
paddingRight: 1,
|
|
59
|
+
paddingTop: 1,
|
|
60
|
+
paddingBottom: 2,
|
|
61
|
+
gap: 1,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const headerRow = new BoxRenderable(this.renderer, {
|
|
65
|
+
id: "dashboard-session-header",
|
|
66
|
+
width: "100%",
|
|
67
|
+
flexDirection: "row",
|
|
68
|
+
justifyContent: "space-between",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
flexWrap: "wrap",
|
|
71
|
+
marginBottom: 1,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const headerTitle = new TextRenderable(this.renderer, {
|
|
75
|
+
id: "dashboard-session-title",
|
|
76
|
+
content: "Session",
|
|
77
|
+
fg: THEME.accent,
|
|
78
|
+
attributes: TextAttributes.BOLD,
|
|
79
|
+
});
|
|
80
|
+
headerRow.add(headerTitle);
|
|
81
|
+
|
|
82
|
+
this.sessionMetaText = new TextRenderable(this.renderer, {
|
|
83
|
+
id: "dashboard-session-meta",
|
|
84
|
+
content: "",
|
|
85
|
+
fg: THEME.dim,
|
|
86
|
+
wrapMode: "word",
|
|
87
|
+
});
|
|
88
|
+
headerRow.add(this.sessionMetaText);
|
|
89
|
+
|
|
90
|
+
sessionCard.add(headerRow);
|
|
91
|
+
|
|
92
|
+
const promptLabel = new TextRenderable(this.renderer, {
|
|
93
|
+
id: "dashboard-prompt-label",
|
|
94
|
+
content: "Prompt",
|
|
95
|
+
fg: THEME.dim,
|
|
96
|
+
attributes: TextAttributes.BOLD,
|
|
97
|
+
marginBottom: 0,
|
|
98
|
+
});
|
|
99
|
+
sessionCard.add(promptLabel);
|
|
100
|
+
|
|
101
|
+
this.promptText = new TextRenderable(this.renderer, {
|
|
102
|
+
id: "dashboard-prompt",
|
|
103
|
+
content: "No session selected",
|
|
104
|
+
fg: THEME.white,
|
|
105
|
+
marginBottom: 1,
|
|
106
|
+
width: "100%",
|
|
107
|
+
wrapMode: "word",
|
|
108
|
+
});
|
|
109
|
+
sessionCard.add(this.promptText);
|
|
110
|
+
|
|
111
|
+
// Status and elapsed on separate lines for consistent alignment
|
|
112
|
+
const statusRow = new BoxRenderable(this.renderer, {
|
|
113
|
+
id: "dashboard-status-row",
|
|
114
|
+
width: "100%",
|
|
115
|
+
flexDirection: "row",
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
gap: 1,
|
|
118
|
+
marginBottom: 1,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const statusLabel = new TextRenderable(this.renderer, {
|
|
122
|
+
id: "dashboard-status-label",
|
|
123
|
+
content: "Status:",
|
|
124
|
+
fg: THEME.dim,
|
|
125
|
+
});
|
|
126
|
+
statusRow.add(statusLabel);
|
|
127
|
+
|
|
128
|
+
const statusPill = new BoxRenderable(this.renderer, {
|
|
129
|
+
id: "dashboard-status-pill",
|
|
130
|
+
backgroundColor: THEME.darkAccent,
|
|
131
|
+
paddingLeft: 1,
|
|
132
|
+
paddingRight: 1,
|
|
133
|
+
flexShrink: 0,
|
|
134
|
+
maxWidth: "80%",
|
|
135
|
+
});
|
|
136
|
+
this.statusText = new TextRenderable(this.renderer, {
|
|
137
|
+
id: "dashboard-status-text",
|
|
138
|
+
content: "",
|
|
139
|
+
fg: THEME.accent,
|
|
140
|
+
attributes: TextAttributes.BOLD,
|
|
141
|
+
wrapMode: "none",
|
|
142
|
+
truncate: true,
|
|
143
|
+
width: 32,
|
|
144
|
+
});
|
|
145
|
+
statusPill.add(this.statusText);
|
|
146
|
+
statusRow.add(statusPill);
|
|
147
|
+
sessionCard.add(statusRow);
|
|
148
|
+
|
|
149
|
+
const timeRow = new BoxRenderable(this.renderer, {
|
|
150
|
+
id: "dashboard-time-row",
|
|
151
|
+
width: "100%",
|
|
152
|
+
flexDirection: "row",
|
|
153
|
+
alignItems: "center",
|
|
154
|
+
gap: 1,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const timeLabel = new TextRenderable(this.renderer, {
|
|
158
|
+
id: "dashboard-time-label",
|
|
159
|
+
content: "Elapsed:",
|
|
160
|
+
fg: THEME.dim,
|
|
161
|
+
});
|
|
162
|
+
timeRow.add(timeLabel);
|
|
163
|
+
|
|
164
|
+
this.timeText = new TextRenderable(this.renderer, {
|
|
165
|
+
id: "dashboard-time",
|
|
166
|
+
content: "",
|
|
167
|
+
fg: THEME.text,
|
|
168
|
+
wrapMode: "none",
|
|
169
|
+
truncate: true,
|
|
170
|
+
});
|
|
171
|
+
timeRow.add(this.timeText);
|
|
172
|
+
|
|
173
|
+
sessionCard.add(timeRow);
|
|
174
|
+
|
|
175
|
+
this.dialog.content.add(sessionCard);
|
|
176
|
+
|
|
177
|
+
this.logContainer = new BoxRenderable(this.renderer, {
|
|
178
|
+
id: "dashboard-log-container",
|
|
179
|
+
width: "100%",
|
|
180
|
+
flexGrow: 1,
|
|
181
|
+
flexDirection: "column",
|
|
182
|
+
backgroundColor: THEME.surface,
|
|
183
|
+
border: true,
|
|
184
|
+
borderColor: THEME.darkAccent,
|
|
185
|
+
marginTop: 1,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const logHeaderBar = new BoxRenderable(this.renderer, {
|
|
189
|
+
id: "dashboard-log-header",
|
|
190
|
+
width: "100%",
|
|
191
|
+
height: 2,
|
|
192
|
+
flexDirection: "row",
|
|
193
|
+
justifyContent: "space-between",
|
|
194
|
+
alignItems: "center",
|
|
195
|
+
paddingLeft: 1,
|
|
196
|
+
paddingRight: 1,
|
|
197
|
+
backgroundColor: THEME.surface,
|
|
198
|
+
border: ["bottom"],
|
|
199
|
+
borderColor: THEME.darkAccent,
|
|
200
|
+
flexShrink: 0,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const logHeaderTitle = new TextRenderable(this.renderer, {
|
|
204
|
+
id: "dashboard-log-title",
|
|
205
|
+
content: "Log Output",
|
|
206
|
+
fg: THEME.accent,
|
|
207
|
+
attributes: TextAttributes.BOLD,
|
|
208
|
+
});
|
|
209
|
+
logHeaderBar.add(logHeaderTitle);
|
|
210
|
+
|
|
211
|
+
this.logHeaderRight = new TextRenderable(this.renderer, {
|
|
212
|
+
id: "dashboard-log-meta",
|
|
213
|
+
content: "auto-follow",
|
|
214
|
+
fg: THEME.dim,
|
|
215
|
+
});
|
|
216
|
+
logHeaderBar.add(this.logHeaderRight);
|
|
217
|
+
|
|
218
|
+
this.logContainer.add(logHeaderBar);
|
|
219
|
+
|
|
220
|
+
this.logScrollContainer = new BoxRenderable(this.renderer, {
|
|
221
|
+
id: "dashboard-log-scroll",
|
|
222
|
+
width: "100%",
|
|
223
|
+
flexGrow: 1,
|
|
224
|
+
flexDirection: "column",
|
|
225
|
+
backgroundColor: THEME.bg,
|
|
226
|
+
paddingLeft: 1,
|
|
227
|
+
paddingRight: 1,
|
|
228
|
+
paddingTop: 0,
|
|
229
|
+
overflow: "hidden", // explicitly hide scrollbars
|
|
230
|
+
});
|
|
231
|
+
this.logContainer.add(this.logScrollContainer);
|
|
232
|
+
|
|
233
|
+
this.dialog.content.add(this.logContainer);
|
|
234
|
+
|
|
235
|
+
this.dialog.setOptions([
|
|
236
|
+
{
|
|
237
|
+
title: "Copy",
|
|
238
|
+
value: "dialog.copy",
|
|
239
|
+
description: "logs to clipboard",
|
|
240
|
+
onSelect: async (dialog) => {
|
|
241
|
+
if (this.session) {
|
|
242
|
+
try {
|
|
243
|
+
const logContent = await this.getLogContent();
|
|
244
|
+
await Clipboard.copy(logContent);
|
|
245
|
+
this.logHeaderRight.content = "copied ✓";
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
if (this.session) {
|
|
248
|
+
this.logHeaderRight.content = `session ${this.formatSessionId(this.session.id)}`;
|
|
249
|
+
this.renderer.requestRender();
|
|
250
|
+
}
|
|
251
|
+
}, 1500);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error("Failed to copy logs:", error);
|
|
254
|
+
this.logHeaderRight.content = "copy failed";
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
if (this.session) {
|
|
257
|
+
this.logHeaderRight.content = `session ${this.formatSessionId(this.session.id)}`;
|
|
258
|
+
this.renderer.requestRender();
|
|
259
|
+
}
|
|
260
|
+
}, 2000);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.renderer.requestRender();
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public update(session: SessionData) {
|
|
270
|
+
this.session = session;
|
|
271
|
+
this.startTicker();
|
|
272
|
+
this.refresh();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private refresh() {
|
|
276
|
+
if (!this.session) return;
|
|
277
|
+
|
|
278
|
+
this.promptText.content = this.session.prompt;
|
|
279
|
+
const durationMs = Date.now() - this.session.startTime;
|
|
280
|
+
this.timeText.content = formatDuration(durationMs);
|
|
281
|
+
this.statusText.content = this.formatStatusLabel(this.session.status);
|
|
282
|
+
this.statusText.fg = this.getStatusColor(this.session.status);
|
|
283
|
+
|
|
284
|
+
const sessionId = this.formatSessionId(this.session.id);
|
|
285
|
+
const mode = this.session.isPrdMode ? "PRD" : "Run";
|
|
286
|
+
this.sessionMetaText.content = `${this.session.engine} • ${mode} • ${sessionId}`;
|
|
287
|
+
this.logHeaderRight.content = sessionId ? `session ${sessionId}` : "auto-follow";
|
|
288
|
+
|
|
289
|
+
// Always rebuild log view on update to avoid stale tails and ensure fresh content
|
|
290
|
+
if (this.logView) {
|
|
291
|
+
const existing = this.logScrollContainer?.getChildren() || [];
|
|
292
|
+
existing.forEach(child => this.logScrollContainer?.remove(child.id));
|
|
293
|
+
this.logView.destroy();
|
|
294
|
+
this.logView = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.logView = new LogView(this.renderer, `${this.session.id}/session.log`, () => {
|
|
298
|
+
this.renderer.requestRender();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (this.logScrollContainer) {
|
|
302
|
+
this.logScrollContainer.add(this.logView.root);
|
|
303
|
+
}
|
|
304
|
+
this.currentSessionId = this.session.id;
|
|
305
|
+
|
|
306
|
+
this.renderer.requestRender();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private formatSessionId(sessionId: string): string {
|
|
310
|
+
const segments = sessionId.split("/").filter(Boolean);
|
|
311
|
+
return segments[segments.length - 1] ?? sessionId;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private formatStatusLabel(status: string): string {
|
|
315
|
+
const trimmed = status?.trim() || "Initializing";
|
|
316
|
+
if (trimmed.length <= 32) return trimmed;
|
|
317
|
+
return `${trimmed.slice(0, 29)}...`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private getStatusColor(status: string): string {
|
|
321
|
+
const normalized = status.toLowerCase();
|
|
322
|
+
if (normalized.includes("error") || normalized.includes("failed")) return "#ff5252";
|
|
323
|
+
if (normalized.includes("cancelled") || normalized.includes("canceled")) return "#ff5252";
|
|
324
|
+
if (normalized.includes("done") || normalized.includes("success")) return THEME.green;
|
|
325
|
+
if (normalized.includes("iteration") || normalized.includes("running")) return THEME.accent;
|
|
326
|
+
return THEME.text;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async getLogContent(): Promise<string> {
|
|
330
|
+
if (!this.session) return "";
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const logPath = `${this.session.id}/session.log`;
|
|
334
|
+
console.log("Attempting to read log file:", logPath);
|
|
335
|
+
const content = await readFile(logPath, "utf-8");
|
|
336
|
+
console.log("Successfully read log file, length:", content.length);
|
|
337
|
+
return content;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error("Failed to read log file:", error);
|
|
340
|
+
return `Failed to read log content: ${error instanceof Error ? error.message : String(error)}`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
public show() {
|
|
345
|
+
if (this.session) {
|
|
346
|
+
this.refresh();
|
|
347
|
+
}
|
|
348
|
+
this.dialog.show();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public hide() {
|
|
352
|
+
this.dialog.hide();
|
|
353
|
+
this.session = null;
|
|
354
|
+
this.currentSessionId = null;
|
|
355
|
+
this.stopTicker();
|
|
356
|
+
if (this.logView) {
|
|
357
|
+
const existing = this.logScrollContainer?.getChildren() || [];
|
|
358
|
+
existing.forEach(child => this.logScrollContainer?.remove(child.id));
|
|
359
|
+
this.logView.destroy();
|
|
360
|
+
this.logView = null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public isOpen(): boolean {
|
|
365
|
+
return this.dialog.isOpen();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public get root() {
|
|
369
|
+
return this.dialog.root;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
public destroy() {
|
|
373
|
+
this.stopTicker();
|
|
374
|
+
if (this.logView) {
|
|
375
|
+
this.logView.destroy();
|
|
376
|
+
}
|
|
377
|
+
if (this.logScrollContainer) {
|
|
378
|
+
const children = this.logScrollContainer.getChildren();
|
|
379
|
+
children.forEach(child => this.logScrollContainer.remove(child.id));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private startTicker() {
|
|
384
|
+
if (this.ticker) return;
|
|
385
|
+
this.ticker = setInterval(() => {
|
|
386
|
+
if (!this.session) return;
|
|
387
|
+
const durationMs = Date.now() - this.session.startTime;
|
|
388
|
+
this.timeText.content = formatDuration(durationMs);
|
|
389
|
+
this.renderer.requestRender();
|
|
390
|
+
}, 1000);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private stopTicker() {
|
|
394
|
+
if (this.ticker) {
|
|
395
|
+
clearInterval(this.ticker);
|
|
396
|
+
this.ticker = null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mock, expect, test, describe, beforeEach } from "bun:test";
|
|
2
|
+
import { createMockRenderer, type MockRenderer } 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
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("Dialog", () => {
|
|
17
|
+
let mockRenderer: MockRenderer;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockRenderer = createMockRenderer();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("should initialize with title", async () => {
|
|
24
|
+
const { Dialog } = await import("./Dialog.ts");
|
|
25
|
+
const dialog = new Dialog(mockRenderer as unknown as CliRenderer, "Test Title");
|
|
26
|
+
expect(dialog).toBeDefined();
|
|
27
|
+
expect(dialog.isOpen()).toBe(false);
|
|
28
|
+
expect(dialog.root).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("should show and hide", async () => {
|
|
32
|
+
const { Dialog } = await import("./Dialog.ts");
|
|
33
|
+
const dialog = new Dialog(mockRenderer as unknown as CliRenderer, "Test Title");
|
|
34
|
+
|
|
35
|
+
dialog.show();
|
|
36
|
+
expect(dialog.isOpen()).toBe(true);
|
|
37
|
+
expect(dialog.root.visible).toBe(true);
|
|
38
|
+
|
|
39
|
+
dialog.hide();
|
|
40
|
+
expect(dialog.isOpen()).toBe(false);
|
|
41
|
+
expect(dialog.root.visible).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should have setOptions method", async () => {
|
|
45
|
+
const { Dialog } = await import("./Dialog.ts");
|
|
46
|
+
const dialog = new Dialog(mockRenderer as unknown as CliRenderer, "Test Title");
|
|
47
|
+
|
|
48
|
+
expect(typeof dialog.setOptions).toBe("function");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
TextRenderable,
|
|
4
|
+
TextAttributes,
|
|
5
|
+
CliRenderer,
|
|
6
|
+
MouseEvent,
|
|
7
|
+
createTimeline,
|
|
8
|
+
RGBA,
|
|
9
|
+
} from "@opentui/core";
|
|
10
|
+
import { THEME } from "../theme.js";
|
|
11
|
+
|
|
12
|
+
export interface DialogOption {
|
|
13
|
+
title: string;
|
|
14
|
+
value: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
onSelect: (dialog: Dialog) => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Dialog {
|
|
20
|
+
public root: BoxRenderable;
|
|
21
|
+
private overlay: BoxRenderable;
|
|
22
|
+
private dialogPanel: BoxRenderable;
|
|
23
|
+
private renderer: CliRenderer;
|
|
24
|
+
private isVisible = false;
|
|
25
|
+
private onShow?: () => void;
|
|
26
|
+
private onHide?: () => void;
|
|
27
|
+
|
|
28
|
+
private titleText: TextRenderable;
|
|
29
|
+
public content: BoxRenderable;
|
|
30
|
+
private optionsContainer: BoxRenderable;
|
|
31
|
+
private options: DialogOption[] = [];
|
|
32
|
+
|
|
33
|
+
constructor(renderer: CliRenderer, title: string) {
|
|
34
|
+
this.renderer = renderer;
|
|
35
|
+
|
|
36
|
+
// Background overlay
|
|
37
|
+
this.overlay = new BoxRenderable(renderer, {
|
|
38
|
+
id: "dialog-overlay",
|
|
39
|
+
width: "100%",
|
|
40
|
+
height: "100%",
|
|
41
|
+
position: "absolute",
|
|
42
|
+
left: 0,
|
|
43
|
+
top: 0,
|
|
44
|
+
backgroundColor: RGBA.fromInts(0, 0, 0, 175), // Increased opacity by 10% (150 -> 175)
|
|
45
|
+
visible: false,
|
|
46
|
+
opacity: 0,
|
|
47
|
+
zIndex: 20000,
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Dialog panel
|
|
53
|
+
this.dialogPanel = new BoxRenderable(renderer, {
|
|
54
|
+
id: "dialog-panel",
|
|
55
|
+
width: 90,
|
|
56
|
+
maxWidth: "92%",
|
|
57
|
+
height: "70%", // Slightly taller for consistent centering
|
|
58
|
+
maxHeight: "80%",
|
|
59
|
+
flexDirection: "column",
|
|
60
|
+
backgroundColor: THEME.bg,
|
|
61
|
+
paddingLeft: 1,
|
|
62
|
+
paddingRight: 1,
|
|
63
|
+
paddingTop: 1,
|
|
64
|
+
paddingBottom: 1,
|
|
65
|
+
zIndex: 20001, // Higher than overlay to ensure clickability
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.overlay.add(this.dialogPanel);
|
|
69
|
+
this.root = this.overlay;
|
|
70
|
+
|
|
71
|
+
this.titleText = new TextRenderable(renderer, {
|
|
72
|
+
id: "dialog-title",
|
|
73
|
+
content: title,
|
|
74
|
+
fg: THEME.white,
|
|
75
|
+
attributes: TextAttributes.BOLD,
|
|
76
|
+
marginBottom: 1,
|
|
77
|
+
width: "100%",
|
|
78
|
+
});
|
|
79
|
+
this.dialogPanel.add(this.titleText);
|
|
80
|
+
|
|
81
|
+
// Create a main container that will hold content and options
|
|
82
|
+
const mainContainer = new BoxRenderable(renderer, {
|
|
83
|
+
id: "dialog-main-container",
|
|
84
|
+
width: "100%",
|
|
85
|
+
flexGrow: 1,
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.content = new BoxRenderable(renderer, {
|
|
90
|
+
id: "dialog-content",
|
|
91
|
+
width: "100%",
|
|
92
|
+
flexGrow: 1,
|
|
93
|
+
flexDirection: "column",
|
|
94
|
+
backgroundColor: THEME.bg,
|
|
95
|
+
paddingLeft: 1,
|
|
96
|
+
paddingRight: 1,
|
|
97
|
+
});
|
|
98
|
+
mainContainer.add(this.content);
|
|
99
|
+
|
|
100
|
+
this.optionsContainer = new BoxRenderable(renderer, {
|
|
101
|
+
id: "dialog-options",
|
|
102
|
+
width: "100%",
|
|
103
|
+
flexDirection: "column",
|
|
104
|
+
marginTop: 1,
|
|
105
|
+
paddingTop: 1,
|
|
106
|
+
flexShrink: 0, // Prevent options from shrinking
|
|
107
|
+
});
|
|
108
|
+
mainContainer.add(this.optionsContainer);
|
|
109
|
+
|
|
110
|
+
this.dialogPanel.add(mainContainer);
|
|
111
|
+
|
|
112
|
+
this.setupCloseButton();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private setupCloseButton() {
|
|
116
|
+
const closeButton = new BoxRenderable(this.renderer, {
|
|
117
|
+
id: "dialog-close",
|
|
118
|
+
width: 5,
|
|
119
|
+
height: 1,
|
|
120
|
+
position: "absolute",
|
|
121
|
+
right: 1,
|
|
122
|
+
top: 1,
|
|
123
|
+
zIndex: 20001,
|
|
124
|
+
justifyContent: "center",
|
|
125
|
+
alignItems: "center",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const closeText = new TextRenderable(this.renderer, {
|
|
129
|
+
id: "dialog-close-text",
|
|
130
|
+
content: "[X]",
|
|
131
|
+
fg: THEME.dim,
|
|
132
|
+
});
|
|
133
|
+
closeButton.add(closeText);
|
|
134
|
+
|
|
135
|
+
closeButton.onMouse = (e: MouseEvent) => {
|
|
136
|
+
if ((e.type as any) === "click" || e.type === "up") {
|
|
137
|
+
this.hide();
|
|
138
|
+
} else if (e.type === "over") {
|
|
139
|
+
closeText.fg = THEME.accent;
|
|
140
|
+
this.renderer.requestRender();
|
|
141
|
+
} else if (e.type === "out") {
|
|
142
|
+
closeText.fg = THEME.dim;
|
|
143
|
+
this.renderer.requestRender();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
this.dialogPanel.add(closeButton);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public setOptions(options: DialogOption[]) {
|
|
151
|
+
this.options = options;
|
|
152
|
+
const childIds = this.optionsContainer.getChildren().map(c => c.id);
|
|
153
|
+
childIds.forEach(id => this.optionsContainer.remove(id));
|
|
154
|
+
|
|
155
|
+
options.forEach((option, index) => {
|
|
156
|
+
const optionRow = new BoxRenderable(this.renderer, {
|
|
157
|
+
id: `dialog-option-${index}`,
|
|
158
|
+
width: "100%",
|
|
159
|
+
flexDirection: "row",
|
|
160
|
+
gap: 1,
|
|
161
|
+
marginBottom: 1,
|
|
162
|
+
paddingLeft: 1,
|
|
163
|
+
paddingRight: 1,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Title with accent color on hover (matching OpenCode's primary theme color)
|
|
167
|
+
const titleText = new TextRenderable(this.renderer, {
|
|
168
|
+
id: `dialog-option-title-${index}`,
|
|
169
|
+
content: option.title,
|
|
170
|
+
fg: THEME.accent,
|
|
171
|
+
attributes: TextAttributes.BOLD,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Description in muted color
|
|
175
|
+
const descText = new TextRenderable(this.renderer, {
|
|
176
|
+
id: `dialog-option-desc-${index}`,
|
|
177
|
+
content: option.description || "",
|
|
178
|
+
fg: THEME.dim,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
optionRow.add(titleText);
|
|
182
|
+
if (option.description) {
|
|
183
|
+
optionRow.add(descText);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
optionRow.onMouse = (e: MouseEvent) => {
|
|
187
|
+
if ((e.type as any) === "click" || e.type === "up") {
|
|
188
|
+
option.onSelect(this);
|
|
189
|
+
} else if (e.type === "over") {
|
|
190
|
+
titleText.fg = THEME.accent;
|
|
191
|
+
optionRow.backgroundColor = THEME.darkAccent;
|
|
192
|
+
this.renderer.requestRender();
|
|
193
|
+
} else if (e.type === "out") {
|
|
194
|
+
titleText.fg = THEME.accent;
|
|
195
|
+
optionRow.backgroundColor = undefined;
|
|
196
|
+
this.renderer.requestRender();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
this.optionsContainer.add(optionRow);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public show(onShow?: () => void, onHide?: () => void) {
|
|
205
|
+
if (this.isVisible) return;
|
|
206
|
+
|
|
207
|
+
this.isVisible = true;
|
|
208
|
+
this.onShow = onShow;
|
|
209
|
+
this.onHide = onHide;
|
|
210
|
+
this.overlay.visible = true;
|
|
211
|
+
this.overlay.opacity = 0;
|
|
212
|
+
|
|
213
|
+
createTimeline().add(this.overlay, {
|
|
214
|
+
opacity: 1,
|
|
215
|
+
duration: 300,
|
|
216
|
+
ease: "outQuad",
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.onShow?.();
|
|
220
|
+
this.renderer.requestRender();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public hide() {
|
|
224
|
+
if (!this.isVisible) return;
|
|
225
|
+
this.isVisible = false;
|
|
226
|
+
this.overlay.visible = false;
|
|
227
|
+
this.onHide?.();
|
|
228
|
+
this.renderer.requestRender();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public clear() {
|
|
232
|
+
this.hide();
|
|
233
|
+
this.options = [];
|
|
234
|
+
const childIds = this.optionsContainer.getChildren().map(c => c.id);
|
|
235
|
+
childIds.forEach(id => this.optionsContainer.remove(id));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public isOpen(): boolean {
|
|
239
|
+
return this.isVisible;
|
|
240
|
+
}
|
|
241
|
+
}
|