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.
@@ -1,7 +1,11 @@
1
1
  import { useState } from "react";
2
2
  import { useStore } from "../store";
3
3
  import { DOCKER_DEEP_DIVE_LAB, parseInfraLabWorkspace } from "../infraLab";
4
- import { DEFAULT_GHA_LAB, parseGhaLabWorkspace } from "../githubActionsLab";
4
+ import {
5
+ DEFAULT_GHA_LAB,
6
+ parseGhaLabWorkspace,
7
+ REACT_VITE_TYPESCRIPT_GHA_LAB,
8
+ } from "../githubActionsLab";
5
9
  import { ENTERPRISE_LOCAL_AUTH_LAB } from "../enterpriseLocalLab";
6
10
  import {
7
11
  parseFrontendLabWorkspace,
@@ -656,6 +660,12 @@ export default function LabsPanel() {
656
660
  origin="github-actions"
657
661
  emptyText="Save a GitHub Actions lab to reopen it here"
658
662
  newLabMenu={[
663
+ {
664
+ label: "React Vite TypeScript Starter",
665
+ description:
666
+ "Scaffolded React app with an empty .github/workflows/ci.yml",
667
+ onClick: () => openGhaLab(REACT_VITE_TYPESCRIPT_GHA_LAB),
668
+ },
659
669
  {
660
670
  label: "Workflows + Composite Action",
661
671
  description:
@@ -119,7 +119,9 @@ export default function Sidebar() {
119
119
  selectDriveSubfolder,
120
120
  clearDriveSubfolder,
121
121
  syncWorkspace,
122
+ syncTopic,
122
123
  exportWorkspace,
124
+ exportTopic,
123
125
  workspaceFiles,
124
126
  uploadWorkspaceFiles,
125
127
  removeWorkspaceFile,
@@ -162,6 +164,7 @@ export default function Sidebar() {
162
164
  const [openMenuQuestionId, setOpenMenuQuestionId] = useState<string | null>(
163
165
  null,
164
166
  );
167
+ const [openMenuTopicId, setOpenMenuTopicId] = useState<string | null>(null);
165
168
  const [openTopicPrompts, setOpenTopicPrompts] = useState<Set<string>>(
166
169
  new Set(),
167
170
  );
@@ -216,6 +219,11 @@ export default function Sidebar() {
216
219
  const [navigating, setNavigating] = useState(false);
217
220
  const [syncing, setSyncing] = useState(false);
218
221
  const [pushing, setPushing] = useState(false);
222
+ const [topicSyncingId, setTopicSyncingId] = useState<string | null>(null);
223
+ const [topicPushingId, setTopicPushingId] = useState<string | null>(null);
224
+ const [topicDriveStatus, setTopicDriveStatus] = useState<
225
+ Record<string, string>
226
+ >({});
219
227
  const [wsFilesExpanded, setWsFilesExpanded] = useState(true);
220
228
  const [driveFileSyncStatus, setDriveFileSyncStatus] = useState<string | null>(
221
229
  null,
@@ -294,6 +302,61 @@ export default function Sidebar() {
294
302
  }
295
303
  };
296
304
 
305
+ const setTopicStatus = (topicId: string, value: string) => {
306
+ setTopicDriveStatus((prev) => ({ ...prev, [topicId]: value }));
307
+ };
308
+
309
+ const handlePullTopicFromDrive = async (topicId: string) => {
310
+ if (!activeWorkspaceId || !activeWs?.driveConfig?.folderId) return;
311
+ setTopicSyncingId(topicId);
312
+ setTopicStatus(topicId, "Pulling topic from Drive…");
313
+ try {
314
+ const result = await syncTopic(activeWorkspaceId, topicId);
315
+ if ("needsAuth" in result && result.needsAuth) {
316
+ window.location.href = result.authUrl;
317
+ return;
318
+ }
319
+ const firstError = result.errors[0];
320
+ setTopicStatus(
321
+ topicId,
322
+ result.errors.length > 0
323
+ ? `Topic pull finished with ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}. ${firstError ? `First: ${firstError}` : ""}`
324
+ : `Pulled ${result.filesImported} file${result.filesImported === 1 ? "" : "s"} into this topic.`,
325
+ );
326
+ } catch (err: any) {
327
+ setTopicStatus(topicId, err?.message || "Topic pull failed.");
328
+ } finally {
329
+ setTopicSyncingId(null);
330
+ }
331
+ };
332
+
333
+ const handlePushTopicToDrive = async (topicId: string) => {
334
+ if (!activeWorkspaceId || !activeWs?.driveConfig?.folderId) return;
335
+ setTopicPushingId(topicId);
336
+ setTopicStatus(topicId, "Pushing topic to Drive…");
337
+ try {
338
+ const result = await exportTopic(
339
+ activeWorkspaceId,
340
+ topicId,
341
+ activeWs.driveConfig.subFolderId,
342
+ );
343
+ if ("needsAuth" in result && result.needsAuth) {
344
+ window.location.href = result.authUrl;
345
+ return;
346
+ }
347
+ setTopicStatus(
348
+ topicId,
349
+ result.errors.length > 0
350
+ ? `Topic push finished with ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}.`
351
+ : `Pushed ${result.questionsExported} question${result.questionsExported === 1 ? "" : "s"} and ${result.filesExported} file${result.filesExported === 1 ? "" : "s"} to Drive.`,
352
+ );
353
+ } catch (err: any) {
354
+ setTopicStatus(topicId, err?.message || "Topic push failed.");
355
+ } finally {
356
+ setTopicPushingId(null);
357
+ }
358
+ };
359
+
297
360
  useEffect(() => {
298
361
  if (editingTopicId || editingQuestionId) {
299
362
  editInputRef.current?.select();
@@ -1194,6 +1257,9 @@ export default function Sidebar() {
1194
1257
  sensitivity: "base",
1195
1258
  }),
1196
1259
  );
1260
+ const isTopicMenuOpen = openMenuTopicId === topic.id;
1261
+ const topicBusy =
1262
+ topicSyncingId === topic.id || topicPushingId === topic.id;
1197
1263
 
1198
1264
  return (
1199
1265
  <div key={topic.id}>
@@ -1238,69 +1304,160 @@ export default function Sidebar() {
1238
1304
  </span>
1239
1305
  </button>
1240
1306
  {editingTopicId !== topic.id && (
1241
- <button
1242
- onClick={(e) => {
1243
- e.stopPropagation();
1244
- setEditingTopicId(topic.id);
1245
- setEditingTopicName(topic.name);
1246
- }}
1247
- className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
1248
- title="Rename"
1307
+ <div
1308
+ className="relative shrink-0 flex items-center"
1309
+ onClick={(e) => e.stopPropagation()}
1249
1310
  >
1250
- <Pencil className="w-3 h-3" />
1251
- </button>
1311
+ {topicBusy && !isTopicMenuOpen ? (
1312
+ <Loader2 className="w-3 h-3 animate-spin text-cyan-400" />
1313
+ ) : (
1314
+ <button
1315
+ onClick={() =>
1316
+ setOpenMenuTopicId(
1317
+ isTopicMenuOpen ? null : topic.id,
1318
+ )
1319
+ }
1320
+ className={`p-0.5 rounded transition-all ${
1321
+ isTopicMenuOpen
1322
+ ? "text-cyan-400"
1323
+ : "opacity-0 group-hover:opacity-100 text-slate-600 hover:text-slate-300"
1324
+ }`}
1325
+ title="Topic options"
1326
+ >
1327
+ <MoreHorizontal className="w-3.5 h-3.5" />
1328
+ </button>
1329
+ )}
1330
+
1331
+ {isTopicMenuOpen && (
1332
+ <>
1333
+ <div
1334
+ className="fixed inset-0 z-40"
1335
+ onClick={() => setOpenMenuTopicId(null)}
1336
+ />
1337
+ <div className="absolute right-0 top-full mt-0.5 z-50 bg-slate-800 border border-slate-700 rounded-md shadow-xl min-w-[170px] py-0.5">
1338
+ <button
1339
+ onClick={() => {
1340
+ setOpenMenuTopicId(null);
1341
+ setEditingTopicId(topic.id);
1342
+ setEditingTopicName(topic.name);
1343
+ }}
1344
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
1345
+ >
1346
+ <Pencil className="w-3 h-3" /> Rename
1347
+ </button>
1348
+ <button
1349
+ onClick={() => {
1350
+ setOpenMenuTopicId(null);
1351
+ setAddingQuestionTo(topic.id);
1352
+ setNewQuestionTitle("");
1353
+ if (!isExpanded) toggleTopic(topic.id);
1354
+ }}
1355
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
1356
+ >
1357
+ <Plus className="w-3 h-3" /> Add question
1358
+ </button>
1359
+ <button
1360
+ onClick={async () => {
1361
+ setOpenMenuTopicId(null);
1362
+ let topicQuestions =
1363
+ questionsByTopic[topic.id] ?? [];
1364
+ try {
1365
+ topicQuestions = await api.fetchQuestions(
1366
+ topic.id,
1367
+ );
1368
+ } catch {
1369
+ // Fall back to the already-loaded sidebar snapshot.
1370
+ }
1371
+ const rootQuestions = topicQuestions.filter(
1372
+ (q) => !q.parentQuestionId,
1373
+ );
1374
+ const exportedQuestions = await Promise.all(
1375
+ rootQuestions.map((q) =>
1376
+ buildQuestionExport(q, topicQuestions),
1377
+ ),
1378
+ );
1379
+ downloadJson(
1380
+ {
1381
+ id: topic.id,
1382
+ name: topic.name,
1383
+ systemContext: topic.systemContext ?? "",
1384
+ contextFiles: topic.contextFiles,
1385
+ createdAt: topic.createdAt,
1386
+ questions: exportedQuestions,
1387
+ },
1388
+ `${topic.name.replace(/[^a-z0-9]+/gi, "-")}.json`,
1389
+ );
1390
+ }}
1391
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
1392
+ >
1393
+ <Download className="w-3 h-3" /> Download
1394
+ </button>
1395
+
1396
+ {canSyncDriveFolder && (
1397
+ <>
1398
+ <div className="border-t border-slate-700 my-0.5" />
1399
+ <button
1400
+ onClick={() => {
1401
+ setOpenMenuTopicId(null);
1402
+ void handlePushTopicToDrive(topic.id);
1403
+ }}
1404
+ disabled={topicBusy || pushing || syncing}
1405
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-cyan-300 hover:bg-slate-700 hover:text-cyan-200 disabled:opacity-50 transition-colors"
1406
+ >
1407
+ {topicPushingId === topic.id ? (
1408
+ <Loader2 className="w-3 h-3 animate-spin" />
1409
+ ) : (
1410
+ <Upload className="w-3 h-3" />
1411
+ )}
1412
+ Push topic
1413
+ </button>
1414
+ <button
1415
+ onClick={() => {
1416
+ setOpenMenuTopicId(null);
1417
+ void handlePullTopicFromDrive(topic.id);
1418
+ }}
1419
+ disabled={topicBusy || pushing || syncing}
1420
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-cyan-300 disabled:opacity-50 transition-colors"
1421
+ >
1422
+ {topicSyncingId === topic.id ? (
1423
+ <Loader2 className="w-3 h-3 animate-spin" />
1424
+ ) : (
1425
+ <RefreshCw className="w-3 h-3" />
1426
+ )}
1427
+ Pull topic
1428
+ </button>
1429
+ </>
1430
+ )}
1431
+
1432
+ <div className="border-t border-slate-700 my-0.5" />
1433
+ <button
1434
+ onClick={() => {
1435
+ setOpenMenuTopicId(null);
1436
+ if (
1437
+ confirm(
1438
+ `Delete topic "${topic.name}" and all its questions?`,
1439
+ )
1440
+ ) {
1441
+ removeTopic(topic.id);
1442
+ }
1443
+ }}
1444
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-slate-700 hover:text-red-300 transition-colors"
1445
+ >
1446
+ <Trash2 className="w-3 h-3" /> Delete
1447
+ </button>
1448
+ </div>
1449
+ </>
1450
+ )}
1451
+ </div>
1252
1452
  )}
1253
- <button
1254
- onClick={(e) => {
1255
- e.stopPropagation();
1256
- if (
1257
- confirm(
1258
- `Delete topic "${topic.name}" and all its questions?`,
1259
- )
1260
- ) {
1261
- removeTopic(topic.id);
1262
- }
1263
- }}
1264
- className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all"
1265
- >
1266
- <Trash2 className="w-3 h-3" />
1267
- </button>
1268
- <button
1269
- onClick={async (e) => {
1270
- e.stopPropagation();
1271
- let topicQuestions = questionsByTopic[topic.id] ?? [];
1272
- try {
1273
- topicQuestions = await api.fetchQuestions(topic.id);
1274
- } catch {
1275
- // Fall back to the already-loaded sidebar snapshot.
1276
- }
1277
- const rootQuestions = topicQuestions.filter(
1278
- (q) => !q.parentQuestionId,
1279
- );
1280
- const exportedQuestions = await Promise.all(
1281
- rootQuestions.map((q) =>
1282
- buildQuestionExport(q, topicQuestions),
1283
- ),
1284
- );
1285
- downloadJson(
1286
- {
1287
- id: topic.id,
1288
- name: topic.name,
1289
- systemContext: topic.systemContext ?? "",
1290
- contextFiles: topic.contextFiles,
1291
- createdAt: topic.createdAt,
1292
- questions: exportedQuestions,
1293
- },
1294
- `${topic.name.replace(/[^a-z0-9]+/gi, "-")}.json`,
1295
- );
1296
- }}
1297
- className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
1298
- title="Download topic as JSON"
1299
- >
1300
- <Download className="w-3 h-3" />
1301
- </button>
1302
1453
  </div>
1303
1454
 
1455
+ {topicDriveStatus[topic.id] && (
1456
+ <p className="px-3 pb-1 text-[10px] leading-relaxed text-slate-500">
1457
+ {topicDriveStatus[topic.id]}
1458
+ </p>
1459
+ )}
1460
+
1304
1461
  {/* Questions list */}
1305
1462
  {isExpanded && (
1306
1463
  <div className="ml-3 border-l border-slate-800">
@@ -164,6 +164,223 @@ require("fs").readdirSync(".").forEach((f) => console.log(" -", f));
164
164
  `,
165
165
  };
166
166
 
167
+ const REACT_VITE_TYPESCRIPT_FILES: Record<string, string> = {
168
+ "README.md": `# React Vite TypeScript GitHub Actions Lab
169
+
170
+ This template starts with a small React + Vite + TypeScript application and an intentionally empty workflow file.
171
+
172
+ ## App files
173
+
174
+ - \`package.json\` defines the Vite scripts and React dependencies.
175
+ - \`index.html\` mounts the app.
176
+ - \`src/main.tsx\` boots React.
177
+ - \`src/App.tsx\` contains the starter UI.
178
+ - \`.github/workflows/ci.yml\` is blank on purpose so you can write the CI workflow from scratch.
179
+
180
+ ## Local app commands
181
+
182
+ npm install
183
+ npm run dev
184
+ npm run build
185
+
186
+ ## GitHub Actions task
187
+
188
+ Open \`.github/workflows/ci.yml\`, add your workflow, then run it with the lab controls.
189
+ `,
190
+
191
+ ".github/workflows/ci.yml": "",
192
+
193
+ ".gitignore": `node_modules
194
+ dist
195
+ .DS_Store
196
+ *.local
197
+ `,
198
+
199
+ "package.json": `{
200
+ "name": "gha-react-vite-typescript-app",
201
+ "private": true,
202
+ "version": "0.0.0",
203
+ "type": "module",
204
+ "scripts": {
205
+ "dev": "vite --host 0.0.0.0",
206
+ "build": "tsc --noEmit && vite build",
207
+ "preview": "vite preview --host 0.0.0.0"
208
+ },
209
+ "dependencies": {
210
+ "react": "^19.0.0",
211
+ "react-dom": "^19.0.0"
212
+ },
213
+ "devDependencies": {
214
+ "@types/react": "^19.0.0",
215
+ "@types/react-dom": "^19.0.0",
216
+ "@vitejs/plugin-react": "^4.3.0",
217
+ "typescript": "^5.9.3",
218
+ "vite": "^6.0.0"
219
+ }
220
+ }
221
+ `,
222
+
223
+ "index.html": `<!doctype html>
224
+ <html lang="en">
225
+ <head>
226
+ <meta charset="UTF-8" />
227
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
228
+ <title>React Vite TypeScript App</title>
229
+ </head>
230
+ <body>
231
+ <div id="root"></div>
232
+ <script type="module" src="/src/main.tsx"></script>
233
+ </body>
234
+ </html>
235
+ `,
236
+
237
+ "tsconfig.json": `{
238
+ "compilerOptions": {
239
+ "target": "ES2020",
240
+ "useDefineForClassFields": true,
241
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
242
+ "allowJs": false,
243
+ "skipLibCheck": true,
244
+ "esModuleInterop": true,
245
+ "allowSyntheticDefaultImports": true,
246
+ "strict": true,
247
+ "forceConsistentCasingInFileNames": true,
248
+ "module": "ESNext",
249
+ "moduleResolution": "Node",
250
+ "resolveJsonModule": true,
251
+ "isolatedModules": true,
252
+ "noEmit": true,
253
+ "jsx": "react-jsx"
254
+ },
255
+ "include": ["src"]
256
+ }
257
+ `,
258
+
259
+ "vite.config.ts": `import { defineConfig } from "vite";
260
+ import react from "@vitejs/plugin-react";
261
+
262
+ export default defineConfig({
263
+ plugins: [react()],
264
+ });
265
+ `,
266
+
267
+ "src/main.tsx": `import { StrictMode } from "react";
268
+ import { createRoot } from "react-dom/client";
269
+ import App from "./App";
270
+ import "./index.css";
271
+
272
+ createRoot(document.getElementById("root")!).render(
273
+ <StrictMode>
274
+ <App />
275
+ </StrictMode>,
276
+ );
277
+ `,
278
+
279
+ "src/App.tsx": `const checks = [
280
+ "Edit .github/workflows/ci.yml from a clean slate",
281
+ "Install dependencies with npm install",
282
+ "Run npm run build in your workflow",
283
+ ];
284
+
285
+ export default function App() {
286
+ return (
287
+ <main className="app-shell">
288
+ <section className="hero-card">
289
+ <p className="eyebrow">GitHub Actions Practice</p>
290
+ <h1>React + Vite + TypeScript</h1>
291
+ <p className="lead">
292
+ The application is ready. Your CI workflow starts empty so you can
293
+ build it one line at a time.
294
+ </p>
295
+ <ul>
296
+ {checks.map((check) => (
297
+ <li key={check}>{check}</li>
298
+ ))}
299
+ </ul>
300
+ </section>
301
+ </main>
302
+ );
303
+ }
304
+ `,
305
+
306
+ "src/index.css": `:root {
307
+ color: #e5eefb;
308
+ background: #0f172a;
309
+ font-family:
310
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
311
+ sans-serif;
312
+ }
313
+
314
+ * {
315
+ box-sizing: border-box;
316
+ }
317
+
318
+ body {
319
+ margin: 0;
320
+ min-width: 320px;
321
+ min-height: 100vh;
322
+ }
323
+
324
+ .app-shell {
325
+ display: grid;
326
+ min-height: 100vh;
327
+ place-items: center;
328
+ padding: 2rem;
329
+ background:
330
+ radial-gradient(circle at top left, rgba(59, 130, 246, 0.35), transparent 32rem),
331
+ linear-gradient(135deg, #0f172a 0%, #111827 100%);
332
+ }
333
+
334
+ .hero-card {
335
+ width: min(100%, 720px);
336
+ padding: 2rem;
337
+ border: 1px solid rgba(148, 163, 184, 0.2);
338
+ border-radius: 24px;
339
+ background: rgba(15, 23, 42, 0.78);
340
+ box-shadow: 0 24px 80px rgba(2, 6, 23, 0.45);
341
+ }
342
+
343
+ .eyebrow {
344
+ margin: 0 0 0.75rem;
345
+ color: #fbbf24;
346
+ font-size: 0.78rem;
347
+ font-weight: 700;
348
+ letter-spacing: 0.16em;
349
+ text-transform: uppercase;
350
+ }
351
+
352
+ h1 {
353
+ margin: 0;
354
+ font-size: clamp(2.5rem, 8vw, 5rem);
355
+ line-height: 0.95;
356
+ }
357
+
358
+ .lead {
359
+ color: #cbd5e1;
360
+ font-size: 1.1rem;
361
+ line-height: 1.7;
362
+ }
363
+
364
+ ul {
365
+ display: grid;
366
+ gap: 0.75rem;
367
+ margin: 1.5rem 0 0;
368
+ padding: 0;
369
+ list-style: none;
370
+ }
371
+
372
+ li {
373
+ padding: 0.85rem 1rem;
374
+ border: 1px solid rgba(148, 163, 184, 0.16);
375
+ border-radius: 14px;
376
+ background: rgba(30, 41, 59, 0.72);
377
+ }
378
+ `,
379
+
380
+ "src/vite-env.d.ts": `/// <reference types="vite/client" />
381
+ `,
382
+ };
383
+
167
384
  export const DEFAULT_GHA_LAB: GithubActionsLabWorkspace = {
168
385
  version: 1,
169
386
  label: "GitHub Actions Playground",
@@ -173,6 +390,15 @@ export const DEFAULT_GHA_LAB: GithubActionsLabWorkspace = {
173
390
  files: DEFAULT_FILES,
174
391
  };
175
392
 
393
+ export const REACT_VITE_TYPESCRIPT_GHA_LAB: GithubActionsLabWorkspace = {
394
+ version: 1,
395
+ label: "React Vite TypeScript Starter",
396
+ activeFile: ".github/workflows/ci.yml",
397
+ defaultEvent: "push",
398
+ defaultWorkflow: ".github/workflows/ci.yml",
399
+ files: REACT_VITE_TYPESCRIPT_FILES,
400
+ };
401
+
176
402
  // ─── Helpers (mirror infraLab.ts API surface) ────────────────────────────
177
403
 
178
404
  export function cloneGhaLabWorkspace(
@@ -183,7 +409,10 @@ export function cloneGhaLabWorkspace(
183
409
  source.files && Object.keys(source.files).length > 0
184
410
  ? { ...source.files }
185
411
  : { ...DEFAULT_FILES };
186
- const activeFile = sourceFiles[source.activeFile]
412
+ const activeFile = Object.prototype.hasOwnProperty.call(
413
+ sourceFiles,
414
+ source.activeFile,
415
+ )
187
416
  ? source.activeFile
188
417
  : (Object.keys(sourceFiles)[0] ?? ".github/workflows/ci.yml");
189
418
 
@@ -193,12 +422,17 @@ export function cloneGhaLabWorkspace(
193
422
  activeFile,
194
423
  defaultEvent: source.defaultEvent || "push",
195
424
  defaultWorkflow:
196
- source.defaultWorkflow && sourceFiles[source.defaultWorkflow]
425
+ source.defaultWorkflow &&
426
+ Object.prototype.hasOwnProperty.call(sourceFiles, source.defaultWorkflow)
197
427
  ? source.defaultWorkflow
198
428
  : Object.keys(sourceFiles).find((f) =>
199
429
  f.startsWith(".github/workflows/"),
200
430
  ) || ".github/workflows/ci.yml",
201
431
  files: sourceFiles,
432
+ // Preserve optional UX flags so they round-trip through save/load.
433
+ ...(source.includeRunHistoryInContext
434
+ ? { includeRunHistoryInContext: true }
435
+ : {}),
202
436
  };
203
437
  }
204
438
 
@@ -280,6 +514,9 @@ export function parseGhaLabWorkspace(
280
514
  ? parsed.defaultWorkflow
281
515
  : DEFAULT_GHA_LAB.defaultWorkflow,
282
516
  files,
517
+ ...(parsed.includeRunHistoryInContext === true
518
+ ? { includeRunHistoryInContext: true }
519
+ : {}),
283
520
  });
284
521
  } catch {
285
522
  return null;