create-interview-cockpit 0.17.3 → 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.
@@ -1206,6 +1206,415 @@ export const DEFAULT_NEXTJS_LAB: FrontendLabWorkspace = {
1206
1206
  files: NEXTJS_DEFAULT_FILES,
1207
1207
  };
1208
1208
 
1209
+ const NEXTJS_BFF_AUTH_CLIENT_FILES: Record<string, string> = {
1210
+ "README.md": `# Next.js BFF Auth Client Lab
1211
+
1212
+ This lab is intentionally separate from the Infrastructure Lab.
1213
+
1214
+ ## Flow
1215
+
1216
+ 1. Deploy the infrastructure lab first:
1217
+ - terraform init
1218
+ - terraform plan
1219
+ - terraform apply -auto-approve
1220
+ 2. Start this Next.js lab.
1221
+ 3. Click **Sign in through BFF**.
1222
+ 4. The browser goes to the deployed BFF at http://localhost:4300.
1223
+ 5. The BFF redirects to the local Cognito-like provider.
1224
+ 6. After sign-in, the BFF stores tokens in Redis and redirects back to this Next.js app.
1225
+ 7. This app calls the BFF with credentials included.
1226
+
1227
+ ## Why separate labs?
1228
+
1229
+ - Infra Lab owns deployment.
1230
+ - Next.js Lab owns the frontend shell/client experience.
1231
+ - The integration point is the BFF URL: http://localhost:4300.
1232
+ `,
1233
+ "app/layout.tsx": `import "./globals.css";
1234
+
1235
+ export const metadata = {
1236
+ title: "Enterprise Auth Client Lab",
1237
+ description: "Next.js client talking to a locally deployed BFF",
1238
+ };
1239
+
1240
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1241
+ return (
1242
+ <html lang="en">
1243
+ <body>{children}</body>
1244
+ </html>
1245
+ );
1246
+ }
1247
+ `,
1248
+ "app/page.tsx": `import { AuthDashboard } from "../components/AuthDashboard";
1249
+
1250
+ export default function HomePage() {
1251
+ return <AuthDashboard />;
1252
+ }
1253
+ `,
1254
+ "app/globals.css": `* {
1255
+ box-sizing: border-box;
1256
+ }
1257
+
1258
+ html,
1259
+ body {
1260
+ margin: 0;
1261
+ min-height: 100%;
1262
+ background:
1263
+ radial-gradient(circle at top left, rgba(6, 182, 212, 0.18), transparent 30rem),
1264
+ radial-gradient(circle at bottom right, rgba(124, 58, 237, 0.16), transparent 28rem),
1265
+ #020617;
1266
+ color: #e2e8f0;
1267
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1268
+ }
1269
+
1270
+ button,
1271
+ textarea {
1272
+ font: inherit;
1273
+ }
1274
+
1275
+ button {
1276
+ cursor: pointer;
1277
+ }
1278
+
1279
+ .shell {
1280
+ width: min(1120px, calc(100vw - 32px));
1281
+ margin: 0 auto;
1282
+ padding: 48px 0;
1283
+ }
1284
+
1285
+ .hero,
1286
+ .card {
1287
+ border: 1px solid rgba(148, 163, 184, 0.18);
1288
+ background: rgba(15, 23, 42, 0.78);
1289
+ box-shadow: 0 24px 80px rgba(2, 6, 23, 0.45);
1290
+ backdrop-filter: blur(18px);
1291
+ }
1292
+
1293
+ .hero {
1294
+ border-radius: 28px;
1295
+ padding: 40px;
1296
+ }
1297
+
1298
+ .eyebrow {
1299
+ margin: 0 0 12px;
1300
+ color: #22d3ee;
1301
+ font-size: 0.78rem;
1302
+ font-weight: 800;
1303
+ letter-spacing: 0.22em;
1304
+ text-transform: uppercase;
1305
+ }
1306
+
1307
+ h1,
1308
+ h2,
1309
+ p {
1310
+ margin-top: 0;
1311
+ }
1312
+
1313
+ h1 {
1314
+ max-width: 760px;
1315
+ margin-bottom: 12px;
1316
+ color: #f8fafc;
1317
+ font-size: clamp(2.25rem, 6vw, 4.5rem);
1318
+ line-height: 0.95;
1319
+ letter-spacing: -0.06em;
1320
+ }
1321
+
1322
+ .hero p {
1323
+ max-width: 720px;
1324
+ color: #cbd5e1;
1325
+ font-size: 1.05rem;
1326
+ line-height: 1.7;
1327
+ }
1328
+
1329
+ .actions {
1330
+ display: flex;
1331
+ flex-wrap: wrap;
1332
+ gap: 12px;
1333
+ margin-top: 28px;
1334
+ }
1335
+
1336
+ button {
1337
+ border: 1px solid rgba(148, 163, 184, 0.26);
1338
+ border-radius: 999px;
1339
+ background: rgba(15, 23, 42, 0.92);
1340
+ color: #e2e8f0;
1341
+ padding: 11px 16px;
1342
+ font-weight: 800;
1343
+ transition: transform 150ms ease, border-color 150ms ease, background 150ms ease;
1344
+ }
1345
+
1346
+ button:hover:not(:disabled) {
1347
+ transform: translateY(-1px);
1348
+ border-color: rgba(34, 211, 238, 0.7);
1349
+ background: rgba(8, 47, 73, 0.9);
1350
+ }
1351
+
1352
+ button:disabled {
1353
+ cursor: not-allowed;
1354
+ opacity: 0.45;
1355
+ }
1356
+
1357
+ button.primary {
1358
+ border-color: rgba(34, 211, 238, 0.5);
1359
+ background: linear-gradient(135deg, #06b6d4, #8b5cf6);
1360
+ color: #020617;
1361
+ }
1362
+
1363
+ .grid {
1364
+ display: grid;
1365
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1366
+ gap: 18px;
1367
+ margin-top: 18px;
1368
+ }
1369
+
1370
+ .card {
1371
+ border-radius: 22px;
1372
+ padding: 24px;
1373
+ }
1374
+
1375
+ .card.wide {
1376
+ grid-column: 1 / -1;
1377
+ }
1378
+
1379
+ .cardHeader {
1380
+ display: flex;
1381
+ align-items: center;
1382
+ gap: 12px;
1383
+ }
1384
+
1385
+ .cardHeader span {
1386
+ display: inline-grid;
1387
+ width: 30px;
1388
+ height: 30px;
1389
+ place-items: center;
1390
+ border-radius: 999px;
1391
+ background: rgba(34, 211, 238, 0.14);
1392
+ color: #67e8f9;
1393
+ font-weight: 900;
1394
+ }
1395
+
1396
+ h2 {
1397
+ margin-bottom: 0;
1398
+ color: #f8fafc;
1399
+ font-size: 1rem;
1400
+ }
1401
+
1402
+ .hint {
1403
+ margin: 12px 0 16px;
1404
+ color: #94a3b8;
1405
+ line-height: 1.6;
1406
+ }
1407
+
1408
+ pre,
1409
+ textarea {
1410
+ width: 100%;
1411
+ border: 1px solid rgba(51, 65, 85, 0.95);
1412
+ border-radius: 16px;
1413
+ background: rgba(2, 6, 23, 0.8);
1414
+ color: #cbd5e1;
1415
+ }
1416
+
1417
+ pre {
1418
+ min-height: 164px;
1419
+ overflow: auto;
1420
+ padding: 16px;
1421
+ font-size: 0.82rem;
1422
+ line-height: 1.55;
1423
+ }
1424
+
1425
+ textarea {
1426
+ display: block;
1427
+ margin-bottom: 12px;
1428
+ padding: 14px;
1429
+ resize: vertical;
1430
+ }
1431
+
1432
+ @media (max-width: 820px) {
1433
+ .grid {
1434
+ grid-template-columns: 1fr;
1435
+ }
1436
+ }
1437
+ `,
1438
+ "components/AuthDashboard.tsx": `"use client";
1439
+
1440
+ import { useEffect, useMemo, useState } from "react";
1441
+
1442
+ const BFF_BASE_URL = "http://localhost:4300";
1443
+
1444
+ type ApiResult = {
1445
+ status: number;
1446
+ body: unknown;
1447
+ };
1448
+
1449
+ function getCookie(name: string): string | null {
1450
+ const match = document.cookie
1451
+ .split("; ")
1452
+ .find((row) => row.startsWith(name + "="));
1453
+
1454
+ return match ? decodeURIComponent(match.split("=")[1]) : null;
1455
+ }
1456
+
1457
+ async function bffFetch(path: string, init: RequestInit = {}): Promise<ApiResult> {
1458
+ const method = (init.method || "GET").toUpperCase();
1459
+ const headers = new Headers(init.headers || {});
1460
+
1461
+ if (!["GET", "HEAD", "OPTIONS"].includes(method)) {
1462
+ const csrf = getCookie("XSRF-TOKEN");
1463
+ if (csrf) headers.set("x-csrf-token", csrf);
1464
+ }
1465
+
1466
+ const response = await fetch(BFF_BASE_URL + path, {
1467
+ ...init,
1468
+ headers,
1469
+ credentials: "include",
1470
+ });
1471
+
1472
+ const text = await response.text();
1473
+ let body: unknown = text;
1474
+ try {
1475
+ body = text ? JSON.parse(text) : null;
1476
+ } catch {
1477
+ body = text;
1478
+ }
1479
+
1480
+ return { status: response.status, body };
1481
+ }
1482
+
1483
+ export function AuthDashboard() {
1484
+ const [me, setMe] = useState<ApiResult | null>(null);
1485
+ const [claim, setClaim] = useState<ApiResult | null>(null);
1486
+ const [noteResult, setNoteResult] = useState<ApiResult | null>(null);
1487
+ const [note, setNote] = useState("Customer uploaded first notice of loss.");
1488
+ const [loading, setLoading] = useState<string | null>(null);
1489
+
1490
+ const isAuthenticated = useMemo(() => {
1491
+ if (!me || typeof me.body !== "object" || me.body === null) return false;
1492
+ return Boolean((me.body as { authenticated?: boolean }).authenticated);
1493
+ }, [me]);
1494
+
1495
+ async function run(label: string, action: () => Promise<void>) {
1496
+ setLoading(label);
1497
+ try {
1498
+ await action();
1499
+ } finally {
1500
+ setLoading(null);
1501
+ }
1502
+ }
1503
+
1504
+ async function refreshMe() {
1505
+ setMe(await bffFetch("/auth/me"));
1506
+ }
1507
+
1508
+ useEffect(() => {
1509
+ void refreshMe();
1510
+ }, []);
1511
+
1512
+ function login() {
1513
+ const returnTo = encodeURIComponent(window.location.origin);
1514
+ window.location.href = BFF_BASE_URL + "/auth/login?returnTo=" + returnTo;
1515
+ }
1516
+
1517
+ async function logout() {
1518
+ await run("logout", async () => {
1519
+ await bffFetch("/auth/logout", { method: "POST" });
1520
+ await refreshMe();
1521
+ setClaim(null);
1522
+ setNoteResult(null);
1523
+ });
1524
+ }
1525
+
1526
+ async function loadClaim() {
1527
+ await run("claim", async () => {
1528
+ setClaim(await bffFetch("/api/claims/CLM-1001"));
1529
+ });
1530
+ }
1531
+
1532
+ async function addNote() {
1533
+ await run("note", async () => {
1534
+ setNoteResult(
1535
+ await bffFetch("/api/claims/CLM-1001/notes", {
1536
+ method: "POST",
1537
+ headers: { "content-type": "application/json" },
1538
+ body: JSON.stringify({ text: note }),
1539
+ }),
1540
+ );
1541
+ await loadClaim();
1542
+ });
1543
+ }
1544
+
1545
+ return (
1546
+ <main className="shell">
1547
+ <section className="hero">
1548
+ <p className="eyebrow">Next.js shell + deployed BFF</p>
1549
+ <h1>Enterprise BFF Auth Client</h1>
1550
+ <p>
1551
+ This frontend stores no bearer tokens. It redirects through the BFF,
1552
+ then calls BFF endpoints with cookies and CSRF protection.
1553
+ </p>
1554
+ <div className="actions">
1555
+ <button className="primary" onClick={login}>Sign in through BFF</button>
1556
+ <button onClick={() => void refreshMe()}>Refresh /auth/me</button>
1557
+ <button onClick={() => void logout()} disabled={!isAuthenticated || loading === "logout"}>
1558
+ Logout
1559
+ </button>
1560
+ </div>
1561
+ </section>
1562
+
1563
+ <section className="grid">
1564
+ <article className="card">
1565
+ <div className="cardHeader">
1566
+ <span>1</span>
1567
+ <h2>Session</h2>
1568
+ </div>
1569
+ <p className="hint">The browser sees only cookie-backed auth state.</p>
1570
+ <pre>{JSON.stringify(me?.body ?? "Not loaded", null, 2)}</pre>
1571
+ </article>
1572
+
1573
+ <article className="card">
1574
+ <div className="cardHeader">
1575
+ <span>2</span>
1576
+ <h2>Downstream API through BFF</h2>
1577
+ </div>
1578
+ <p className="hint">The client never calls the claims API directly.</p>
1579
+ <button onClick={() => void loadClaim()} disabled={!isAuthenticated || loading === "claim"}>
1580
+ Load claim CLM-1001
1581
+ </button>
1582
+ <pre>{JSON.stringify(claim?.body ?? "No claim loaded", null, 2)}</pre>
1583
+ </article>
1584
+
1585
+ <article className="card wide">
1586
+ <div className="cardHeader">
1587
+ <span>3</span>
1588
+ <h2>Write with CSRF header</h2>
1589
+ </div>
1590
+ <p className="hint">
1591
+ The readable XSRF-TOKEN cookie is copied into x-csrf-token for writes.
1592
+ </p>
1593
+ <textarea value={note} onChange={(event) => setNote(event.target.value)} rows={3} />
1594
+ <button onClick={() => void addNote()} disabled={!isAuthenticated || loading === "note"}>
1595
+ Add note
1596
+ </button>
1597
+ <pre>{JSON.stringify(noteResult?.body ?? "No note submitted", null, 2)}</pre>
1598
+ </article>
1599
+ </section>
1600
+ </main>
1601
+ );
1602
+ }
1603
+ `,
1604
+ "components/AuthDashboard.module.css": `/* Optional scratch file: move styles here if you want CSS modules practice. */
1605
+ `,
1606
+ "app/styles.css": `/* Optional scratch file for experimenting with additional global styles. */
1607
+ `,
1608
+ };
1609
+
1610
+ export const NEXTJS_BFF_AUTH_CLIENT_LAB: FrontendLabWorkspace = {
1611
+ version: 1,
1612
+ label: "Next.js BFF Auth Client",
1613
+ type: "nextjs",
1614
+ activeFile: "components/AuthDashboard.tsx",
1615
+ files: NEXTJS_BFF_AUTH_CLIENT_FILES,
1616
+ };
1617
+
1209
1618
  export const DEFAULT_MODULE_FEDERATION_LAB: FrontendLabWorkspace = {
1210
1619
  version: 1,
1211
1620
  label: "Webpack Module Federation Lab",
@@ -6,6 +6,7 @@ import type {
6
6
  WorkspaceMeta,
7
7
  InfraLabWorkspace,
8
8
  FrontendLabWorkspace,
9
+ GithubActionsLabWorkspace,
9
10
  ContextFileOrigin,
10
11
  } from "./types";
11
12
  import type { AiSettings } from "./api";
@@ -126,12 +127,11 @@ interface Store {
126
127
  deleteWorkspace: (id: string) => Promise<void>;
127
128
  renameWorkspace: (id: string, name: string) => Promise<void>;
128
129
  patchWorkspace: (id: string, data: object) => Promise<void>;
129
- syncWorkspace: (id: string) => Promise<{
130
- topicsUpserted: number;
131
- filesImported: number;
132
- filesSkipped: number;
133
- errors: string[];
134
- }>;
130
+ syncWorkspace: (id: string) => Promise<import("./api").SyncWorkspaceResult>;
131
+ syncTopic: (
132
+ workspaceId: string,
133
+ topicId: string,
134
+ ) => Promise<import("./api").SyncWorkspaceResult>;
135
135
  linkDriveFolder: (
136
136
  workspaceId: string,
137
137
  url: string,
@@ -148,6 +148,11 @@ interface Store {
148
148
  id: string,
149
149
  targetFolderId?: string,
150
150
  ) => Promise<import("./api").ExportWorkspaceResult>;
151
+ exportTopic: (
152
+ workspaceId: string,
153
+ topicId: string,
154
+ targetFolderId?: string,
155
+ ) => Promise<import("./api").ExportWorkspaceResult>;
151
156
  fetchDriveSubfolders: (id: string) => Promise<import("./api").DriveFolder[]>;
152
157
  createDriveSubfolder: (
153
158
  id: string,
@@ -176,6 +181,12 @@ interface Store {
176
181
  topicId: string,
177
182
  parentQuestionId: string | null,
178
183
  ) => Promise<void>;
184
+ copyQuestion: (
185
+ questionId: string,
186
+ topicId: string,
187
+ parentQuestionId: string | null,
188
+ targetTopicId?: string,
189
+ ) => Promise<void>;
179
190
  removeQuestion: (questionId: string, topicId: string) => Promise<void>;
180
191
  renameQuestion: (
181
192
  questionId: string,
@@ -330,6 +341,13 @@ interface Store {
330
341
  openInfraLab: (workspace?: InfraLabWorkspace, fileId?: string) => void;
331
342
  closeInfraLab: () => void;
332
343
 
344
+ // ── GitHub Actions Lab ───────────────────────────────────────
345
+ showGhaLab: boolean;
346
+ runnerInitialGha: GithubActionsLabWorkspace | null;
347
+ runnerInitialGhaFileId: string | null;
348
+ openGhaLab: (workspace?: GithubActionsLabWorkspace, fileId?: string) => void;
349
+ closeGhaLab: () => void;
350
+
333
351
  // ── Frontend Labs (React / Next.js / Module Federation) — open inside the sandbox ──
334
352
  openReactLab: (
335
353
  workspace?: FrontendLabWorkspace,
@@ -387,6 +405,9 @@ export const useStore = create<Store>((set, get) => ({
387
405
  showInfraLab: false,
388
406
  runnerInitialInfra: null,
389
407
  runnerInitialInfraFileId: null,
408
+ showGhaLab: false,
409
+ runnerInitialGha: null,
410
+ runnerInitialGhaFileId: null,
390
411
  showCanvasLab: false,
391
412
  canvasLabInitialCode: null,
392
413
  canvasLabInitialFileId: null,
@@ -467,9 +488,49 @@ export const useStore = create<Store>((set, get) => ({
467
488
 
468
489
  syncWorkspace: async (id) => {
469
490
  const result = await api.syncWorkspaceApi(id);
491
+ if ("needsAuth" in result && result.needsAuth) {
492
+ return result;
493
+ }
470
494
  if (id === get().activeWorkspaceId) {
471
- const topics = await api.fetchTopics();
472
- set({ topics, questionsByTopic: {} });
495
+ const [topics, workspaceFiles] = await Promise.all([
496
+ api.fetchTopics(),
497
+ api.fetchWorkspaceFiles(),
498
+ ]);
499
+ set({ topics, workspaceFiles, questionsByTopic: {} });
500
+ }
501
+ const registry = await api.fetchWorkspaces();
502
+ set({ workspaces: registry.workspaces });
503
+ return result;
504
+ },
505
+
506
+ syncTopic: async (workspaceId, topicId) => {
507
+ const result = await api.syncTopicApi(workspaceId, topicId);
508
+ if ("needsAuth" in result && result.needsAuth) {
509
+ return result;
510
+ }
511
+ if (workspaceId === get().activeWorkspaceId) {
512
+ const [topics, questions] = await Promise.all([
513
+ api.fetchTopics(),
514
+ api.fetchQuestions(topicId),
515
+ ]);
516
+ set((s) => {
517
+ const selectedStillExists = questions.some(
518
+ (q) => q.id === s.selectedQuestionId,
519
+ );
520
+ const selectedWasInTopic = s.currentQuestion?.topicId === topicId;
521
+ return {
522
+ topics,
523
+ questionsByTopic: { ...s.questionsByTopic, [topicId]: questions },
524
+ selectedQuestionId:
525
+ selectedWasInTopic && !selectedStillExists
526
+ ? null
527
+ : s.selectedQuestionId,
528
+ currentQuestion:
529
+ selectedWasInTopic && !selectedStillExists
530
+ ? null
531
+ : s.currentQuestion,
532
+ };
533
+ });
473
534
  }
474
535
  const registry = await api.fetchWorkspaces();
475
536
  set({ workspaces: registry.workspaces });
@@ -491,8 +552,15 @@ export const useStore = create<Store>((set, get) => ({
491
552
  if (workspaceId === get().activeWorkspaceId) {
492
553
  set({ topics: [], questionsByTopic: {} });
493
554
  const result = await api.syncWorkspaceApi(workspaceId);
494
- const topics = await api.fetchTopics();
495
- set({ topics, questionsByTopic: {} });
555
+ if ("needsAuth" in result && result.needsAuth) {
556
+ window.location.href = result.authUrl;
557
+ return;
558
+ }
559
+ const [topics, workspaceFiles] = await Promise.all([
560
+ api.fetchTopics(),
561
+ api.fetchWorkspaceFiles(),
562
+ ]);
563
+ set({ topics, workspaceFiles, questionsByTopic: {} });
496
564
  const reg2 = await api.fetchWorkspaces();
497
565
  set({ workspaces: reg2.workspaces });
498
566
  }
@@ -508,6 +576,7 @@ export const useStore = create<Store>((set, get) => ({
508
576
  selectedTopicId: null,
509
577
  selectedQuestionId: null,
510
578
  currentQuestion: null,
579
+ workspaceFiles: [],
511
580
  });
512
581
  },
513
582
 
@@ -530,6 +599,10 @@ export const useStore = create<Store>((set, get) => ({
530
599
  return api.exportWorkspaceToDrive(id, targetFolderId);
531
600
  },
532
601
 
602
+ exportTopic: async (workspaceId, topicId, targetFolderId) => {
603
+ return api.exportTopicToDrive(workspaceId, topicId, targetFolderId);
604
+ },
605
+
533
606
  fetchDriveSubfolders: async (id) => {
534
607
  return api.fetchDriveSubfolders(id);
535
608
  },
@@ -629,6 +702,39 @@ export const useStore = create<Store>((set, get) => ({
629
702
  }));
630
703
  },
631
704
 
705
+ copyQuestion: async (
706
+ questionId,
707
+ topicId,
708
+ parentQuestionId,
709
+ targetTopicId,
710
+ ) => {
711
+ const destinationTopicId = targetTopicId ?? topicId;
712
+ const copied = await api.copyQuestion(questionId, {
713
+ parentQuestionId,
714
+ targetTopicId: destinationTopicId,
715
+ });
716
+ const copiedRoot = copied[0];
717
+ set((s) => ({
718
+ questionsByTopic: {
719
+ ...s.questionsByTopic,
720
+ [destinationTopicId]: [
721
+ ...(s.questionsByTopic[destinationTopicId] || []),
722
+ ...copied,
723
+ ],
724
+ },
725
+ selectedTopicId: copiedRoot?.id ? destinationTopicId : s.selectedTopicId,
726
+ selectedQuestionId: copiedRoot?.id ?? s.selectedQuestionId,
727
+ currentQuestion: copiedRoot ?? s.currentQuestion,
728
+ expandedTopics: s.expandedTopics.includes(destinationTopicId)
729
+ ? s.expandedTopics
730
+ : [...s.expandedTopics, destinationTopicId],
731
+ }));
732
+ if (copiedRoot) {
733
+ sessionStorage.setItem("lastTopicId", destinationTopicId);
734
+ sessionStorage.setItem("lastQuestionId", copiedRoot.id);
735
+ }
736
+ },
737
+
632
738
  removeQuestion: async (questionId, topicId) => {
633
739
  await api.deleteQuestion(questionId);
634
740
  set((s) => ({
@@ -1079,6 +1185,20 @@ export const useStore = create<Store>((set, get) => ({
1079
1185
  openBrowserSecurityLab: () => set({ showBrowserSecurityLab: true }),
1080
1186
  closeBrowserSecurityLab: () => set({ showBrowserSecurityLab: false }),
1081
1187
  closeInfraLab: () => set({ showInfraLab: false }),
1188
+ openGhaLab: (workspace, fileId?) =>
1189
+ set({
1190
+ showGhaLab: true,
1191
+ showInfraLab: false,
1192
+ showCodeRunner: false,
1193
+ runnerInitialGha: workspace ?? null,
1194
+ runnerInitialGhaFileId: fileId ?? null,
1195
+ }),
1196
+ closeGhaLab: () =>
1197
+ set({
1198
+ showGhaLab: false,
1199
+ runnerInitialGha: null,
1200
+ runnerInitialGhaFileId: null,
1201
+ }),
1082
1202
  openCanvasLab: (code?, fileId?) =>
1083
1203
  set({
1084
1204
  showCanvasLab: true,
@@ -8,7 +8,8 @@ export type ContextFileOrigin =
8
8
  | "react"
9
9
  | "nextjs"
10
10
  | "module-federation"
11
- | "canvas";
11
+ | "canvas"
12
+ | "github-actions";
12
13
 
13
14
  export interface ContextFile {
14
15
  id: string;
@@ -42,12 +43,29 @@ export interface FrontendLabWorkspace {
42
43
  export interface InfraLabWorkspace {
43
44
  version: 1;
44
45
  label: string;
45
- provider: "aws";
46
- executionMode: "plan-only" | "localstack";
46
+ provider: "aws" | "docker";
47
+ executionMode: "plan-only" | "localstack" | "docker";
47
48
  activeFile: string;
48
49
  files: Record<string, string>;
49
50
  }
50
51
 
52
+ export interface GithubActionsLabWorkspace {
53
+ version: 1;
54
+ label: string;
55
+ activeFile: string;
56
+ files: Record<string, string>;
57
+ /** Optional default event the run button uses (push, pull_request, workflow_dispatch). */
58
+ defaultEvent?: string;
59
+ /** Optional default workflow file path under .github/workflows. */
60
+ defaultWorkflow?: string;
61
+ /**
62
+ * When true, the most recent act runs for this lab are embedded into the
63
+ * saved snapshot so the chat LLM can reason about real execution results
64
+ * (job statuses, durations, exit codes) instead of just the YAML.
65
+ */
66
+ includeRunHistoryInContext?: boolean;
67
+ }
68
+
51
69
  export interface WorkspaceMeta {
52
70
  id: string;
53
71
  name: string;
@@ -116,6 +134,16 @@ export interface ReadingBookmark {
116
134
  blockIndex: number;
117
135
  }
118
136
 
137
+ export interface StoredCodeAnnotation {
138
+ id: string;
139
+ lineNumber: number;
140
+ lineContent: string;
141
+ prompt: string;
142
+ response: string;
143
+ filePath: string;
144
+ createdAt: string;
145
+ }
146
+
119
147
  export interface Question {
120
148
  id: string;
121
149
  topicId: string;
@@ -127,6 +155,8 @@ export interface Question {
127
155
  messages: Message[];
128
156
  annotations?: Annotation[];
129
157
  readingBookmark?: ReadingBookmark;
158
+ /** Code-line annotations keyed by file path. */
159
+ codeAnnotations?: { [filePath: string]: StoredCodeAnnotation[] };
130
160
  /** IDs of other questions in the same topic whose conversation history is injected as context. */
131
161
  linkedConversationIds?: string[];
132
162
  createdAt: string;