create-interview-cockpit 0.18.0 → 0.20.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 +583 -76
- package/template/client/src/components/LabsPanel.tsx +11 -1
- package/template/client/src/components/Sidebar.tsx +216 -59
- package/template/client/src/githubActionsLab.ts +239 -2
- 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 +327 -1
- 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,
|
|
@@ -15,7 +19,12 @@ import {
|
|
|
15
19
|
X,
|
|
16
20
|
} from "lucide-react";
|
|
17
21
|
import MonacoEditorLib from "@monaco-editor/react";
|
|
18
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
BeforeMount,
|
|
24
|
+
Monaco,
|
|
25
|
+
OnChange,
|
|
26
|
+
OnMount,
|
|
27
|
+
} from "@monaco-editor/react";
|
|
19
28
|
import { useStore } from "../store";
|
|
20
29
|
import {
|
|
21
30
|
cloneGhaLabWorkspace,
|
|
@@ -26,7 +35,9 @@ import {
|
|
|
26
35
|
} from "../githubActionsLab";
|
|
27
36
|
import type { GithubActionsLabWorkspace } from "../types";
|
|
28
37
|
import * as api from "../api";
|
|
29
|
-
import type { GhaStreamMessage } from "../api";
|
|
38
|
+
import type { GhaJobSnapshot, GhaStreamMessage } from "../api";
|
|
39
|
+
import GhaJobsPanel from "./GhaJobsPanel";
|
|
40
|
+
import GhaHistoryPanel from "./GhaHistoryPanel";
|
|
30
41
|
|
|
31
42
|
// ─── Modal layout constants ──────────────────────────────────────────────
|
|
32
43
|
|
|
@@ -36,6 +47,10 @@ const DEFAULT_W = Math.min(1280, window.innerWidth - 48);
|
|
|
36
47
|
const DEFAULT_H = Math.min(820, window.innerHeight - 48);
|
|
37
48
|
const EDITOR_FONT =
|
|
38
49
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
50
|
+
const GHA_LAB_MODEL_ROOT = "file:///gha-lab/";
|
|
51
|
+
type ResizeDir = "e" | "s" | "se" | "sw" | "w" | "ne" | "nw" | "n";
|
|
52
|
+
|
|
53
|
+
let ghaLabMonacoTypeLibsInjected = false;
|
|
39
54
|
|
|
40
55
|
const EVENTS = [
|
|
41
56
|
"push",
|
|
@@ -54,6 +69,8 @@ function baseName(filePath: string): string {
|
|
|
54
69
|
function getEditorLanguage(filePath: string): string {
|
|
55
70
|
const lower = filePath.toLowerCase();
|
|
56
71
|
if (lower.endsWith(".yml") || lower.endsWith(".yaml")) return "yaml";
|
|
72
|
+
if (lower.endsWith(".html")) return "html";
|
|
73
|
+
if (lower.endsWith(".css")) return "css";
|
|
57
74
|
if (lower.endsWith(".json")) return "json";
|
|
58
75
|
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "markdown";
|
|
59
76
|
if (lower.endsWith(".js") || lower.endsWith(".mjs")) return "javascript";
|
|
@@ -63,6 +80,13 @@ function getEditorLanguage(filePath: string): string {
|
|
|
63
80
|
return "plaintext";
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
function getGhaLabModelPath(filePath: string): string {
|
|
84
|
+
return `${GHA_LAB_MODEL_ROOT}${filePath
|
|
85
|
+
.split("/")
|
|
86
|
+
.map(encodeURIComponent)
|
|
87
|
+
.join("/")}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
// Tiny grouped-by-folder list to keep the modal lean.
|
|
67
91
|
function groupByFolder(paths: string[]): { folder: string; files: string[] }[] {
|
|
68
92
|
const map = new Map<string, string[]>();
|
|
@@ -135,8 +159,21 @@ export default function GithubActionsLabModal() {
|
|
|
135
159
|
const [running, setRunning] = useState(false);
|
|
136
160
|
const [consoleLines, setConsoleLines] = useState<ConsoleLine[]>([]);
|
|
137
161
|
const [runError, setRunError] = useState<string | null>(null);
|
|
162
|
+
// Live job snapshots reported by the server during the active run.
|
|
163
|
+
// Reset every time the user kicks off a new run.
|
|
164
|
+
const [liveJobs, setLiveJobs] = useState<GhaJobSnapshot[]>([]);
|
|
165
|
+
// "console" | "jobs" | "history" — controls the right pane tab.
|
|
166
|
+
const [rightTab, setRightTab] = useState<"console" | "jobs" | "history">(
|
|
167
|
+
"console",
|
|
168
|
+
);
|
|
169
|
+
// Bumped each time a run completes so the History tab refetches.
|
|
170
|
+
const [historyNonce, setHistoryNonce] = useState(0);
|
|
171
|
+
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
172
|
+
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
138
173
|
const abortRef = useRef<AbortController | null>(null);
|
|
139
174
|
const consoleEndRef = useRef<HTMLDivElement | null>(null);
|
|
175
|
+
const monacoRef = useRef<Monaco | null>(null);
|
|
176
|
+
const monacoModelUrisRef = useRef<Set<string>>(new Set());
|
|
140
177
|
|
|
141
178
|
useEffect(() => {
|
|
142
179
|
// Keep workflow selection valid when files change
|
|
@@ -162,6 +199,14 @@ export default function GithubActionsLabModal() {
|
|
|
162
199
|
ox: number;
|
|
163
200
|
oy: number;
|
|
164
201
|
} | null>(null);
|
|
202
|
+
const resizeStart = useRef<{
|
|
203
|
+
mx: number;
|
|
204
|
+
my: number;
|
|
205
|
+
ox: number;
|
|
206
|
+
oy: number;
|
|
207
|
+
ow: number;
|
|
208
|
+
oh: number;
|
|
209
|
+
} | null>(null);
|
|
165
210
|
|
|
166
211
|
const onTitleMouseDown = useCallback(
|
|
167
212
|
(e: React.MouseEvent) => {
|
|
@@ -196,6 +241,64 @@ export default function GithubActionsLabModal() {
|
|
|
196
241
|
[pos.x, pos.y, maximized],
|
|
197
242
|
);
|
|
198
243
|
|
|
244
|
+
const startResize = useCallback(
|
|
245
|
+
(dir: ResizeDir) => (e: React.MouseEvent) => {
|
|
246
|
+
if (maximized) return;
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
e.stopPropagation();
|
|
249
|
+
resizeStart.current = {
|
|
250
|
+
mx: e.clientX,
|
|
251
|
+
my: e.clientY,
|
|
252
|
+
ox: pos.x,
|
|
253
|
+
oy: pos.y,
|
|
254
|
+
ow: size.w,
|
|
255
|
+
oh: size.h,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const originalCursor = document.body.style.cursor;
|
|
259
|
+
const originalUserSelect = document.body.style.userSelect;
|
|
260
|
+
document.body.style.cursor = `${dir}-resize`;
|
|
261
|
+
document.body.style.userSelect = "none";
|
|
262
|
+
|
|
263
|
+
const onMove = (ev: MouseEvent) => {
|
|
264
|
+
const resize = resizeStart.current;
|
|
265
|
+
if (!resize) return;
|
|
266
|
+
const dx = ev.clientX - resize.mx;
|
|
267
|
+
const dy = ev.clientY - resize.my;
|
|
268
|
+
let w = resize.ow;
|
|
269
|
+
let h = resize.oh;
|
|
270
|
+
let x = resize.ox;
|
|
271
|
+
let y = resize.oy;
|
|
272
|
+
|
|
273
|
+
if (dir.includes("e")) w = Math.max(MIN_W, resize.ow + dx);
|
|
274
|
+
if (dir.includes("s")) h = Math.max(MIN_H, resize.oh + dy);
|
|
275
|
+
if (dir.includes("w")) {
|
|
276
|
+
w = Math.max(MIN_W, resize.ow - dx);
|
|
277
|
+
x = Math.min(resize.ox + resize.ow - MIN_W, resize.ox + dx);
|
|
278
|
+
}
|
|
279
|
+
if (dir.includes("n")) {
|
|
280
|
+
h = Math.max(MIN_H, resize.oh - dy);
|
|
281
|
+
y = Math.min(resize.oy + resize.oh - MIN_H, resize.oy + dy);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
setSize({ w, h });
|
|
285
|
+
setPos({ x: Math.max(0, x), y: Math.max(0, y) });
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const onUp = () => {
|
|
289
|
+
resizeStart.current = null;
|
|
290
|
+
document.body.style.cursor = originalCursor;
|
|
291
|
+
document.body.style.userSelect = originalUserSelect;
|
|
292
|
+
window.removeEventListener("mousemove", onMove);
|
|
293
|
+
window.removeEventListener("mouseup", onUp);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
window.addEventListener("mousemove", onMove);
|
|
297
|
+
window.addEventListener("mouseup", onUp);
|
|
298
|
+
},
|
|
299
|
+
[maximized, pos.x, pos.y, size.w, size.h],
|
|
300
|
+
);
|
|
301
|
+
|
|
199
302
|
// ── File operations ───────────────────────────────────────────────
|
|
200
303
|
const fileOrder = useMemo(() => getGhaLabFileOrder(workspace), [workspace]);
|
|
201
304
|
const grouped = useMemo(() => groupByFolder(fileOrder), [fileOrder]);
|
|
@@ -258,13 +361,51 @@ export default function GithubActionsLabModal() {
|
|
|
258
361
|
if (!currentQuestion) return;
|
|
259
362
|
setSaving(true);
|
|
260
363
|
try {
|
|
261
|
-
|
|
364
|
+
// When the user opted in, embed a compact summary of the most recent
|
|
365
|
+
// runs into the saved JSON so the chat LLM has real execution data
|
|
366
|
+
// to reason about (job statuses, durations, exit codes).
|
|
367
|
+
let recentRunsExtras: Record<string, unknown> = {};
|
|
368
|
+
if (workspace.includeRunHistoryInContext) {
|
|
369
|
+
try {
|
|
370
|
+
const runs = await api.listGhaRuns({
|
|
371
|
+
questionId: currentQuestion.id,
|
|
372
|
+
...(activeGhaId ? { fileId: activeGhaId } : {}),
|
|
373
|
+
limit: 5,
|
|
374
|
+
});
|
|
375
|
+
recentRunsExtras = {
|
|
376
|
+
recentRuns: runs.map((r) => ({
|
|
377
|
+
command: r.command,
|
|
378
|
+
status: r.status,
|
|
379
|
+
exitCode: r.exitCode,
|
|
380
|
+
startedAt: r.startedAt,
|
|
381
|
+
durationMs: r.durationMs,
|
|
382
|
+
jobs: (r.jobs ?? []).map((j) => ({
|
|
383
|
+
name: j.name,
|
|
384
|
+
status: j.status,
|
|
385
|
+
durationMs: j.durationMs,
|
|
386
|
+
})),
|
|
387
|
+
})),
|
|
388
|
+
};
|
|
389
|
+
} catch {
|
|
390
|
+
// Non-fatal — just skip the appendix if the list call fails.
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const serialized = serializeGhaLabWorkspace({
|
|
262
394
|
...workspace,
|
|
263
395
|
label: labName || workspace.label,
|
|
264
396
|
activeFile,
|
|
265
397
|
defaultEvent: event,
|
|
266
398
|
defaultWorkflow: workflow,
|
|
267
399
|
});
|
|
400
|
+
// Splice the extras into the serialised JSON without breaking the
|
|
401
|
+
// existing schema parsers tolerate.
|
|
402
|
+
const payload = Object.keys(recentRunsExtras).length
|
|
403
|
+
? JSON.stringify(
|
|
404
|
+
{ ...JSON.parse(serialized), ...recentRunsExtras },
|
|
405
|
+
null,
|
|
406
|
+
2,
|
|
407
|
+
)
|
|
408
|
+
: serialized;
|
|
268
409
|
if (activeGhaId) {
|
|
269
410
|
await overwriteContextFileContent(
|
|
270
411
|
currentQuestion.id,
|
|
@@ -322,6 +463,11 @@ export default function GithubActionsLabModal() {
|
|
|
322
463
|
async (command: string) => {
|
|
323
464
|
setRunning(true);
|
|
324
465
|
setRunError(null);
|
|
466
|
+
// Reset the DAG so the user always sees a fresh "pending → running →
|
|
467
|
+
// success/failed" lifecycle for this invocation. Switch tabs to Jobs
|
|
468
|
+
// so the visualisation is immediately visible.
|
|
469
|
+
setLiveJobs([]);
|
|
470
|
+
setRightTab("jobs");
|
|
325
471
|
appendConsole({ kind: "input", text: `$ ${command}\n` });
|
|
326
472
|
|
|
327
473
|
try {
|
|
@@ -342,6 +488,16 @@ export default function GithubActionsLabModal() {
|
|
|
342
488
|
(message: GhaStreamMessage) => {
|
|
343
489
|
if (message.type === "output") {
|
|
344
490
|
appendConsole({ kind: message.kind, text: message.text });
|
|
491
|
+
} else if (message.type === "job") {
|
|
492
|
+
// Merge by job name so repeated updates (running → success)
|
|
493
|
+
// replace the prior entry rather than appending duplicates.
|
|
494
|
+
setLiveJobs((prev) => {
|
|
495
|
+
const idx = prev.findIndex((j) => j.name === message.job.name);
|
|
496
|
+
if (idx === -1) return [...prev, message.job];
|
|
497
|
+
const next = prev.slice();
|
|
498
|
+
next[idx] = message.job;
|
|
499
|
+
return next;
|
|
500
|
+
});
|
|
345
501
|
} else if (message.type === "error") {
|
|
346
502
|
setRunError(message.error);
|
|
347
503
|
appendConsole({
|
|
@@ -353,6 +509,8 @@ export default function GithubActionsLabModal() {
|
|
|
353
509
|
kind: "info",
|
|
354
510
|
text: `\n[done] exit=${message.exitCode} • ${message.durationMs}ms\n`,
|
|
355
511
|
});
|
|
512
|
+
// Refresh History so the just-completed run shows up.
|
|
513
|
+
setHistoryNonce((n) => n + 1);
|
|
356
514
|
}
|
|
357
515
|
},
|
|
358
516
|
);
|
|
@@ -382,6 +540,50 @@ export default function GithubActionsLabModal() {
|
|
|
382
540
|
|
|
383
541
|
const clearConsole = () => setConsoleLines([]);
|
|
384
542
|
|
|
543
|
+
const syncMonacoWorkspaceModels = useCallback(
|
|
544
|
+
(monaco: Monaco, files: Record<string, string>) => {
|
|
545
|
+
const nextUris = new Set<string>();
|
|
546
|
+
|
|
547
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
548
|
+
const uri = monaco.Uri.parse(getGhaLabModelPath(filePath));
|
|
549
|
+
const uriText = uri.toString();
|
|
550
|
+
nextUris.add(uriText);
|
|
551
|
+
|
|
552
|
+
const existing = monaco.editor.getModel(uri);
|
|
553
|
+
if (existing) {
|
|
554
|
+
if (existing.getValue() !== content) existing.setValue(content);
|
|
555
|
+
} else {
|
|
556
|
+
monaco.editor.createModel(content, getEditorLanguage(filePath), uri);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
for (const uriText of monacoModelUrisRef.current) {
|
|
561
|
+
if (nextUris.has(uriText)) continue;
|
|
562
|
+
monaco.editor.getModel(monaco.Uri.parse(uriText))?.dispose();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
monacoModelUrisRef.current = nextUris;
|
|
566
|
+
},
|
|
567
|
+
[],
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
const monaco = monacoRef.current;
|
|
572
|
+
if (!monaco) return;
|
|
573
|
+
syncMonacoWorkspaceModels(monaco, workspace.files);
|
|
574
|
+
}, [syncMonacoWorkspaceModels, workspace.files]);
|
|
575
|
+
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
return () => {
|
|
578
|
+
const monaco = monacoRef.current;
|
|
579
|
+
if (!monaco) return;
|
|
580
|
+
for (const uriText of monacoModelUrisRef.current) {
|
|
581
|
+
monaco.editor.getModel(monaco.Uri.parse(uriText))?.dispose();
|
|
582
|
+
}
|
|
583
|
+
monacoModelUrisRef.current.clear();
|
|
584
|
+
};
|
|
585
|
+
}, []);
|
|
586
|
+
|
|
385
587
|
// ── Console input ─────────────────────────────────────────────────
|
|
386
588
|
const [consoleInput, setConsoleInput] = useState("");
|
|
387
589
|
const handleConsoleSubmit = (e: React.FormEvent) => {
|
|
@@ -394,6 +596,139 @@ export default function GithubActionsLabModal() {
|
|
|
394
596
|
|
|
395
597
|
// ── Monaco config ─────────────────────────────────────────────────
|
|
396
598
|
const handleBeforeMount = useCallback<BeforeMount>((monaco: Monaco) => {
|
|
599
|
+
monacoRef.current = monaco;
|
|
600
|
+
|
|
601
|
+
// Monaco does not read the template's tsconfig.json or package.json, so
|
|
602
|
+
// configure its in-browser TypeScript worker like a Vite React TS project.
|
|
603
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
604
|
+
const tsLang = monaco.languages.typescript as any;
|
|
605
|
+
const compilerOptions = {
|
|
606
|
+
target: tsLang.ScriptTarget.ESNext,
|
|
607
|
+
module: tsLang.ModuleKind.ESNext,
|
|
608
|
+
moduleResolution:
|
|
609
|
+
tsLang.ModuleResolutionKind.Bundler ??
|
|
610
|
+
tsLang.ModuleResolutionKind.NodeJs,
|
|
611
|
+
jsx: tsLang.JsxEmit.ReactJSX ?? tsLang.JsxEmit.Preserve,
|
|
612
|
+
jsxImportSource: "react",
|
|
613
|
+
allowJs: true,
|
|
614
|
+
strict: true,
|
|
615
|
+
esModuleInterop: true,
|
|
616
|
+
allowSyntheticDefaultImports: true,
|
|
617
|
+
allowNonTsExtensions: true,
|
|
618
|
+
resolveJsonModule: true,
|
|
619
|
+
};
|
|
620
|
+
tsLang.typescriptDefaults.setCompilerOptions(compilerOptions);
|
|
621
|
+
tsLang.javascriptDefaults.setCompilerOptions(compilerOptions);
|
|
622
|
+
tsLang.typescriptDefaults.setDiagnosticsOptions({
|
|
623
|
+
noSemanticValidation: false,
|
|
624
|
+
noSyntaxValidation: false,
|
|
625
|
+
diagnosticCodesToIgnore: [2688],
|
|
626
|
+
});
|
|
627
|
+
tsLang.javascriptDefaults.setDiagnosticsOptions({
|
|
628
|
+
noSemanticValidation: false,
|
|
629
|
+
noSyntaxValidation: false,
|
|
630
|
+
diagnosticCodesToIgnore: [2688],
|
|
631
|
+
});
|
|
632
|
+
tsLang.typescriptDefaults.setEagerModelSync(true);
|
|
633
|
+
tsLang.javascriptDefaults.setEagerModelSync(true);
|
|
634
|
+
|
|
635
|
+
if (!ghaLabMonacoTypeLibsInjected) {
|
|
636
|
+
const reactViteTypes = `
|
|
637
|
+
declare module "react" {
|
|
638
|
+
export type ReactNode = any;
|
|
639
|
+
export type ReactElement = any;
|
|
640
|
+
export type CSSProperties = Record<string, string | number>;
|
|
641
|
+
export interface FC<P = {}> {
|
|
642
|
+
(props: P): ReactElement | null;
|
|
643
|
+
}
|
|
644
|
+
export const StrictMode: FC<{ children?: ReactNode }>;
|
|
645
|
+
const React: {
|
|
646
|
+
createElement: (...args: any[]) => ReactElement;
|
|
647
|
+
Fragment: any;
|
|
648
|
+
StrictMode: FC<{ children?: ReactNode }>;
|
|
649
|
+
};
|
|
650
|
+
export default React;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
declare module "react/jsx-runtime" {
|
|
654
|
+
export namespace JSX {
|
|
655
|
+
interface Element {}
|
|
656
|
+
interface IntrinsicElements {
|
|
657
|
+
[elemName: string]: any;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
export const Fragment: any;
|
|
661
|
+
export function jsx(type: any, props: any, key?: any): any;
|
|
662
|
+
export function jsxs(type: any, props: any, key?: any): any;
|
|
663
|
+
export function jsxDEV(
|
|
664
|
+
type: any,
|
|
665
|
+
props: any,
|
|
666
|
+
key: any,
|
|
667
|
+
isStaticChildren: boolean,
|
|
668
|
+
source: any,
|
|
669
|
+
self: any,
|
|
670
|
+
): any;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
declare namespace JSX {
|
|
674
|
+
interface Element {}
|
|
675
|
+
interface IntrinsicElements {
|
|
676
|
+
[elemName: string]: any;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
declare module "react-dom/client" {
|
|
681
|
+
export function createRoot(container: Element | DocumentFragment): {
|
|
682
|
+
render(children: any): void;
|
|
683
|
+
unmount(): void;
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
declare module "vite" {
|
|
688
|
+
export function defineConfig(config: any): any;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
declare module "@vitejs/plugin-react" {
|
|
692
|
+
export default function react(options?: any): any;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
declare module "*.css" {
|
|
696
|
+
const classes: Record<string, string>;
|
|
697
|
+
export default classes;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
interface ImportMetaEnv {
|
|
701
|
+
readonly BASE_URL: string;
|
|
702
|
+
readonly DEV: boolean;
|
|
703
|
+
readonly MODE: string;
|
|
704
|
+
readonly PROD: boolean;
|
|
705
|
+
readonly SSR: boolean;
|
|
706
|
+
readonly [key: string]: any;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
interface ImportMeta {
|
|
710
|
+
readonly env: ImportMetaEnv;
|
|
711
|
+
}
|
|
712
|
+
`;
|
|
713
|
+
tsLang.typescriptDefaults.addExtraLib(
|
|
714
|
+
reactViteTypes,
|
|
715
|
+
"file:///__gha-lab-types/react-vite.d.ts",
|
|
716
|
+
);
|
|
717
|
+
tsLang.javascriptDefaults.addExtraLib(
|
|
718
|
+
reactViteTypes,
|
|
719
|
+
"file:///__gha-lab-types/react-vite.d.ts",
|
|
720
|
+
);
|
|
721
|
+
tsLang.typescriptDefaults.addExtraLib(
|
|
722
|
+
reactViteTypes,
|
|
723
|
+
"file:///node_modules/vite/client.d.ts",
|
|
724
|
+
);
|
|
725
|
+
tsLang.javascriptDefaults.addExtraLib(
|
|
726
|
+
reactViteTypes,
|
|
727
|
+
"file:///node_modules/vite/client.d.ts",
|
|
728
|
+
);
|
|
729
|
+
ghaLabMonacoTypeLibsInjected = true;
|
|
730
|
+
}
|
|
731
|
+
|
|
397
732
|
monaco.editor.defineTheme("gha-lab-dark", {
|
|
398
733
|
base: "vs-dark",
|
|
399
734
|
inherit: true,
|
|
@@ -415,6 +750,14 @@ export default function GithubActionsLabModal() {
|
|
|
415
750
|
});
|
|
416
751
|
}, []);
|
|
417
752
|
|
|
753
|
+
const handleMount = useCallback<OnMount>(
|
|
754
|
+
(_editor, monaco) => {
|
|
755
|
+
monacoRef.current = monaco;
|
|
756
|
+
syncMonacoWorkspaceModels(monaco, workspace.files);
|
|
757
|
+
},
|
|
758
|
+
[syncMonacoWorkspaceModels, workspace.files],
|
|
759
|
+
);
|
|
760
|
+
|
|
418
761
|
const handleEditorChange = useCallback<OnChange>(
|
|
419
762
|
(next) => {
|
|
420
763
|
updateFile(activeFile, next ?? "");
|
|
@@ -425,14 +768,59 @@ export default function GithubActionsLabModal() {
|
|
|
425
768
|
// ── Render ────────────────────────────────────────────────────────
|
|
426
769
|
const containerStyle: React.CSSProperties = maximized
|
|
427
770
|
? { top: 0, left: 0, width: "100vw", height: "100vh" }
|
|
428
|
-
: {
|
|
771
|
+
: {
|
|
772
|
+
top: pos.y,
|
|
773
|
+
left: pos.x,
|
|
774
|
+
width: size.w,
|
|
775
|
+
height: size.h,
|
|
776
|
+
minWidth: MIN_W,
|
|
777
|
+
minHeight: MIN_H,
|
|
778
|
+
};
|
|
429
779
|
|
|
430
780
|
return (
|
|
431
|
-
<div className="fixed inset-0 z-40 bg-black/40
|
|
781
|
+
<div className="fixed inset-0 z-40 bg-black/40">
|
|
432
782
|
<div
|
|
433
783
|
className="absolute flex flex-col rounded-2xl border border-slate-800 bg-slate-950 shadow-2xl overflow-hidden"
|
|
434
784
|
style={containerStyle}
|
|
435
785
|
>
|
|
786
|
+
{/* Resize handles */}
|
|
787
|
+
{!maximized && (
|
|
788
|
+
<>
|
|
789
|
+
<div
|
|
790
|
+
className="absolute inset-x-0 top-0 h-1 cursor-n-resize z-20"
|
|
791
|
+
onMouseDown={startResize("n")}
|
|
792
|
+
/>
|
|
793
|
+
<div
|
|
794
|
+
className="absolute inset-x-0 bottom-0 h-1 cursor-s-resize z-20"
|
|
795
|
+
onMouseDown={startResize("s")}
|
|
796
|
+
/>
|
|
797
|
+
<div
|
|
798
|
+
className="absolute inset-y-0 left-0 w-1 cursor-w-resize z-20"
|
|
799
|
+
onMouseDown={startResize("w")}
|
|
800
|
+
/>
|
|
801
|
+
<div
|
|
802
|
+
className="absolute inset-y-0 right-0 w-1 cursor-e-resize z-20"
|
|
803
|
+
onMouseDown={startResize("e")}
|
|
804
|
+
/>
|
|
805
|
+
<div
|
|
806
|
+
className="absolute top-0 left-0 w-3 h-3 cursor-nw-resize z-30"
|
|
807
|
+
onMouseDown={startResize("nw")}
|
|
808
|
+
/>
|
|
809
|
+
<div
|
|
810
|
+
className="absolute top-0 right-0 w-3 h-3 cursor-ne-resize z-30"
|
|
811
|
+
onMouseDown={startResize("ne")}
|
|
812
|
+
/>
|
|
813
|
+
<div
|
|
814
|
+
className="absolute bottom-0 left-0 w-3 h-3 cursor-sw-resize z-30"
|
|
815
|
+
onMouseDown={startResize("sw")}
|
|
816
|
+
/>
|
|
817
|
+
<div
|
|
818
|
+
className="absolute bottom-0 right-0 w-3 h-3 cursor-se-resize z-30"
|
|
819
|
+
onMouseDown={startResize("se")}
|
|
820
|
+
/>
|
|
821
|
+
</>
|
|
822
|
+
)}
|
|
823
|
+
|
|
436
824
|
{/* Title bar */}
|
|
437
825
|
<div
|
|
438
826
|
onMouseDown={onTitleMouseDown}
|
|
@@ -558,9 +946,25 @@ export default function GithubActionsLabModal() {
|
|
|
558
946
|
</div>
|
|
559
947
|
|
|
560
948
|
{/* Main body */}
|
|
561
|
-
<div
|
|
949
|
+
<div
|
|
950
|
+
className="flex-1 min-h-0 grid"
|
|
951
|
+
style={{
|
|
952
|
+
gridTemplateColumns: [
|
|
953
|
+
leftCollapsed ? "0px" : "220px",
|
|
954
|
+
"minmax(0,1fr)",
|
|
955
|
+
rightCollapsed ? "0px" : "minmax(320px,1fr)",
|
|
956
|
+
].join(" "),
|
|
957
|
+
}}
|
|
958
|
+
>
|
|
562
959
|
{/* File tree */}
|
|
563
|
-
<div
|
|
960
|
+
<div
|
|
961
|
+
className="flex flex-col border-r border-slate-800 bg-slate-900/30 min-h-0 overflow-hidden"
|
|
962
|
+
style={{
|
|
963
|
+
gridColumn: 1,
|
|
964
|
+
visibility: leftCollapsed ? "hidden" : undefined,
|
|
965
|
+
borderRightWidth: leftCollapsed ? 0 : undefined,
|
|
966
|
+
}}
|
|
967
|
+
>
|
|
564
968
|
<div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-800/60">
|
|
565
969
|
<span className="text-[10px] font-semibold tracking-widest text-slate-500">
|
|
566
970
|
FILES
|
|
@@ -626,9 +1030,44 @@ export default function GithubActionsLabModal() {
|
|
|
626
1030
|
</div>
|
|
627
1031
|
|
|
628
1032
|
{/* Editor */}
|
|
629
|
-
<div
|
|
630
|
-
|
|
631
|
-
|
|
1033
|
+
<div
|
|
1034
|
+
className="min-h-0 flex flex-col border-r border-slate-800"
|
|
1035
|
+
style={{ gridColumn: 2 }}
|
|
1036
|
+
>
|
|
1037
|
+
<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">
|
|
1038
|
+
<div className="flex items-center gap-1 min-w-0">
|
|
1039
|
+
<button
|
|
1040
|
+
onClick={() => setLeftCollapsed((v) => !v)}
|
|
1041
|
+
className="p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60 shrink-0"
|
|
1042
|
+
title={
|
|
1043
|
+
leftCollapsed ? "Show files panel" : "Hide files panel"
|
|
1044
|
+
}
|
|
1045
|
+
>
|
|
1046
|
+
{leftCollapsed ? (
|
|
1047
|
+
<PanelLeftOpen className="w-3.5 h-3.5" />
|
|
1048
|
+
) : (
|
|
1049
|
+
<PanelLeftClose className="w-3.5 h-3.5" />
|
|
1050
|
+
)}
|
|
1051
|
+
</button>
|
|
1052
|
+
<span className="truncate">
|
|
1053
|
+
{activeFile || "(no file selected)"}
|
|
1054
|
+
</span>
|
|
1055
|
+
</div>
|
|
1056
|
+
<button
|
|
1057
|
+
onClick={() => setRightCollapsed((v) => !v)}
|
|
1058
|
+
className="p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60 shrink-0"
|
|
1059
|
+
title={
|
|
1060
|
+
rightCollapsed
|
|
1061
|
+
? "Show console/jobs panel"
|
|
1062
|
+
: "Hide console/jobs panel"
|
|
1063
|
+
}
|
|
1064
|
+
>
|
|
1065
|
+
{rightCollapsed ? (
|
|
1066
|
+
<PanelRightOpen className="w-3.5 h-3.5" />
|
|
1067
|
+
) : (
|
|
1068
|
+
<PanelRightClose className="w-3.5 h-3.5" />
|
|
1069
|
+
)}
|
|
1070
|
+
</button>
|
|
632
1071
|
</div>
|
|
633
1072
|
<div className="flex-1 min-h-0">
|
|
634
1073
|
{activeFile && workspace.files[activeFile] !== undefined && (
|
|
@@ -638,9 +1077,10 @@ export default function GithubActionsLabModal() {
|
|
|
638
1077
|
width="100%"
|
|
639
1078
|
language={getEditorLanguage(activeFile)}
|
|
640
1079
|
theme="gha-lab-dark"
|
|
641
|
-
path={
|
|
1080
|
+
path={getGhaLabModelPath(activeFile)}
|
|
642
1081
|
value={workspace.files[activeFile]}
|
|
643
1082
|
beforeMount={handleBeforeMount}
|
|
1083
|
+
onMount={handleMount}
|
|
644
1084
|
onChange={handleEditorChange}
|
|
645
1085
|
options={{
|
|
646
1086
|
fontFamily: EDITOR_FONT,
|
|
@@ -658,12 +1098,41 @@ export default function GithubActionsLabModal() {
|
|
|
658
1098
|
</div>
|
|
659
1099
|
</div>
|
|
660
1100
|
|
|
661
|
-
{/* Console */}
|
|
662
|
-
<div
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1101
|
+
{/* Right pane: tabbed Console / Jobs / History */}
|
|
1102
|
+
<div
|
|
1103
|
+
className="min-h-0 flex flex-col bg-slate-950 overflow-hidden"
|
|
1104
|
+
style={{
|
|
1105
|
+
gridColumn: 3,
|
|
1106
|
+
visibility: rightCollapsed ? "hidden" : undefined,
|
|
1107
|
+
}}
|
|
1108
|
+
>
|
|
1109
|
+
<div className="flex items-center justify-between border-b border-slate-800/60 px-2 py-1">
|
|
1110
|
+
<div className="flex items-center gap-0.5 text-[11px]">
|
|
1111
|
+
{(
|
|
1112
|
+
[
|
|
1113
|
+
{ id: "console", label: "Console" },
|
|
1114
|
+
{ id: "jobs", label: "Jobs" },
|
|
1115
|
+
{ id: "history", label: "History" },
|
|
1116
|
+
] as const
|
|
1117
|
+
).map((t) => (
|
|
1118
|
+
<button
|
|
1119
|
+
key={t.id}
|
|
1120
|
+
onClick={() => setRightTab(t.id)}
|
|
1121
|
+
className={`px-2 py-1 rounded ${
|
|
1122
|
+
rightTab === t.id
|
|
1123
|
+
? "bg-slate-800/70 text-amber-200"
|
|
1124
|
+
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800/40"
|
|
1125
|
+
}`}
|
|
1126
|
+
>
|
|
1127
|
+
{t.id === "console" && (
|
|
1128
|
+
<Terminal className="inline w-3 h-3 mr-1 -mt-px" />
|
|
1129
|
+
)}
|
|
1130
|
+
{t.label}
|
|
1131
|
+
{t.id === "jobs" && running && (
|
|
1132
|
+
<span className="ml-1 inline-block w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
|
1133
|
+
)}
|
|
1134
|
+
</button>
|
|
1135
|
+
))}
|
|
667
1136
|
</div>
|
|
668
1137
|
<div className="flex items-center gap-1">
|
|
669
1138
|
{running && (
|
|
@@ -675,69 +1144,107 @@ export default function GithubActionsLabModal() {
|
|
|
675
1144
|
<StopCircle className="w-3.5 h-3.5" />
|
|
676
1145
|
</button>
|
|
677
1146
|
)}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1147
|
+
{rightTab === "console" && (
|
|
1148
|
+
<button
|
|
1149
|
+
onClick={clearConsole}
|
|
1150
|
+
className="p-1 rounded text-slate-500 hover:text-slate-200 hover:bg-slate-800/60"
|
|
1151
|
+
title="Clear console"
|
|
1152
|
+
>
|
|
1153
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
1154
|
+
</button>
|
|
1155
|
+
)}
|
|
685
1156
|
</div>
|
|
686
1157
|
</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
|
-
|
|
1158
|
+
|
|
1159
|
+
{rightTab === "console" && (
|
|
1160
|
+
<>
|
|
1161
|
+
<div className="flex-1 min-h-0 overflow-auto px-3 py-2 text-[11px] font-mono whitespace-pre-wrap break-words">
|
|
1162
|
+
{consoleLines.length === 0 ? (
|
|
1163
|
+
<div className="text-slate-600">
|
|
1164
|
+
Click <span className="text-amber-300">Run</span> to
|
|
1165
|
+
execute the selected workflow with{" "}
|
|
1166
|
+
<span className="text-amber-300">act</span>. Output will
|
|
1167
|
+
stream here just like the Actions tab on GitHub.
|
|
1168
|
+
{"\n\n"}
|
|
1169
|
+
Tips:
|
|
1170
|
+
{"\n"} • First run pulls a ~500 MB runner image. Be
|
|
1171
|
+
patient.
|
|
1172
|
+
{"\n"} • Use{" "}
|
|
1173
|
+
<span className="text-amber-300">Dry run</span> to plan a
|
|
1174
|
+
workflow without Docker.
|
|
1175
|
+
{"\n"} • Switch to the{" "}
|
|
1176
|
+
<span className="text-amber-300">Jobs</span> tab to see a
|
|
1177
|
+
live DAG of running, succeeded, and failed jobs.
|
|
1178
|
+
</div>
|
|
1179
|
+
) : (
|
|
1180
|
+
consoleLines.map((line) => (
|
|
1181
|
+
<div
|
|
1182
|
+
key={line.id}
|
|
1183
|
+
className={
|
|
1184
|
+
line.kind === "stderr"
|
|
1185
|
+
? "text-red-300"
|
|
1186
|
+
: line.kind === "info"
|
|
1187
|
+
? "text-slate-400"
|
|
1188
|
+
: line.kind === "input"
|
|
1189
|
+
? "text-amber-200"
|
|
1190
|
+
: "text-slate-200"
|
|
1191
|
+
}
|
|
1192
|
+
>
|
|
1193
|
+
{line.text}
|
|
1194
|
+
</div>
|
|
1195
|
+
))
|
|
1196
|
+
)}
|
|
1197
|
+
{runError && (
|
|
1198
|
+
<div className="mt-2 rounded border border-red-500/30 bg-red-500/10 px-2 py-1 text-red-200">
|
|
1199
|
+
{runError}
|
|
1200
|
+
</div>
|
|
1201
|
+
)}
|
|
1202
|
+
<div ref={consoleEndRef} />
|
|
724
1203
|
</div>
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1204
|
+
<form
|
|
1205
|
+
onSubmit={handleConsoleSubmit}
|
|
1206
|
+
className="flex items-center gap-2 border-t border-slate-800/60 px-3 py-1.5"
|
|
1207
|
+
>
|
|
1208
|
+
<span className="text-amber-400 text-xs font-mono">$</span>
|
|
1209
|
+
<input
|
|
1210
|
+
value={consoleInput}
|
|
1211
|
+
onChange={(e) => setConsoleInput(e.target.value)}
|
|
1212
|
+
disabled={running}
|
|
1213
|
+
placeholder="act -l | act push -j greet | act -n"
|
|
1214
|
+
className="flex-1 bg-transparent text-xs font-mono text-slate-200 placeholder:text-slate-600 outline-none disabled:opacity-50"
|
|
1215
|
+
/>
|
|
1216
|
+
</form>
|
|
1217
|
+
</>
|
|
1218
|
+
)}
|
|
1219
|
+
|
|
1220
|
+
{rightTab === "jobs" && (
|
|
1221
|
+
<GhaJobsPanel
|
|
1222
|
+
workflowYaml={workflow ? workspace.files[workflow] : undefined}
|
|
1223
|
+
jobs={liveJobs}
|
|
1224
|
+
caption={
|
|
1225
|
+
running
|
|
1226
|
+
? "Live — updating as act emits job lines"
|
|
1227
|
+
: liveJobs.length
|
|
1228
|
+
? "Last run"
|
|
1229
|
+
: "Pre-run plan (parsed from workflow YAML)"
|
|
1230
|
+
}
|
|
739
1231
|
/>
|
|
740
|
-
|
|
1232
|
+
)}
|
|
1233
|
+
|
|
1234
|
+
{rightTab === "history" && (
|
|
1235
|
+
<GhaHistoryPanel
|
|
1236
|
+
{...(currentQuestion ? { questionId: currentQuestion.id } : {})}
|
|
1237
|
+
{...(activeGhaId ? { fileId: activeGhaId } : {})}
|
|
1238
|
+
includeInContext={!!workspace.includeRunHistoryInContext}
|
|
1239
|
+
onToggleIncludeInContext={(next) =>
|
|
1240
|
+
setWorkspace((prev) => ({
|
|
1241
|
+
...prev,
|
|
1242
|
+
includeRunHistoryInContext: next,
|
|
1243
|
+
}))
|
|
1244
|
+
}
|
|
1245
|
+
refreshNonce={historyNonce}
|
|
1246
|
+
/>
|
|
1247
|
+
)}
|
|
741
1248
|
</div>
|
|
742
1249
|
</div>
|
|
743
1250
|
</div>
|