create-interview-cockpit 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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">
@@ -199,6 +199,10 @@ export function cloneGhaLabWorkspace(
199
199
  f.startsWith(".github/workflows/"),
200
200
  ) || ".github/workflows/ci.yml",
201
201
  files: sourceFiles,
202
+ // Preserve optional UX flags so they round-trip through save/load.
203
+ ...(source.includeRunHistoryInContext
204
+ ? { includeRunHistoryInContext: true }
205
+ : {}),
202
206
  };
203
207
  }
204
208
 
@@ -280,6 +284,9 @@ export function parseGhaLabWorkspace(
280
284
  ? parsed.defaultWorkflow
281
285
  : DEFAULT_GHA_LAB.defaultWorkflow,
282
286
  files,
287
+ ...(parsed.includeRunHistoryInContext === true
288
+ ? { includeRunHistoryInContext: true }
289
+ : {}),
283
290
  });
284
291
  } catch {
285
292
  return null;
@@ -128,6 +128,10 @@ interface Store {
128
128
  renameWorkspace: (id: string, name: string) => Promise<void>;
129
129
  patchWorkspace: (id: string, data: object) => Promise<void>;
130
130
  syncWorkspace: (id: string) => Promise<import("./api").SyncWorkspaceResult>;
131
+ syncTopic: (
132
+ workspaceId: string,
133
+ topicId: string,
134
+ ) => Promise<import("./api").SyncWorkspaceResult>;
131
135
  linkDriveFolder: (
132
136
  workspaceId: string,
133
137
  url: string,
@@ -144,6 +148,11 @@ interface Store {
144
148
  id: string,
145
149
  targetFolderId?: string,
146
150
  ) => Promise<import("./api").ExportWorkspaceResult>;
151
+ exportTopic: (
152
+ workspaceId: string,
153
+ topicId: string,
154
+ targetFolderId?: string,
155
+ ) => Promise<import("./api").ExportWorkspaceResult>;
147
156
  fetchDriveSubfolders: (id: string) => Promise<import("./api").DriveFolder[]>;
148
157
  createDriveSubfolder: (
149
158
  id: string,
@@ -494,6 +503,40 @@ export const useStore = create<Store>((set, get) => ({
494
503
  return result;
495
504
  },
496
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
+ });
534
+ }
535
+ const registry = await api.fetchWorkspaces();
536
+ set({ workspaces: registry.workspaces });
537
+ return result;
538
+ },
539
+
497
540
  linkDriveFolder: async (workspaceId, url) => {
498
541
  const { registry, folders } = await api.linkDriveFolder(workspaceId, url);
499
542
  set({ workspaces: registry.workspaces, driveRootFolders: folders });
@@ -556,6 +599,10 @@ export const useStore = create<Store>((set, get) => ({
556
599
  return api.exportWorkspaceToDrive(id, targetFolderId);
557
600
  },
558
601
 
602
+ exportTopic: async (workspaceId, topicId, targetFolderId) => {
603
+ return api.exportTopicToDrive(workspaceId, topicId, targetFolderId);
604
+ },
605
+
559
606
  fetchDriveSubfolders: async (id) => {
560
607
  return api.fetchDriveSubfolders(id);
561
608
  },
@@ -58,6 +58,12 @@ export interface GithubActionsLabWorkspace {
58
58
  defaultEvent?: string;
59
59
  /** Optional default workflow file path under .github/workflows. */
60
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;
61
67
  }
62
68
 
63
69
  export interface WorkspaceMeta {
@@ -1 +1 @@
1
- {"root":["./src/app.tsx","./src/api.ts","./src/browsersecuritytemplates.ts","./src/enterpriselocallab.ts","./src/githubactionslab.ts","./src/infralab.ts","./src/main.tsx","./src/reactlab.ts","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/aisettingsmodal.tsx","./src/components/annotationdialog.tsx","./src/components/browsersecuritylabmodal.tsx","./src/components/canvaslabmodal.tsx","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/codelineannotationpopup.tsx","./src/components/coderunnermodal.tsx","./src/components/deploymentlabmodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/githubactionslabmodal.tsx","./src/components/infralabmodal.tsx","./src/components/labspanel.tsx","./src/components/linkedconvospicker.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/notesmodal.tsx","./src/components/plotembed.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"version":"5.9.3"}
1
+ {"root":["./src/app.tsx","./src/api.ts","./src/browsersecuritytemplates.ts","./src/enterpriselocallab.ts","./src/githubactionslab.ts","./src/infralab.ts","./src/main.tsx","./src/reactlab.ts","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/aisettingsmodal.tsx","./src/components/annotationdialog.tsx","./src/components/browsersecuritylabmodal.tsx","./src/components/canvaslabmodal.tsx","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/codelineannotationpopup.tsx","./src/components/coderunnermodal.tsx","./src/components/deploymentlabmodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/ghahistorypanel.tsx","./src/components/ghajobspanel.tsx","./src/components/githubactionslabmodal.tsx","./src/components/infralabmodal.tsx","./src/components/labspanel.tsx","./src/components/linkedconvospicker.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/notesmodal.tsx","./src/components/plotembed.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"version":"5.9.3"}
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.17.2"
2
+ "version": "0.17.3"
3
3
  }