create-interview-cockpit 0.21.0 → 0.23.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
CHANGED
|
@@ -295,6 +295,13 @@ export default function CodeContextPanel() {
|
|
|
295
295
|
[currentQuestion, updateCodeContext],
|
|
296
296
|
);
|
|
297
297
|
|
|
298
|
+
const selectAllFiles = useCallback(() => {
|
|
299
|
+
if (!currentQuestion || availableFiles.length === 0) return;
|
|
300
|
+
const next = [...availableFiles];
|
|
301
|
+
setSelectedFiles(next);
|
|
302
|
+
updateCodeContext(currentQuestion.id, next);
|
|
303
|
+
}, [availableFiles, currentQuestion, updateCodeContext]);
|
|
304
|
+
|
|
298
305
|
const toggleExpand = useCallback((path: string) => {
|
|
299
306
|
setExpandedFolders((prev) => {
|
|
300
307
|
const next = new Set(prev);
|
|
@@ -308,6 +315,10 @@ export default function CodeContextPanel() {
|
|
|
308
315
|
}, []);
|
|
309
316
|
|
|
310
317
|
const filter = search.toLowerCase();
|
|
318
|
+
const selectedSet = new Set(selectedFiles);
|
|
319
|
+
const allFilesSelected =
|
|
320
|
+
availableFiles.length > 0 &&
|
|
321
|
+
availableFiles.every((filePath) => selectedSet.has(filePath));
|
|
311
322
|
|
|
312
323
|
return (
|
|
313
324
|
<div className="w-72 h-full min-h-0 border-l border-slate-800 flex flex-col bg-slate-900/30 shrink-0 overflow-hidden">
|
|
@@ -317,7 +328,19 @@ export default function CodeContextPanel() {
|
|
|
317
328
|
<span className="text-xs font-bold uppercase tracking-wider text-slate-500">
|
|
318
329
|
Code Context
|
|
319
330
|
</span>
|
|
320
|
-
<
|
|
331
|
+
<div className="flex items-center gap-2">
|
|
332
|
+
{currentQuestion && availableFiles.length > 0 && (
|
|
333
|
+
<button
|
|
334
|
+
onClick={selectAllFiles}
|
|
335
|
+
disabled={allFilesSelected}
|
|
336
|
+
className="text-[10px] text-cyan-400/70 hover:text-cyan-300 disabled:text-slate-600 disabled:cursor-not-allowed"
|
|
337
|
+
title="Select all code-context files"
|
|
338
|
+
>
|
|
339
|
+
Select all
|
|
340
|
+
</button>
|
|
341
|
+
)}
|
|
342
|
+
<FolderOpen className="w-3.5 h-3.5 text-slate-600" />
|
|
343
|
+
</div>
|
|
321
344
|
</div>
|
|
322
345
|
<div className="relative">
|
|
323
346
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-600" />
|
|
@@ -2,8 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
ChevronDown,
|
|
4
4
|
ChevronRight,
|
|
5
|
+
Copy,
|
|
5
6
|
FilePlus,
|
|
6
7
|
Folder,
|
|
8
|
+
ListChecks,
|
|
7
9
|
Loader2,
|
|
8
10
|
Maximize2,
|
|
9
11
|
Minimize2,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
PanelLeftOpen,
|
|
12
14
|
PanelRightClose,
|
|
13
15
|
PanelRightOpen,
|
|
16
|
+
Pencil,
|
|
14
17
|
Play,
|
|
15
18
|
Save,
|
|
16
19
|
StopCircle,
|
|
@@ -66,6 +69,120 @@ function baseName(filePath: string): string {
|
|
|
66
69
|
return filePath.split("/").pop() || filePath;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
function folderName(filePath: string): string {
|
|
73
|
+
const idx = filePath.lastIndexOf("/");
|
|
74
|
+
return idx === -1 ? "" : filePath.slice(0, idx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function joinLabPath(folder: string, name: string): string {
|
|
78
|
+
return folder ? `${folder}/${name}` : name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getKnownFolders(paths: string[]): Set<string> {
|
|
82
|
+
const folders = new Set<string>([""]);
|
|
83
|
+
for (const filePath of paths) {
|
|
84
|
+
const parts = folderName(filePath).split("/").filter(Boolean);
|
|
85
|
+
for (let i = 1; i <= parts.length; i += 1) {
|
|
86
|
+
folders.add(parts.slice(0, i).join("/"));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return folders;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeDestinationInput(input: string): {
|
|
93
|
+
path: string;
|
|
94
|
+
isDirectoryHint: boolean;
|
|
95
|
+
error?: string;
|
|
96
|
+
} {
|
|
97
|
+
const raw = input.trim().replace(/\\/g, "/");
|
|
98
|
+
if (!raw) {
|
|
99
|
+
return {
|
|
100
|
+
path: "",
|
|
101
|
+
isDirectoryHint: false,
|
|
102
|
+
error: "Enter a destination path.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (raw.startsWith("/") || /^[a-zA-Z]:\//.test(raw)) {
|
|
106
|
+
return {
|
|
107
|
+
path: "",
|
|
108
|
+
isDirectoryHint: false,
|
|
109
|
+
error: "Use a relative path inside the lab workspace.",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isDirectoryHint = raw === "." || raw.endsWith("/");
|
|
114
|
+
let value = raw;
|
|
115
|
+
while (value.startsWith("./")) value = value.slice(2);
|
|
116
|
+
value = value.replace(/\/+/g, "/");
|
|
117
|
+
if (value === ".") value = "";
|
|
118
|
+
value = value.replace(/\/+$/, "");
|
|
119
|
+
|
|
120
|
+
const segments = value ? value.split("/") : [];
|
|
121
|
+
if (
|
|
122
|
+
segments.some(
|
|
123
|
+
(segment) =>
|
|
124
|
+
!segment ||
|
|
125
|
+
segment === "." ||
|
|
126
|
+
segment === ".." ||
|
|
127
|
+
segment.includes("\0"),
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
path: "",
|
|
132
|
+
isDirectoryHint: false,
|
|
133
|
+
error: "Destination paths cannot contain empty, '.', or '..' segments.",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { path: segments.join("/"), isDirectoryHint };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveDestinationPath(
|
|
141
|
+
input: string,
|
|
142
|
+
sourceFile: string,
|
|
143
|
+
knownFolders: Set<string>,
|
|
144
|
+
): string | null {
|
|
145
|
+
const normalized = normalizeDestinationInput(input);
|
|
146
|
+
if (normalized.error) {
|
|
147
|
+
window.alert(normalized.error);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let target = normalized.path;
|
|
152
|
+
if (normalized.isDirectoryHint || knownFolders.has(target)) {
|
|
153
|
+
target = joinLabPath(target, baseName(sourceFile));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!target) {
|
|
157
|
+
window.alert("Destination file path is required.");
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return target;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getCopyCandidatePath(
|
|
164
|
+
sourceFile: string,
|
|
165
|
+
existingPaths: Set<string>,
|
|
166
|
+
targetFolder = folderName(sourceFile),
|
|
167
|
+
): string {
|
|
168
|
+
const sourceName = baseName(sourceFile);
|
|
169
|
+
const dot = sourceName.lastIndexOf(".");
|
|
170
|
+
const hasExtension = dot > 0;
|
|
171
|
+
const stem = hasExtension ? sourceName.slice(0, dot) : sourceName;
|
|
172
|
+
const ext = hasExtension ? sourceName.slice(dot) : "";
|
|
173
|
+
let count = 1;
|
|
174
|
+
|
|
175
|
+
while (true) {
|
|
176
|
+
const marker = count === 1 ? "copy" : `copy-${count}`;
|
|
177
|
+
const nextName = hasExtension
|
|
178
|
+
? `${stem}.${marker}${ext}`
|
|
179
|
+
: `${sourceName}.${marker}`;
|
|
180
|
+
const candidate = joinLabPath(targetFolder, nextName);
|
|
181
|
+
if (!existingPaths.has(candidate)) return candidate;
|
|
182
|
+
count += 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
69
186
|
function getEditorLanguage(filePath: string): string {
|
|
70
187
|
const lower = filePath.toLowerCase();
|
|
71
188
|
if (lower.endsWith(".yml") || lower.endsWith(".yaml")) return "yaml";
|
|
@@ -305,6 +422,32 @@ export default function GithubActionsLabModal() {
|
|
|
305
422
|
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
|
|
306
423
|
() => new Set(),
|
|
307
424
|
);
|
|
425
|
+
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(
|
|
426
|
+
() => new Set(),
|
|
427
|
+
);
|
|
428
|
+
const [draggingFile, setDraggingFile] = useState<string | null>(null);
|
|
429
|
+
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
|
430
|
+
const [openFileMenu, setOpenFileMenu] = useState<string | null>(null);
|
|
431
|
+
const [bulkMenuOpen, setBulkMenuOpen] = useState(false);
|
|
432
|
+
// When false the row checkboxes stay hidden so the tree reads like a
|
|
433
|
+
// normal file list; flipped on by the toolbar toggle, by checking a
|
|
434
|
+
// single row via its hover affordance, or whenever something is selected.
|
|
435
|
+
const [selectMode, setSelectMode] = useState(false);
|
|
436
|
+
const selectedFileList = useMemo(
|
|
437
|
+
() => fileOrder.filter((filePath) => selectedFiles.has(filePath)),
|
|
438
|
+
[fileOrder, selectedFiles],
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
setSelectedFiles((prev) => {
|
|
443
|
+
const known = new Set(fileOrder);
|
|
444
|
+
const next = new Set(
|
|
445
|
+
Array.from(prev).filter((filePath) => known.has(filePath)),
|
|
446
|
+
);
|
|
447
|
+
return next.size === prev.size ? prev : next;
|
|
448
|
+
});
|
|
449
|
+
}, [fileOrder]);
|
|
450
|
+
|
|
308
451
|
const toggleFolder = (folder: string) => {
|
|
309
452
|
setCollapsedFolders((prev) => {
|
|
310
453
|
const next = new Set(prev);
|
|
@@ -314,6 +457,27 @@ export default function GithubActionsLabModal() {
|
|
|
314
457
|
});
|
|
315
458
|
};
|
|
316
459
|
|
|
460
|
+
const toggleFileSelection = (fileName: string) => {
|
|
461
|
+
setSelectedFiles((prev) => {
|
|
462
|
+
const next = new Set(prev);
|
|
463
|
+
if (next.has(fileName)) next.delete(fileName);
|
|
464
|
+
else next.add(fileName);
|
|
465
|
+
if (next.size > 0) setSelectMode(true);
|
|
466
|
+
return next;
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const toggleSelectAllFiles = () => {
|
|
471
|
+
setSelectedFiles((prev) => {
|
|
472
|
+
if (prev.size === fileOrder.length) {
|
|
473
|
+
setSelectMode(false);
|
|
474
|
+
return new Set();
|
|
475
|
+
}
|
|
476
|
+
setSelectMode(true);
|
|
477
|
+
return new Set(fileOrder);
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
|
|
317
481
|
const updateFile = (fileName: string, content: string) => {
|
|
318
482
|
setWorkspace((prev) => ({
|
|
319
483
|
...prev,
|
|
@@ -322,6 +486,325 @@ export default function GithubActionsLabModal() {
|
|
|
322
486
|
}));
|
|
323
487
|
};
|
|
324
488
|
|
|
489
|
+
const applyFilePathOperation = useCallback(
|
|
490
|
+
(
|
|
491
|
+
sourceFile: string,
|
|
492
|
+
requestedTarget: string,
|
|
493
|
+
operation: "move" | "copy",
|
|
494
|
+
collisionStrategy: "prompt" | "unique" = "prompt",
|
|
495
|
+
) => {
|
|
496
|
+
const sourceContent = workspace.files[sourceFile];
|
|
497
|
+
if (sourceContent === undefined) return;
|
|
498
|
+
|
|
499
|
+
const existingPaths = new Set(Object.keys(workspace.files));
|
|
500
|
+
let targetFile = requestedTarget;
|
|
501
|
+
if (
|
|
502
|
+
operation === "copy" &&
|
|
503
|
+
(targetFile === sourceFile ||
|
|
504
|
+
(collisionStrategy === "unique" && existingPaths.has(targetFile)))
|
|
505
|
+
) {
|
|
506
|
+
targetFile = getCopyCandidatePath(
|
|
507
|
+
sourceFile,
|
|
508
|
+
existingPaths,
|
|
509
|
+
folderName(targetFile),
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (operation === "move" && targetFile === sourceFile) return;
|
|
514
|
+
|
|
515
|
+
const overwrites =
|
|
516
|
+
Object.prototype.hasOwnProperty.call(workspace.files, targetFile) &&
|
|
517
|
+
targetFile !== sourceFile;
|
|
518
|
+
if (
|
|
519
|
+
overwrites &&
|
|
520
|
+
!window.confirm(`Overwrite ${targetFile} with ${sourceFile}?`)
|
|
521
|
+
) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
setWorkspace((prev) => {
|
|
526
|
+
const content = prev.files[sourceFile];
|
|
527
|
+
if (content === undefined) return prev;
|
|
528
|
+
|
|
529
|
+
const files = { ...prev.files };
|
|
530
|
+
if (operation === "move") {
|
|
531
|
+
delete files[sourceFile];
|
|
532
|
+
}
|
|
533
|
+
files[targetFile] = content;
|
|
534
|
+
|
|
535
|
+
const nextActiveFile =
|
|
536
|
+
operation === "copy" || prev.activeFile === sourceFile
|
|
537
|
+
? targetFile
|
|
538
|
+
: prev.activeFile;
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
...prev,
|
|
542
|
+
activeFile: nextActiveFile,
|
|
543
|
+
defaultWorkflow:
|
|
544
|
+
operation === "move" && prev.defaultWorkflow === sourceFile
|
|
545
|
+
? targetFile
|
|
546
|
+
: prev.defaultWorkflow,
|
|
547
|
+
files,
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (operation === "copy" || activeFile === sourceFile) {
|
|
552
|
+
setActiveFile(targetFile);
|
|
553
|
+
}
|
|
554
|
+
if (operation === "move" && workflow === sourceFile) {
|
|
555
|
+
setWorkflow(targetFile);
|
|
556
|
+
}
|
|
557
|
+
setSelectedFiles((prev) => {
|
|
558
|
+
if (!prev.has(sourceFile)) return prev;
|
|
559
|
+
const next = new Set(prev);
|
|
560
|
+
if (operation === "move") next.delete(sourceFile);
|
|
561
|
+
next.add(targetFile);
|
|
562
|
+
return next;
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
[activeFile, workflow, workspace.files],
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const applyBulkFolderOperation = useCallback(
|
|
569
|
+
(
|
|
570
|
+
sourceFiles: string[],
|
|
571
|
+
targetFolder: string,
|
|
572
|
+
operation: "move" | "copy",
|
|
573
|
+
) => {
|
|
574
|
+
const uniqueSources = Array.from(new Set(sourceFiles)).filter(
|
|
575
|
+
(fileName) => workspace.files[fileName] !== undefined,
|
|
576
|
+
);
|
|
577
|
+
if (uniqueSources.length === 0) return;
|
|
578
|
+
|
|
579
|
+
const existingPaths = new Set(Object.keys(workspace.files));
|
|
580
|
+
const plannedTargets = new Set<string>();
|
|
581
|
+
const pairs: Array<{ source: string; target: string }> = [];
|
|
582
|
+
|
|
583
|
+
for (const source of uniqueSources) {
|
|
584
|
+
let target = joinLabPath(targetFolder, baseName(source));
|
|
585
|
+
|
|
586
|
+
if (operation === "move" && target === source) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (operation === "copy") {
|
|
591
|
+
if (
|
|
592
|
+
target === source ||
|
|
593
|
+
existingPaths.has(target) ||
|
|
594
|
+
plannedTargets.has(target)
|
|
595
|
+
) {
|
|
596
|
+
target = getCopyCandidatePath(
|
|
597
|
+
source,
|
|
598
|
+
new Set([...existingPaths, ...plannedTargets]),
|
|
599
|
+
targetFolder,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
} else if (plannedTargets.has(target)) {
|
|
603
|
+
window.alert(
|
|
604
|
+
`Multiple selected files would become ${target}. Rename one first, or move them separately.`,
|
|
605
|
+
);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
pairs.push({ source, target });
|
|
610
|
+
plannedTargets.add(target);
|
|
611
|
+
if (operation === "copy") existingPaths.add(target);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (pairs.length === 0) return;
|
|
615
|
+
|
|
616
|
+
const conflicts =
|
|
617
|
+
operation === "move"
|
|
618
|
+
? pairs.filter(
|
|
619
|
+
({ source, target }) =>
|
|
620
|
+
source !== target && workspace.files[target] !== undefined,
|
|
621
|
+
)
|
|
622
|
+
: [];
|
|
623
|
+
if (conflicts.length > 0) {
|
|
624
|
+
const preview = conflicts
|
|
625
|
+
.slice(0, 4)
|
|
626
|
+
.map(({ target }) => target)
|
|
627
|
+
.join("\n");
|
|
628
|
+
const suffix = conflicts.length > 4 ? "\n…" : "";
|
|
629
|
+
if (
|
|
630
|
+
!window.confirm(
|
|
631
|
+
`Overwrite existing file${conflicts.length === 1 ? "" : "s"}?\n${preview}${suffix}`,
|
|
632
|
+
)
|
|
633
|
+
) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
setWorkspace((prev) => {
|
|
639
|
+
const files = { ...prev.files };
|
|
640
|
+
const materialized = pairs
|
|
641
|
+
.map(({ source, target }) => ({
|
|
642
|
+
source,
|
|
643
|
+
target,
|
|
644
|
+
content: prev.files[source],
|
|
645
|
+
}))
|
|
646
|
+
.filter(
|
|
647
|
+
(
|
|
648
|
+
entry,
|
|
649
|
+
): entry is { source: string; target: string; content: string } =>
|
|
650
|
+
entry.content !== undefined,
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
if (operation === "move") {
|
|
654
|
+
for (const { source } of materialized) delete files[source];
|
|
655
|
+
}
|
|
656
|
+
for (const { target, content } of materialized) files[target] = content;
|
|
657
|
+
|
|
658
|
+
const activeMapping = materialized.find(
|
|
659
|
+
({ source }) => source === prev.activeFile,
|
|
660
|
+
);
|
|
661
|
+
const defaultWorkflowMapping = materialized.find(
|
|
662
|
+
({ source }) => source === prev.defaultWorkflow,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
...prev,
|
|
667
|
+
activeFile:
|
|
668
|
+
operation === "copy"
|
|
669
|
+
? (materialized[0]?.target ?? prev.activeFile)
|
|
670
|
+
: (activeMapping?.target ?? prev.activeFile),
|
|
671
|
+
defaultWorkflow:
|
|
672
|
+
operation === "move" && defaultWorkflowMapping
|
|
673
|
+
? defaultWorkflowMapping.target
|
|
674
|
+
: prev.defaultWorkflow,
|
|
675
|
+
files,
|
|
676
|
+
};
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const activePair = pairs.find(({ source }) => source === activeFile);
|
|
680
|
+
if (operation === "copy") {
|
|
681
|
+
setActiveFile(pairs[0]?.target ?? activeFile);
|
|
682
|
+
} else if (activePair) {
|
|
683
|
+
setActiveFile(activePair.target);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const workflowPair = pairs.find(({ source }) => source === workflow);
|
|
687
|
+
if (operation === "move" && workflowPair) {
|
|
688
|
+
setWorkflow(workflowPair.target);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
setSelectedFiles(new Set(pairs.map(({ target }) => target)));
|
|
692
|
+
},
|
|
693
|
+
[activeFile, workflow, workspace.files],
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const resolveBulkFolderPath = (input: string): string | null => {
|
|
697
|
+
const normalized = normalizeDestinationInput(input);
|
|
698
|
+
if (normalized.error) {
|
|
699
|
+
window.alert(normalized.error);
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
return normalized.path;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const moveFilesToFolder = (fileNames: string[]) => {
|
|
706
|
+
if (fileNames.length === 0) return;
|
|
707
|
+
const next = window.prompt(
|
|
708
|
+
`Move ${fileNames.length} file${fileNames.length === 1 ? "" : "s"} into folder path. Use . for the workspace root.`,
|
|
709
|
+
folderName(fileNames[0]) || ".",
|
|
710
|
+
);
|
|
711
|
+
if (!next) return;
|
|
712
|
+
const targetFolder = resolveBulkFolderPath(next);
|
|
713
|
+
if (targetFolder === null) return;
|
|
714
|
+
applyBulkFolderOperation(fileNames, targetFolder, "move");
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const copyFilesToFolder = (fileNames: string[]) => {
|
|
718
|
+
if (fileNames.length === 0) return;
|
|
719
|
+
const next = window.prompt(
|
|
720
|
+
`Copy ${fileNames.length} file${fileNames.length === 1 ? "" : "s"} into folder path. Use . for the workspace root.`,
|
|
721
|
+
folderName(fileNames[0]) || ".",
|
|
722
|
+
);
|
|
723
|
+
if (!next) return;
|
|
724
|
+
const targetFolder = resolveBulkFolderPath(next);
|
|
725
|
+
if (targetFolder === null) return;
|
|
726
|
+
applyBulkFolderOperation(fileNames, targetFolder, "copy");
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const moveFile = (fileName: string) => {
|
|
730
|
+
const next = window.prompt(
|
|
731
|
+
"Move or rename file. Enter a destination path, or an existing folder / folder ending in / to keep the same file name.",
|
|
732
|
+
fileName,
|
|
733
|
+
);
|
|
734
|
+
if (!next) return;
|
|
735
|
+
const target = resolveDestinationPath(
|
|
736
|
+
next,
|
|
737
|
+
fileName,
|
|
738
|
+
getKnownFolders(Object.keys(workspace.files)),
|
|
739
|
+
);
|
|
740
|
+
if (!target) return;
|
|
741
|
+
applyFilePathOperation(fileName, target, "move");
|
|
742
|
+
setOpenFileMenu(null);
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const copyFile = (fileName: string) => {
|
|
746
|
+
const next = window.prompt(
|
|
747
|
+
"Copy file. Enter a destination path, or an existing folder / folder ending in / to keep the same file name.",
|
|
748
|
+
getCopyCandidatePath(fileName, new Set(Object.keys(workspace.files))),
|
|
749
|
+
);
|
|
750
|
+
if (!next) return;
|
|
751
|
+
const target = resolveDestinationPath(
|
|
752
|
+
next,
|
|
753
|
+
fileName,
|
|
754
|
+
getKnownFolders(Object.keys(workspace.files)),
|
|
755
|
+
);
|
|
756
|
+
if (!target) return;
|
|
757
|
+
applyFilePathOperation(fileName, target, "copy");
|
|
758
|
+
setOpenFileMenu(null);
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const handleFileDragStart = (
|
|
762
|
+
e: React.DragEvent<HTMLDivElement>,
|
|
763
|
+
fileName: string,
|
|
764
|
+
) => {
|
|
765
|
+
setDraggingFile(fileName);
|
|
766
|
+
e.dataTransfer.effectAllowed = "copyMove";
|
|
767
|
+
e.dataTransfer.setData("text/x-gha-lab-file", fileName);
|
|
768
|
+
e.dataTransfer.setData("text/plain", fileName);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const handleFolderDragOver = (
|
|
772
|
+
e: React.DragEvent<HTMLElement>,
|
|
773
|
+
folder: string,
|
|
774
|
+
) => {
|
|
775
|
+
if (!draggingFile) return;
|
|
776
|
+
e.preventDefault();
|
|
777
|
+
e.dataTransfer.dropEffect = e.altKey ? "copy" : "move";
|
|
778
|
+
setDragOverFolder(folder);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const handleFolderDrop = (
|
|
782
|
+
e: React.DragEvent<HTMLElement>,
|
|
783
|
+
folder: string,
|
|
784
|
+
) => {
|
|
785
|
+
e.preventDefault();
|
|
786
|
+
const sourceFile =
|
|
787
|
+
e.dataTransfer.getData("text/x-gha-lab-file") || draggingFile;
|
|
788
|
+
setDraggingFile(null);
|
|
789
|
+
setDragOverFolder(null);
|
|
790
|
+
if (!sourceFile) return;
|
|
791
|
+
|
|
792
|
+
const operation = e.altKey ? "copy" : "move";
|
|
793
|
+
const sourceGroup = selectedFiles.has(sourceFile)
|
|
794
|
+
? selectedFileList
|
|
795
|
+
: [sourceFile];
|
|
796
|
+
if (sourceGroup.length > 1) {
|
|
797
|
+
applyBulkFolderOperation(sourceGroup, folder, operation);
|
|
798
|
+
} else {
|
|
799
|
+
applyFilePathOperation(
|
|
800
|
+
sourceFile,
|
|
801
|
+
joinLabPath(folder, baseName(sourceFile)),
|
|
802
|
+
operation,
|
|
803
|
+
operation === "copy" ? "unique" : "prompt",
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
325
808
|
const addFile = () => {
|
|
326
809
|
const name = window.prompt(
|
|
327
810
|
"New file path (e.g. .github/workflows/release.yml)",
|
|
@@ -354,6 +837,41 @@ export default function GithubActionsLabModal() {
|
|
|
354
837
|
);
|
|
355
838
|
setActiveFile(remaining[0] ?? "");
|
|
356
839
|
}
|
|
840
|
+
setSelectedFiles((prev) => {
|
|
841
|
+
if (!prev.has(fileName)) return prev;
|
|
842
|
+
const next = new Set(prev);
|
|
843
|
+
next.delete(fileName);
|
|
844
|
+
return next;
|
|
845
|
+
});
|
|
846
|
+
setOpenFileMenu(null);
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const deleteSelectedFiles = () => {
|
|
850
|
+
if (selectedFileList.length === 0) return;
|
|
851
|
+
if (
|
|
852
|
+
!window.confirm(
|
|
853
|
+
`Delete ${selectedFileList.length} selected file${selectedFileList.length === 1 ? "" : "s"}?`,
|
|
854
|
+
)
|
|
855
|
+
) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const selected = new Set(selectedFileList);
|
|
859
|
+
setWorkspace((prev) => {
|
|
860
|
+
const files = { ...prev.files };
|
|
861
|
+
for (const fileName of selected) delete files[fileName];
|
|
862
|
+
const nextActive = selected.has(prev.activeFile)
|
|
863
|
+
? (Object.keys(files)[0] ?? "")
|
|
864
|
+
: prev.activeFile;
|
|
865
|
+
return { ...prev, activeFile: nextActive, files };
|
|
866
|
+
});
|
|
867
|
+
if (selected.has(activeFile)) {
|
|
868
|
+
const remaining = Object.keys(workspace.files).filter(
|
|
869
|
+
(fileName) => !selected.has(fileName),
|
|
870
|
+
);
|
|
871
|
+
setActiveFile(remaining[0] ?? "");
|
|
872
|
+
}
|
|
873
|
+
setSelectedFiles(new Set());
|
|
874
|
+
setBulkMenuOpen(false);
|
|
357
875
|
};
|
|
358
876
|
|
|
359
877
|
// ── Save lab as context file ──────────────────────────────────────
|
|
@@ -966,18 +1484,132 @@ interface ImportMeta {
|
|
|
966
1484
|
}}
|
|
967
1485
|
>
|
|
968
1486
|
<div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-800/60">
|
|
969
|
-
<
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
</
|
|
1487
|
+
<div className="flex items-center gap-1 min-w-0">
|
|
1488
|
+
<span className="text-[10px] font-semibold tracking-widest text-slate-500">
|
|
1489
|
+
FILES
|
|
1490
|
+
</span>
|
|
1491
|
+
{selectedFileList.length > 0 && (
|
|
1492
|
+
<span className="rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-medium text-amber-200">
|
|
1493
|
+
{selectedFileList.length}
|
|
1494
|
+
</span>
|
|
1495
|
+
)}
|
|
1496
|
+
</div>
|
|
1497
|
+
<div className="relative flex items-center gap-1">
|
|
1498
|
+
<button
|
|
1499
|
+
onClick={(e) => {
|
|
1500
|
+
e.stopPropagation();
|
|
1501
|
+
setSelectMode((prev) => {
|
|
1502
|
+
const next = !prev;
|
|
1503
|
+
if (!next) setSelectedFiles(new Set());
|
|
1504
|
+
return next;
|
|
1505
|
+
});
|
|
1506
|
+
setBulkMenuOpen(false);
|
|
1507
|
+
setOpenFileMenu(null);
|
|
1508
|
+
}}
|
|
1509
|
+
className={`p-1 rounded hover:bg-slate-800/60 ${
|
|
1510
|
+
selectMode
|
|
1511
|
+
? "text-amber-300"
|
|
1512
|
+
: "text-slate-400 hover:text-amber-300"
|
|
1513
|
+
}`}
|
|
1514
|
+
title={selectMode ? "Exit selection mode" : "Select files"}
|
|
1515
|
+
>
|
|
1516
|
+
<ListChecks className="w-3.5 h-3.5" />
|
|
1517
|
+
</button>
|
|
1518
|
+
{selectedFileList.length > 0 && (
|
|
1519
|
+
<>
|
|
1520
|
+
<button
|
|
1521
|
+
onClick={(e) => {
|
|
1522
|
+
e.stopPropagation();
|
|
1523
|
+
setBulkMenuOpen((v) => !v);
|
|
1524
|
+
setOpenFileMenu(null);
|
|
1525
|
+
}}
|
|
1526
|
+
className="rounded px-1.5 py-0.5 text-[11px] text-slate-400 hover:bg-slate-800/60 hover:text-amber-200"
|
|
1527
|
+
title="Selected file actions"
|
|
1528
|
+
>
|
|
1529
|
+
Selected ▾
|
|
1530
|
+
</button>
|
|
1531
|
+
{bulkMenuOpen && (
|
|
1532
|
+
<div
|
|
1533
|
+
onClick={(e) => e.stopPropagation()}
|
|
1534
|
+
className="absolute right-6 top-6 z-40 w-44 overflow-hidden rounded-lg border border-slate-700 bg-slate-950 py-1 text-[11px] shadow-xl"
|
|
1535
|
+
>
|
|
1536
|
+
<button
|
|
1537
|
+
onClick={() => {
|
|
1538
|
+
moveFilesToFolder(selectedFileList);
|
|
1539
|
+
setBulkMenuOpen(false);
|
|
1540
|
+
}}
|
|
1541
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
|
|
1542
|
+
>
|
|
1543
|
+
<Pencil className="w-3 h-3 text-amber-300" />
|
|
1544
|
+
Move to folder…
|
|
1545
|
+
</button>
|
|
1546
|
+
<button
|
|
1547
|
+
onClick={() => {
|
|
1548
|
+
copyFilesToFolder(selectedFileList);
|
|
1549
|
+
setBulkMenuOpen(false);
|
|
1550
|
+
}}
|
|
1551
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
|
|
1552
|
+
>
|
|
1553
|
+
<Copy className="w-3 h-3 text-sky-300" />
|
|
1554
|
+
Copy to folder…
|
|
1555
|
+
</button>
|
|
1556
|
+
<button
|
|
1557
|
+
onClick={() => {
|
|
1558
|
+
setSelectedFiles(new Set());
|
|
1559
|
+
setSelectMode(false);
|
|
1560
|
+
setBulkMenuOpen(false);
|
|
1561
|
+
}}
|
|
1562
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-400 hover:bg-slate-800/70"
|
|
1563
|
+
>
|
|
1564
|
+
Clear selection
|
|
1565
|
+
</button>
|
|
1566
|
+
<button
|
|
1567
|
+
onClick={deleteSelectedFiles}
|
|
1568
|
+
className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-red-300 hover:bg-red-500/10"
|
|
1569
|
+
>
|
|
1570
|
+
<Trash2 className="w-3 h-3" />
|
|
1571
|
+
Delete selected
|
|
1572
|
+
</button>
|
|
1573
|
+
</div>
|
|
1574
|
+
)}
|
|
1575
|
+
</>
|
|
1576
|
+
)}
|
|
1577
|
+
<button
|
|
1578
|
+
onClick={addFile}
|
|
1579
|
+
className="p-1 rounded text-slate-400 hover:text-amber-300 hover:bg-slate-800/60"
|
|
1580
|
+
title="Add file"
|
|
1581
|
+
>
|
|
1582
|
+
<FilePlus className="w-3.5 h-3.5" />
|
|
1583
|
+
</button>
|
|
1584
|
+
</div>
|
|
979
1585
|
</div>
|
|
980
|
-
<div
|
|
1586
|
+
<div
|
|
1587
|
+
className="flex-1 overflow-auto p-1 text-xs"
|
|
1588
|
+
onClick={() => {
|
|
1589
|
+
setOpenFileMenu(null);
|
|
1590
|
+
setBulkMenuOpen(false);
|
|
1591
|
+
}}
|
|
1592
|
+
>
|
|
1593
|
+
{draggingFile && (
|
|
1594
|
+
<div
|
|
1595
|
+
onDragOver={(e) => handleFolderDragOver(e, "")}
|
|
1596
|
+
onDragLeave={() =>
|
|
1597
|
+
setDragOverFolder((current) =>
|
|
1598
|
+
current === "" ? null : current,
|
|
1599
|
+
)
|
|
1600
|
+
}
|
|
1601
|
+
onDrop={(e) => handleFolderDrop(e, "")}
|
|
1602
|
+
className={`mb-1 rounded border border-dashed px-2 py-1 text-[11px] ${
|
|
1603
|
+
dragOverFolder === ""
|
|
1604
|
+
? "border-amber-400/70 bg-amber-500/10 text-amber-200"
|
|
1605
|
+
: "border-slate-700 text-slate-500"
|
|
1606
|
+
}`}
|
|
1607
|
+
>
|
|
1608
|
+
Drop{" "}
|
|
1609
|
+
{selectedFiles.has(draggingFile) ? "selected files" : "file"}{" "}
|
|
1610
|
+
in workspace root • hold Option/Alt to copy
|
|
1611
|
+
</div>
|
|
1612
|
+
)}
|
|
981
1613
|
{grouped.map(({ folder, files }) => {
|
|
982
1614
|
const collapsed = collapsedFolders.has(folder);
|
|
983
1615
|
return (
|
|
@@ -985,7 +1617,15 @@ interface ImportMeta {
|
|
|
985
1617
|
{folder && (
|
|
986
1618
|
<button
|
|
987
1619
|
onClick={() => toggleFolder(folder)}
|
|
1620
|
+
onDragOver={(e) => handleFolderDragOver(e, folder)}
|
|
1621
|
+
onDragLeave={() =>
|
|
1622
|
+
setDragOverFolder((current) =>
|
|
1623
|
+
current === folder ? null : current,
|
|
1624
|
+
)
|
|
1625
|
+
}
|
|
1626
|
+
onDrop={(e) => handleFolderDrop(e, folder)}
|
|
988
1627
|
className="flex items-center gap-1 w-full px-1 py-0.5 text-slate-400 hover:text-slate-200"
|
|
1628
|
+
title="Drop a file here to move it. Hold Option/Alt while dropping to copy."
|
|
989
1629
|
>
|
|
990
1630
|
{collapsed ? (
|
|
991
1631
|
<ChevronRight className="w-3 h-3" />
|
|
@@ -993,34 +1633,127 @@ interface ImportMeta {
|
|
|
993
1633
|
<ChevronDown className="w-3 h-3" />
|
|
994
1634
|
)}
|
|
995
1635
|
<Folder className="w-3 h-3" />
|
|
996
|
-
<span
|
|
1636
|
+
<span
|
|
1637
|
+
className={`truncate rounded px-1 ${
|
|
1638
|
+
dragOverFolder === folder
|
|
1639
|
+
? "bg-amber-500/15 text-amber-200"
|
|
1640
|
+
: ""
|
|
1641
|
+
}`}
|
|
1642
|
+
>
|
|
1643
|
+
{folder}/
|
|
1644
|
+
</span>
|
|
997
1645
|
</button>
|
|
998
1646
|
)}
|
|
999
1647
|
{!collapsed &&
|
|
1000
1648
|
files.map((filePath) => (
|
|
1001
1649
|
<div
|
|
1002
1650
|
key={filePath}
|
|
1003
|
-
|
|
1651
|
+
data-selected={selectedFiles.has(filePath)}
|
|
1652
|
+
draggable
|
|
1653
|
+
onDragStart={(e) => handleFileDragStart(e, filePath)}
|
|
1654
|
+
onDragEnd={() => {
|
|
1655
|
+
setDraggingFile(null);
|
|
1656
|
+
setDragOverFolder(null);
|
|
1657
|
+
}}
|
|
1658
|
+
className={`group relative flex items-center gap-1 pl-${folder ? 5 : 1} pr-1 py-0.5 rounded cursor-pointer ${
|
|
1004
1659
|
activeFile === filePath
|
|
1005
1660
|
? "bg-amber-500/15 text-amber-200"
|
|
1006
|
-
:
|
|
1661
|
+
: selectedFiles.has(filePath)
|
|
1662
|
+
? "bg-sky-500/10 text-sky-100 hover:bg-sky-500/15"
|
|
1663
|
+
: "text-slate-300 hover:bg-slate-800/40"
|
|
1007
1664
|
}`}
|
|
1008
1665
|
onClick={() => setActiveFile(filePath)}
|
|
1009
1666
|
style={{ paddingLeft: folder ? 20 : 6 }}
|
|
1010
1667
|
>
|
|
1668
|
+
{(selectMode || selectedFiles.has(filePath)) && (
|
|
1669
|
+
<input
|
|
1670
|
+
type="checkbox"
|
|
1671
|
+
checked={selectedFiles.has(filePath)}
|
|
1672
|
+
onClick={(e) => e.stopPropagation()}
|
|
1673
|
+
onChange={() => toggleFileSelection(filePath)}
|
|
1674
|
+
className="h-3 w-3 shrink-0 accent-amber-400"
|
|
1675
|
+
title="Select file"
|
|
1676
|
+
/>
|
|
1677
|
+
)}
|
|
1011
1678
|
<span className="truncate flex-1">
|
|
1012
1679
|
{baseName(filePath)}
|
|
1013
1680
|
</span>
|
|
1014
1681
|
<button
|
|
1015
1682
|
onClick={(e) => {
|
|
1016
1683
|
e.stopPropagation();
|
|
1017
|
-
|
|
1684
|
+
setOpenFileMenu((current) =>
|
|
1685
|
+
current === filePath ? null : filePath,
|
|
1686
|
+
);
|
|
1687
|
+
setBulkMenuOpen(false);
|
|
1018
1688
|
}}
|
|
1019
|
-
className="
|
|
1020
|
-
title="
|
|
1689
|
+
className="rounded px-1 text-slate-500 opacity-70 hover:bg-slate-800/70 hover:text-amber-200 group-hover:opacity-100"
|
|
1690
|
+
title="File actions"
|
|
1021
1691
|
>
|
|
1022
|
-
|
|
1692
|
+
⋯
|
|
1023
1693
|
</button>
|
|
1694
|
+
{openFileMenu === filePath && (
|
|
1695
|
+
<div
|
|
1696
|
+
onClick={(e) => e.stopPropagation()}
|
|
1697
|
+
className="absolute right-1 top-6 z-40 w-40 overflow-hidden rounded-lg border border-slate-700 bg-slate-950 py-1 text-[11px] shadow-xl"
|
|
1698
|
+
>
|
|
1699
|
+
<button
|
|
1700
|
+
onClick={() => moveFile(filePath)}
|
|
1701
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
|
|
1702
|
+
>
|
|
1703
|
+
<Pencil className="w-3 h-3 text-amber-300" />
|
|
1704
|
+
Move / rename…
|
|
1705
|
+
</button>
|
|
1706
|
+
<button
|
|
1707
|
+
onClick={() => copyFile(filePath)}
|
|
1708
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
|
|
1709
|
+
>
|
|
1710
|
+
<Copy className="w-3 h-3 text-sky-300" />
|
|
1711
|
+
Copy to path…
|
|
1712
|
+
</button>
|
|
1713
|
+
<button
|
|
1714
|
+
onClick={() => {
|
|
1715
|
+
toggleFileSelection(filePath);
|
|
1716
|
+
setOpenFileMenu(null);
|
|
1717
|
+
}}
|
|
1718
|
+
className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
|
|
1719
|
+
>
|
|
1720
|
+
<ListChecks className="w-3 h-3 text-amber-300" />
|
|
1721
|
+
{selectedFiles.has(filePath)
|
|
1722
|
+
? "Deselect"
|
|
1723
|
+
: "Select"}
|
|
1724
|
+
</button>
|
|
1725
|
+
{selectedFileList.length > 1 &&
|
|
1726
|
+
selectedFiles.has(filePath) && (
|
|
1727
|
+
<>
|
|
1728
|
+
<button
|
|
1729
|
+
onClick={() => {
|
|
1730
|
+
moveFilesToFolder(selectedFileList);
|
|
1731
|
+
setOpenFileMenu(null);
|
|
1732
|
+
}}
|
|
1733
|
+
className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
|
|
1734
|
+
>
|
|
1735
|
+
Move selected…
|
|
1736
|
+
</button>
|
|
1737
|
+
<button
|
|
1738
|
+
onClick={() => {
|
|
1739
|
+
copyFilesToFolder(selectedFileList);
|
|
1740
|
+
setOpenFileMenu(null);
|
|
1741
|
+
}}
|
|
1742
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
|
|
1743
|
+
>
|
|
1744
|
+
Copy selected…
|
|
1745
|
+
</button>
|
|
1746
|
+
</>
|
|
1747
|
+
)}
|
|
1748
|
+
<button
|
|
1749
|
+
onClick={() => deleteFile(filePath)}
|
|
1750
|
+
className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-red-300 hover:bg-red-500/10"
|
|
1751
|
+
>
|
|
1752
|
+
<Trash2 className="w-3 h-3" />
|
|
1753
|
+
Delete
|
|
1754
|
+
</button>
|
|
1755
|
+
</div>
|
|
1756
|
+
)}
|
|
1024
1757
|
</div>
|
|
1025
1758
|
))}
|
|
1026
1759
|
</div>
|
package/template/cockpit.json
CHANGED
|
@@ -105,6 +105,21 @@ const ALLOWED_ACT_FLAGS = new Set([
|
|
|
105
105
|
"-v",
|
|
106
106
|
]);
|
|
107
107
|
|
|
108
|
+
// act asks an interactive first-run question for runner image size unless it
|
|
109
|
+
// already has platform mappings. The lab console cannot answer arrow-key
|
|
110
|
+
// prompts cleanly, so we inject the Medium image mappings by default. Users can
|
|
111
|
+
// still override with -P / --platform in the command.
|
|
112
|
+
const DEFAULT_ACT_PLATFORM_ARGS = [
|
|
113
|
+
"-P",
|
|
114
|
+
"ubuntu-latest=catthehacker/ubuntu:act-latest",
|
|
115
|
+
"-P",
|
|
116
|
+
"ubuntu-24.04=catthehacker/ubuntu:act-24.04",
|
|
117
|
+
"-P",
|
|
118
|
+
"ubuntu-22.04=catthehacker/ubuntu:act-22.04",
|
|
119
|
+
"-P",
|
|
120
|
+
"ubuntu-20.04=catthehacker/ubuntu:act-20.04",
|
|
121
|
+
];
|
|
122
|
+
|
|
108
123
|
// ─── Utilities ───────────────────────────────────────────────────────────
|
|
109
124
|
|
|
110
125
|
function getGhaRunsDir(): string {
|
|
@@ -304,6 +319,24 @@ interface ParsedActCommand {
|
|
|
304
319
|
displayCommand: string;
|
|
305
320
|
}
|
|
306
321
|
|
|
322
|
+
function hasPlatformOverride(args: string[]): boolean {
|
|
323
|
+
return args.some(
|
|
324
|
+
(arg) =>
|
|
325
|
+
arg === "-P" ||
|
|
326
|
+
arg === "--platform" ||
|
|
327
|
+
arg.startsWith("-P=") ||
|
|
328
|
+
arg.startsWith("--platform="),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function withDefaultActPlatforms(args: string[]): {
|
|
333
|
+
args: string[];
|
|
334
|
+
injected: boolean;
|
|
335
|
+
} {
|
|
336
|
+
if (hasPlatformOverride(args)) return { args, injected: false };
|
|
337
|
+
return { args: [...args, ...DEFAULT_ACT_PLATFORM_ARGS], injected: true };
|
|
338
|
+
}
|
|
339
|
+
|
|
307
340
|
function parseActCommand(command: string): ParsedActCommand {
|
|
308
341
|
const tokens = splitCommand(command);
|
|
309
342
|
if (tokens.length === 0) throw new Error("Type a command to run");
|
|
@@ -681,6 +714,7 @@ export async function streamGhaCommand(
|
|
|
681
714
|
const workspace = parseWorkspace(input.workspace);
|
|
682
715
|
|
|
683
716
|
const parsed = parseActCommand(input.command);
|
|
717
|
+
const platformArgs = withDefaultActPlatforms(parsed.args);
|
|
684
718
|
const sessionKey =
|
|
685
719
|
input.fileId ?? input.questionId ?? `draft-${randomUUID()}`;
|
|
686
720
|
const runId = randomUUID();
|
|
@@ -696,12 +730,19 @@ export async function streamGhaCommand(
|
|
|
696
730
|
|
|
697
731
|
const emit = (msg: GhaStreamMessage) => input.onMessage?.(msg);
|
|
698
732
|
emit({ type: "output", kind: "info", text: parsed.displayCommand });
|
|
733
|
+
if (platformArgs.injected) {
|
|
734
|
+
emit({
|
|
735
|
+
type: "output",
|
|
736
|
+
kind: "info",
|
|
737
|
+
text: "[info] Using default Medium act runner image mappings. Pass -P/--platform to override.\n",
|
|
738
|
+
});
|
|
739
|
+
}
|
|
699
740
|
|
|
700
741
|
// Track per-job status from act's prefixed stdout/stderr lines so the
|
|
701
742
|
// client can render a live DAG in addition to the raw console.
|
|
702
743
|
const tracker = new JobTracker((job) => emit({ type: "job", job }));
|
|
703
744
|
|
|
704
|
-
const child = spawn("act",
|
|
745
|
+
const child = spawn("act", platformArgs.args, {
|
|
705
746
|
cwd: workspaceDir,
|
|
706
747
|
env: {
|
|
707
748
|
...process.env,
|