create-interview-cockpit 0.18.0 → 0.19.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/package.json +1 -1
- package/template/client/src/api.ts +101 -0
- package/template/client/src/components/GhaHistoryPanel.tsx +194 -0
- package/template/client/src/components/GhaJobsPanel.tsx +432 -0
- package/template/client/src/components/GithubActionsLabModal.tsx +376 -74
- package/template/client/src/components/Sidebar.tsx +216 -59
- package/template/client/src/githubActionsLab.ts +7 -0
- package/template/client/src/store.ts +47 -0
- package/template/client/src/types.ts +6 -0
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/gha-runner.ts +325 -0
- package/template/server/src/google-drive.ts +507 -125
- package/template/server/src/index.ts +87 -1
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
Loader2,
|
|
8
8
|
Maximize2,
|
|
9
9
|
Minimize2,
|
|
10
|
+
PanelLeftClose,
|
|
11
|
+
PanelLeftOpen,
|
|
12
|
+
PanelRightClose,
|
|
13
|
+
PanelRightOpen,
|
|
10
14
|
Play,
|
|
11
15
|
Save,
|
|
12
16
|
StopCircle,
|
|
@@ -26,7 +30,9 @@ import {
|
|
|
26
30
|
} from "../githubActionsLab";
|
|
27
31
|
import type { GithubActionsLabWorkspace } from "../types";
|
|
28
32
|
import * as api from "../api";
|
|
29
|
-
import type { GhaStreamMessage } from "../api";
|
|
33
|
+
import type { GhaJobSnapshot, GhaStreamMessage } from "../api";
|
|
34
|
+
import GhaJobsPanel from "./GhaJobsPanel";
|
|
35
|
+
import GhaHistoryPanel from "./GhaHistoryPanel";
|
|
30
36
|
|
|
31
37
|
// ─── Modal layout constants ──────────────────────────────────────────────
|
|
32
38
|
|
|
@@ -36,6 +42,7 @@ const DEFAULT_W = Math.min(1280, window.innerWidth - 48);
|
|
|
36
42
|
const DEFAULT_H = Math.min(820, window.innerHeight - 48);
|
|
37
43
|
const EDITOR_FONT =
|
|
38
44
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
45
|
+
type ResizeDir = "e" | "s" | "se" | "sw" | "w" | "ne" | "nw" | "n";
|
|
39
46
|
|
|
40
47
|
const EVENTS = [
|
|
41
48
|
"push",
|
|
@@ -135,6 +142,17 @@ export default function GithubActionsLabModal() {
|
|
|
135
142
|
const [running, setRunning] = useState(false);
|
|
136
143
|
const [consoleLines, setConsoleLines] = useState<ConsoleLine[]>([]);
|
|
137
144
|
const [runError, setRunError] = useState<string | null>(null);
|
|
145
|
+
// Live job snapshots reported by the server during the active run.
|
|
146
|
+
// Reset every time the user kicks off a new run.
|
|
147
|
+
const [liveJobs, setLiveJobs] = useState<GhaJobSnapshot[]>([]);
|
|
148
|
+
// "console" | "jobs" | "history" — controls the right pane tab.
|
|
149
|
+
const [rightTab, setRightTab] = useState<"console" | "jobs" | "history">(
|
|
150
|
+
"console",
|
|
151
|
+
);
|
|
152
|
+
// Bumped each time a run completes so the History tab refetches.
|
|
153
|
+
const [historyNonce, setHistoryNonce] = useState(0);
|
|
154
|
+
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
155
|
+
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
138
156
|
const abortRef = useRef<AbortController | null>(null);
|
|
139
157
|
const consoleEndRef = useRef<HTMLDivElement | null>(null);
|
|
140
158
|
|
|
@@ -162,6 +180,14 @@ export default function GithubActionsLabModal() {
|
|
|
162
180
|
ox: number;
|
|
163
181
|
oy: number;
|
|
164
182
|
} | null>(null);
|
|
183
|
+
const resizeStart = useRef<{
|
|
184
|
+
mx: number;
|
|
185
|
+
my: number;
|
|
186
|
+
ox: number;
|
|
187
|
+
oy: number;
|
|
188
|
+
ow: number;
|
|
189
|
+
oh: number;
|
|
190
|
+
} | null>(null);
|
|
165
191
|
|
|
166
192
|
const onTitleMouseDown = useCallback(
|
|
167
193
|
(e: React.MouseEvent) => {
|
|
@@ -196,6 +222,64 @@ export default function GithubActionsLabModal() {
|
|
|
196
222
|
[pos.x, pos.y, maximized],
|
|
197
223
|
);
|
|
198
224
|
|
|
225
|
+
const startResize = useCallback(
|
|
226
|
+
(dir: ResizeDir) => (e: React.MouseEvent) => {
|
|
227
|
+
if (maximized) return;
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
resizeStart.current = {
|
|
231
|
+
mx: e.clientX,
|
|
232
|
+
my: e.clientY,
|
|
233
|
+
ox: pos.x,
|
|
234
|
+
oy: pos.y,
|
|
235
|
+
ow: size.w,
|
|
236
|
+
oh: size.h,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const originalCursor = document.body.style.cursor;
|
|
240
|
+
const originalUserSelect = document.body.style.userSelect;
|
|
241
|
+
document.body.style.cursor = `${dir}-resize`;
|
|
242
|
+
document.body.style.userSelect = "none";
|
|
243
|
+
|
|
244
|
+
const onMove = (ev: MouseEvent) => {
|
|
245
|
+
const resize = resizeStart.current;
|
|
246
|
+
if (!resize) return;
|
|
247
|
+
const dx = ev.clientX - resize.mx;
|
|
248
|
+
const dy = ev.clientY - resize.my;
|
|
249
|
+
let w = resize.ow;
|
|
250
|
+
let h = resize.oh;
|
|
251
|
+
let x = resize.ox;
|
|
252
|
+
let y = resize.oy;
|
|
253
|
+
|
|
254
|
+
if (dir.includes("e")) w = Math.max(MIN_W, resize.ow + dx);
|
|
255
|
+
if (dir.includes("s")) h = Math.max(MIN_H, resize.oh + dy);
|
|
256
|
+
if (dir.includes("w")) {
|
|
257
|
+
w = Math.max(MIN_W, resize.ow - dx);
|
|
258
|
+
x = Math.min(resize.ox + resize.ow - MIN_W, resize.ox + dx);
|
|
259
|
+
}
|
|
260
|
+
if (dir.includes("n")) {
|
|
261
|
+
h = Math.max(MIN_H, resize.oh - dy);
|
|
262
|
+
y = Math.min(resize.oy + resize.oh - MIN_H, resize.oy + dy);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
setSize({ w, h });
|
|
266
|
+
setPos({ x: Math.max(0, x), y: Math.max(0, y) });
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const onUp = () => {
|
|
270
|
+
resizeStart.current = null;
|
|
271
|
+
document.body.style.cursor = originalCursor;
|
|
272
|
+
document.body.style.userSelect = originalUserSelect;
|
|
273
|
+
window.removeEventListener("mousemove", onMove);
|
|
274
|
+
window.removeEventListener("mouseup", onUp);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
window.addEventListener("mousemove", onMove);
|
|
278
|
+
window.addEventListener("mouseup", onUp);
|
|
279
|
+
},
|
|
280
|
+
[maximized, pos.x, pos.y, size.w, size.h],
|
|
281
|
+
);
|
|
282
|
+
|
|
199
283
|
// ── File operations ───────────────────────────────────────────────
|
|
200
284
|
const fileOrder = useMemo(() => getGhaLabFileOrder(workspace), [workspace]);
|
|
201
285
|
const grouped = useMemo(() => groupByFolder(fileOrder), [fileOrder]);
|
|
@@ -258,13 +342,51 @@ export default function GithubActionsLabModal() {
|
|
|
258
342
|
if (!currentQuestion) return;
|
|
259
343
|
setSaving(true);
|
|
260
344
|
try {
|
|
261
|
-
|
|
345
|
+
// When the user opted in, embed a compact summary of the most recent
|
|
346
|
+
// runs into the saved JSON so the chat LLM has real execution data
|
|
347
|
+
// to reason about (job statuses, durations, exit codes).
|
|
348
|
+
let recentRunsExtras: Record<string, unknown> = {};
|
|
349
|
+
if (workspace.includeRunHistoryInContext) {
|
|
350
|
+
try {
|
|
351
|
+
const runs = await api.listGhaRuns({
|
|
352
|
+
questionId: currentQuestion.id,
|
|
353
|
+
...(activeGhaId ? { fileId: activeGhaId } : {}),
|
|
354
|
+
limit: 5,
|
|
355
|
+
});
|
|
356
|
+
recentRunsExtras = {
|
|
357
|
+
recentRuns: runs.map((r) => ({
|
|
358
|
+
command: r.command,
|
|
359
|
+
status: r.status,
|
|
360
|
+
exitCode: r.exitCode,
|
|
361
|
+
startedAt: r.startedAt,
|
|
362
|
+
durationMs: r.durationMs,
|
|
363
|
+
jobs: (r.jobs ?? []).map((j) => ({
|
|
364
|
+
name: j.name,
|
|
365
|
+
status: j.status,
|
|
366
|
+
durationMs: j.durationMs,
|
|
367
|
+
})),
|
|
368
|
+
})),
|
|
369
|
+
};
|
|
370
|
+
} catch {
|
|
371
|
+
// Non-fatal — just skip the appendix if the list call fails.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const serialized = serializeGhaLabWorkspace({
|
|
262
375
|
...workspace,
|
|
263
376
|
label: labName || workspace.label,
|
|
264
377
|
activeFile,
|
|
265
378
|
defaultEvent: event,
|
|
266
379
|
defaultWorkflow: workflow,
|
|
267
380
|
});
|
|
381
|
+
// Splice the extras into the serialised JSON without breaking the
|
|
382
|
+
// existing schema parsers tolerate.
|
|
383
|
+
const payload = Object.keys(recentRunsExtras).length
|
|
384
|
+
? JSON.stringify(
|
|
385
|
+
{ ...JSON.parse(serialized), ...recentRunsExtras },
|
|
386
|
+
null,
|
|
387
|
+
2,
|
|
388
|
+
)
|
|
389
|
+
: serialized;
|
|
268
390
|
if (activeGhaId) {
|
|
269
391
|
await overwriteContextFileContent(
|
|
270
392
|
currentQuestion.id,
|
|
@@ -322,6 +444,11 @@ export default function GithubActionsLabModal() {
|
|
|
322
444
|
async (command: string) => {
|
|
323
445
|
setRunning(true);
|
|
324
446
|
setRunError(null);
|
|
447
|
+
// Reset the DAG so the user always sees a fresh "pending → running →
|
|
448
|
+
// success/failed" lifecycle for this invocation. Switch tabs to Jobs
|
|
449
|
+
// so the visualisation is immediately visible.
|
|
450
|
+
setLiveJobs([]);
|
|
451
|
+
setRightTab("jobs");
|
|
325
452
|
appendConsole({ kind: "input", text: `$ ${command}\n` });
|
|
326
453
|
|
|
327
454
|
try {
|
|
@@ -342,6 +469,16 @@ export default function GithubActionsLabModal() {
|
|
|
342
469
|
(message: GhaStreamMessage) => {
|
|
343
470
|
if (message.type === "output") {
|
|
344
471
|
appendConsole({ kind: message.kind, text: message.text });
|
|
472
|
+
} else if (message.type === "job") {
|
|
473
|
+
// Merge by job name so repeated updates (running → success)
|
|
474
|
+
// replace the prior entry rather than appending duplicates.
|
|
475
|
+
setLiveJobs((prev) => {
|
|
476
|
+
const idx = prev.findIndex((j) => j.name === message.job.name);
|
|
477
|
+
if (idx === -1) return [...prev, message.job];
|
|
478
|
+
const next = prev.slice();
|
|
479
|
+
next[idx] = message.job;
|
|
480
|
+
return next;
|
|
481
|
+
});
|
|
345
482
|
} else if (message.type === "error") {
|
|
346
483
|
setRunError(message.error);
|
|
347
484
|
appendConsole({
|
|
@@ -353,6 +490,8 @@ export default function GithubActionsLabModal() {
|
|
|
353
490
|
kind: "info",
|
|
354
491
|
text: `\n[done] exit=${message.exitCode} • ${message.durationMs}ms\n`,
|
|
355
492
|
});
|
|
493
|
+
// Refresh History so the just-completed run shows up.
|
|
494
|
+
setHistoryNonce((n) => n + 1);
|
|
356
495
|
}
|
|
357
496
|
},
|
|
358
497
|
);
|
|
@@ -425,14 +564,59 @@ export default function GithubActionsLabModal() {
|
|
|
425
564
|
// ── Render ────────────────────────────────────────────────────────
|
|
426
565
|
const containerStyle: React.CSSProperties = maximized
|
|
427
566
|
? { top: 0, left: 0, width: "100vw", height: "100vh" }
|
|
428
|
-
: {
|
|
567
|
+
: {
|
|
568
|
+
top: pos.y,
|
|
569
|
+
left: pos.x,
|
|
570
|
+
width: size.w,
|
|
571
|
+
height: size.h,
|
|
572
|
+
minWidth: MIN_W,
|
|
573
|
+
minHeight: MIN_H,
|
|
574
|
+
};
|
|
429
575
|
|
|
430
576
|
return (
|
|
431
|
-
<div className="fixed inset-0 z-40 bg-black/40
|
|
577
|
+
<div className="fixed inset-0 z-40 bg-black/40">
|
|
432
578
|
<div
|
|
433
579
|
className="absolute flex flex-col rounded-2xl border border-slate-800 bg-slate-950 shadow-2xl overflow-hidden"
|
|
434
580
|
style={containerStyle}
|
|
435
581
|
>
|
|
582
|
+
{/* Resize handles */}
|
|
583
|
+
{!maximized && (
|
|
584
|
+
<>
|
|
585
|
+
<div
|
|
586
|
+
className="absolute inset-x-0 top-0 h-1 cursor-n-resize z-20"
|
|
587
|
+
onMouseDown={startResize("n")}
|
|
588
|
+
/>
|
|
589
|
+
<div
|
|
590
|
+
className="absolute inset-x-0 bottom-0 h-1 cursor-s-resize z-20"
|
|
591
|
+
onMouseDown={startResize("s")}
|
|
592
|
+
/>
|
|
593
|
+
<div
|
|
594
|
+
className="absolute inset-y-0 left-0 w-1 cursor-w-resize z-20"
|
|
595
|
+
onMouseDown={startResize("w")}
|
|
596
|
+
/>
|
|
597
|
+
<div
|
|
598
|
+
className="absolute inset-y-0 right-0 w-1 cursor-e-resize z-20"
|
|
599
|
+
onMouseDown={startResize("e")}
|
|
600
|
+
/>
|
|
601
|
+
<div
|
|
602
|
+
className="absolute top-0 left-0 w-3 h-3 cursor-nw-resize z-30"
|
|
603
|
+
onMouseDown={startResize("nw")}
|
|
604
|
+
/>
|
|
605
|
+
<div
|
|
606
|
+
className="absolute top-0 right-0 w-3 h-3 cursor-ne-resize z-30"
|
|
607
|
+
onMouseDown={startResize("ne")}
|
|
608
|
+
/>
|
|
609
|
+
<div
|
|
610
|
+
className="absolute bottom-0 left-0 w-3 h-3 cursor-sw-resize z-30"
|
|
611
|
+
onMouseDown={startResize("sw")}
|
|
612
|
+
/>
|
|
613
|
+
<div
|
|
614
|
+
className="absolute bottom-0 right-0 w-3 h-3 cursor-se-resize z-30"
|
|
615
|
+
onMouseDown={startResize("se")}
|
|
616
|
+
/>
|
|
617
|
+
</>
|
|
618
|
+
)}
|
|
619
|
+
|
|
436
620
|
{/* Title bar */}
|
|
437
621
|
<div
|
|
438
622
|
onMouseDown={onTitleMouseDown}
|
|
@@ -558,9 +742,25 @@ export default function GithubActionsLabModal() {
|
|
|
558
742
|
</div>
|
|
559
743
|
|
|
560
744
|
{/* Main body */}
|
|
561
|
-
<div
|
|
745
|
+
<div
|
|
746
|
+
className="flex-1 min-h-0 grid"
|
|
747
|
+
style={{
|
|
748
|
+
gridTemplateColumns: [
|
|
749
|
+
leftCollapsed ? "0px" : "220px",
|
|
750
|
+
"minmax(0,1fr)",
|
|
751
|
+
rightCollapsed ? "0px" : "minmax(320px,1fr)",
|
|
752
|
+
].join(" "),
|
|
753
|
+
}}
|
|
754
|
+
>
|
|
562
755
|
{/* File tree */}
|
|
563
|
-
<div
|
|
756
|
+
<div
|
|
757
|
+
className="flex flex-col border-r border-slate-800 bg-slate-900/30 min-h-0 overflow-hidden"
|
|
758
|
+
style={{
|
|
759
|
+
gridColumn: 1,
|
|
760
|
+
visibility: leftCollapsed ? "hidden" : undefined,
|
|
761
|
+
borderRightWidth: leftCollapsed ? 0 : undefined,
|
|
762
|
+
}}
|
|
763
|
+
>
|
|
564
764
|
<div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-800/60">
|
|
565
765
|
<span className="text-[10px] font-semibold tracking-widest text-slate-500">
|
|
566
766
|
FILES
|
|
@@ -626,9 +826,44 @@ export default function GithubActionsLabModal() {
|
|
|
626
826
|
</div>
|
|
627
827
|
|
|
628
828
|
{/* Editor */}
|
|
629
|
-
<div
|
|
630
|
-
|
|
631
|
-
|
|
829
|
+
<div
|
|
830
|
+
className="min-h-0 flex flex-col border-r border-slate-800"
|
|
831
|
+
style={{ gridColumn: 2 }}
|
|
832
|
+
>
|
|
833
|
+
<div className="flex items-center justify-between gap-2 px-2 py-1.5 border-b border-slate-800/60 text-[11px] text-slate-400">
|
|
834
|
+
<div className="flex items-center gap-1 min-w-0">
|
|
835
|
+
<button
|
|
836
|
+
onClick={() => setLeftCollapsed((v) => !v)}
|
|
837
|
+
className="p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60 shrink-0"
|
|
838
|
+
title={
|
|
839
|
+
leftCollapsed ? "Show files panel" : "Hide files panel"
|
|
840
|
+
}
|
|
841
|
+
>
|
|
842
|
+
{leftCollapsed ? (
|
|
843
|
+
<PanelLeftOpen className="w-3.5 h-3.5" />
|
|
844
|
+
) : (
|
|
845
|
+
<PanelLeftClose className="w-3.5 h-3.5" />
|
|
846
|
+
)}
|
|
847
|
+
</button>
|
|
848
|
+
<span className="truncate">
|
|
849
|
+
{activeFile || "(no file selected)"}
|
|
850
|
+
</span>
|
|
851
|
+
</div>
|
|
852
|
+
<button
|
|
853
|
+
onClick={() => setRightCollapsed((v) => !v)}
|
|
854
|
+
className="p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60 shrink-0"
|
|
855
|
+
title={
|
|
856
|
+
rightCollapsed
|
|
857
|
+
? "Show console/jobs panel"
|
|
858
|
+
: "Hide console/jobs panel"
|
|
859
|
+
}
|
|
860
|
+
>
|
|
861
|
+
{rightCollapsed ? (
|
|
862
|
+
<PanelRightOpen className="w-3.5 h-3.5" />
|
|
863
|
+
) : (
|
|
864
|
+
<PanelRightClose className="w-3.5 h-3.5" />
|
|
865
|
+
)}
|
|
866
|
+
</button>
|
|
632
867
|
</div>
|
|
633
868
|
<div className="flex-1 min-h-0">
|
|
634
869
|
{activeFile && workspace.files[activeFile] !== undefined && (
|
|
@@ -658,12 +893,41 @@ export default function GithubActionsLabModal() {
|
|
|
658
893
|
</div>
|
|
659
894
|
</div>
|
|
660
895
|
|
|
661
|
-
{/* Console */}
|
|
662
|
-
<div
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
896
|
+
{/* Right pane: tabbed Console / Jobs / History */}
|
|
897
|
+
<div
|
|
898
|
+
className="min-h-0 flex flex-col bg-slate-950 overflow-hidden"
|
|
899
|
+
style={{
|
|
900
|
+
gridColumn: 3,
|
|
901
|
+
visibility: rightCollapsed ? "hidden" : undefined,
|
|
902
|
+
}}
|
|
903
|
+
>
|
|
904
|
+
<div className="flex items-center justify-between border-b border-slate-800/60 px-2 py-1">
|
|
905
|
+
<div className="flex items-center gap-0.5 text-[11px]">
|
|
906
|
+
{(
|
|
907
|
+
[
|
|
908
|
+
{ id: "console", label: "Console" },
|
|
909
|
+
{ id: "jobs", label: "Jobs" },
|
|
910
|
+
{ id: "history", label: "History" },
|
|
911
|
+
] as const
|
|
912
|
+
).map((t) => (
|
|
913
|
+
<button
|
|
914
|
+
key={t.id}
|
|
915
|
+
onClick={() => setRightTab(t.id)}
|
|
916
|
+
className={`px-2 py-1 rounded ${
|
|
917
|
+
rightTab === t.id
|
|
918
|
+
? "bg-slate-800/70 text-amber-200"
|
|
919
|
+
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800/40"
|
|
920
|
+
}`}
|
|
921
|
+
>
|
|
922
|
+
{t.id === "console" && (
|
|
923
|
+
<Terminal className="inline w-3 h-3 mr-1 -mt-px" />
|
|
924
|
+
)}
|
|
925
|
+
{t.label}
|
|
926
|
+
{t.id === "jobs" && running && (
|
|
927
|
+
<span className="ml-1 inline-block w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
|
928
|
+
)}
|
|
929
|
+
</button>
|
|
930
|
+
))}
|
|
667
931
|
</div>
|
|
668
932
|
<div className="flex items-center gap-1">
|
|
669
933
|
{running && (
|
|
@@ -675,69 +939,107 @@ export default function GithubActionsLabModal() {
|
|
|
675
939
|
<StopCircle className="w-3.5 h-3.5" />
|
|
676
940
|
</button>
|
|
677
941
|
)}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
942
|
+
{rightTab === "console" && (
|
|
943
|
+
<button
|
|
944
|
+
onClick={clearConsole}
|
|
945
|
+
className="p-1 rounded text-slate-500 hover:text-slate-200 hover:bg-slate-800/60"
|
|
946
|
+
title="Clear console"
|
|
947
|
+
>
|
|
948
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
949
|
+
</button>
|
|
950
|
+
)}
|
|
685
951
|
</div>
|
|
686
952
|
</div>
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
953
|
+
|
|
954
|
+
{rightTab === "console" && (
|
|
955
|
+
<>
|
|
956
|
+
<div className="flex-1 min-h-0 overflow-auto px-3 py-2 text-[11px] font-mono whitespace-pre-wrap break-words">
|
|
957
|
+
{consoleLines.length === 0 ? (
|
|
958
|
+
<div className="text-slate-600">
|
|
959
|
+
Click <span className="text-amber-300">Run</span> to
|
|
960
|
+
execute the selected workflow with{" "}
|
|
961
|
+
<span className="text-amber-300">act</span>. Output will
|
|
962
|
+
stream here just like the Actions tab on GitHub.
|
|
963
|
+
{"\n\n"}
|
|
964
|
+
Tips:
|
|
965
|
+
{"\n"} • First run pulls a ~500 MB runner image. Be
|
|
966
|
+
patient.
|
|
967
|
+
{"\n"} • Use{" "}
|
|
968
|
+
<span className="text-amber-300">Dry run</span> to plan a
|
|
969
|
+
workflow without Docker.
|
|
970
|
+
{"\n"} • Switch to the{" "}
|
|
971
|
+
<span className="text-amber-300">Jobs</span> tab to see a
|
|
972
|
+
live DAG of running, succeeded, and failed jobs.
|
|
973
|
+
</div>
|
|
974
|
+
) : (
|
|
975
|
+
consoleLines.map((line) => (
|
|
976
|
+
<div
|
|
977
|
+
key={line.id}
|
|
978
|
+
className={
|
|
979
|
+
line.kind === "stderr"
|
|
980
|
+
? "text-red-300"
|
|
981
|
+
: line.kind === "info"
|
|
982
|
+
? "text-slate-400"
|
|
983
|
+
: line.kind === "input"
|
|
984
|
+
? "text-amber-200"
|
|
985
|
+
: "text-slate-200"
|
|
986
|
+
}
|
|
987
|
+
>
|
|
988
|
+
{line.text}
|
|
989
|
+
</div>
|
|
990
|
+
))
|
|
991
|
+
)}
|
|
992
|
+
{runError && (
|
|
993
|
+
<div className="mt-2 rounded border border-red-500/30 bg-red-500/10 px-2 py-1 text-red-200">
|
|
994
|
+
{runError}
|
|
995
|
+
</div>
|
|
996
|
+
)}
|
|
997
|
+
<div ref={consoleEndRef} />
|
|
724
998
|
</div>
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
999
|
+
<form
|
|
1000
|
+
onSubmit={handleConsoleSubmit}
|
|
1001
|
+
className="flex items-center gap-2 border-t border-slate-800/60 px-3 py-1.5"
|
|
1002
|
+
>
|
|
1003
|
+
<span className="text-amber-400 text-xs font-mono">$</span>
|
|
1004
|
+
<input
|
|
1005
|
+
value={consoleInput}
|
|
1006
|
+
onChange={(e) => setConsoleInput(e.target.value)}
|
|
1007
|
+
disabled={running}
|
|
1008
|
+
placeholder="act -l | act push -j greet | act -n"
|
|
1009
|
+
className="flex-1 bg-transparent text-xs font-mono text-slate-200 placeholder:text-slate-600 outline-none disabled:opacity-50"
|
|
1010
|
+
/>
|
|
1011
|
+
</form>
|
|
1012
|
+
</>
|
|
1013
|
+
)}
|
|
1014
|
+
|
|
1015
|
+
{rightTab === "jobs" && (
|
|
1016
|
+
<GhaJobsPanel
|
|
1017
|
+
workflowYaml={workflow ? workspace.files[workflow] : undefined}
|
|
1018
|
+
jobs={liveJobs}
|
|
1019
|
+
caption={
|
|
1020
|
+
running
|
|
1021
|
+
? "Live — updating as act emits job lines"
|
|
1022
|
+
: liveJobs.length
|
|
1023
|
+
? "Last run"
|
|
1024
|
+
: "Pre-run plan (parsed from workflow YAML)"
|
|
1025
|
+
}
|
|
739
1026
|
/>
|
|
740
|
-
|
|
1027
|
+
)}
|
|
1028
|
+
|
|
1029
|
+
{rightTab === "history" && (
|
|
1030
|
+
<GhaHistoryPanel
|
|
1031
|
+
{...(currentQuestion ? { questionId: currentQuestion.id } : {})}
|
|
1032
|
+
{...(activeGhaId ? { fileId: activeGhaId } : {})}
|
|
1033
|
+
includeInContext={!!workspace.includeRunHistoryInContext}
|
|
1034
|
+
onToggleIncludeInContext={(next) =>
|
|
1035
|
+
setWorkspace((prev) => ({
|
|
1036
|
+
...prev,
|
|
1037
|
+
includeRunHistoryInContext: next,
|
|
1038
|
+
}))
|
|
1039
|
+
}
|
|
1040
|
+
refreshNonce={historyNonce}
|
|
1041
|
+
/>
|
|
1042
|
+
)}
|
|
741
1043
|
</div>
|
|
742
1044
|
</div>
|
|
743
1045
|
</div>
|