fraim 2.0.129 → 2.0.130

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.
@@ -16,9 +16,8 @@ exports.derivePrimaryButtonLabel = derivePrimaryButtonLabel;
16
16
  exports.FIRST_RUN_ROW_IDS = [
17
17
  'node',
18
18
  'git',
19
+ 'fraim',
19
20
  'agent',
20
- 'agent-login',
21
- 'project',
22
21
  ];
23
22
  exports.FIRST_RUN_PROMPT = 'Onboard this project';
24
23
  exports.FIRST_RUN_RESOURCES_URL = 'https://fraimworks.ai/resources.html';
@@ -60,36 +59,26 @@ function createInitialRows() {
60
59
  return [
61
60
  { id: 'node', label: 'Node.js', status: 'pending', verb: "we'll install" },
62
61
  { id: 'git', label: 'git', status: 'pending', verb: "we'll install" },
63
- { id: 'agent', label: 'AI agent', status: 'pending', verb: "we'll set up Claude Code (recommended)" },
64
- { id: 'agent-login', label: 'Sign in', status: 'pending', verb: "you'll sign in after install" },
65
- { id: 'project', label: 'Project folder', status: 'pending', verb: 'pick a folder where FRAIM should work' },
62
+ { id: 'fraim', label: 'FRAIM', status: 'pending', verb: "we'll set up FRAIM" },
63
+ { id: 'agent', label: 'AI Employees', status: 'pending', verb: "we'll check for AI Employees" },
66
64
  ];
67
65
  }
68
66
  /**
69
67
  * State-derived primary-button label per R2.0. NEVER audience-derived.
70
68
  *
71
- * - "Open Hub": every row is ok, OR the project row is ok and every other
72
- * row is ok-or-manual-required (i.e. the user explicitly handed
73
- * themselves any remaining steps via Skip-and-continue, and there is no
74
- * forward action the wizard can take).
69
+ * - "Get Started": every row is ok, OR every row is ok-or-manual-required
70
+ * (skip-path: user explicitly handled remaining steps via Skip-and-continue).
71
+ * Clicking "Get Started" shows the user-type selection screen (#367).
75
72
  * - "Continue": at least one row is ok and there is still wizard work to do.
76
73
  * - "Set up FRAIM": no rows ok yet.
77
- *
78
- * Manual-required is treated as "user-handled" for the Open-Hub gate so a
79
- * Skip-and-continue path doesn't strand the user on a dead-end Continue
80
- * button. It is treated as "in-progress / waiting on user" for the
81
- * Continue gate so a fresh state with only the manual project row open
82
- * still asks the user to continue picking a folder.
83
74
  */
84
75
  function derivePrimaryButtonLabel(rows) {
85
- const projectRow = rows.find((row) => row.id === 'project');
86
- const projectOk = projectRow?.status === 'ok';
87
76
  const allOk = rows.every((row) => row.status === 'ok');
88
77
  if (allOk)
89
- return 'Open Hub';
90
- // User-skipped path: project ok and every other row ok-or-manual-required.
91
- if (projectOk && rows.every((row) => row.status === 'ok' || row.status === 'manual-required')) {
92
- return 'Open Hub';
78
+ return 'Get Started';
79
+ // Skip-path: every row is ok-or-manual-required — nothing left for the wizard.
80
+ if (rows.every((row) => row.status === 'ok' || row.status === 'manual-required')) {
81
+ return 'Get Started';
93
82
  }
94
83
  if (rows.some((row) => row.status === 'ok'))
95
84
  return 'Continue';
@@ -237,27 +237,21 @@ class FraimTemplateEngine {
237
237
  }
238
238
  }
239
239
  buildMissingConfigNotice(path, action) {
240
- const metadata = FraimTemplateEngine.CONFIG_CONTEXT_METADATA[path];
241
- const label = metadata?.label || `config value \`${path}\``;
242
- const onboardingHint = metadata?.onboardingHint || `provide \`${path}\` in \`fraim/config.json\``;
240
+ const configReference = `\`${path}\``;
241
+ const onboardingHint = `set ${configReference} in \`fraim/config.json\``;
243
242
  if (action === 'REQUIRE') {
244
243
  return {
245
244
  action,
246
245
  path,
247
- message: `${this.capitalizeFirst(label)} is required for this task but is not configured. Invoke the manager \`project-onboarding\` job and ${onboardingHint} before continuing.`
246
+ message: `Required config value ${configReference} is not configured. Invoke the manager \`project-onboarding\` job and ${onboardingHint} before continuing.`
248
247
  };
249
248
  }
250
249
  return {
251
250
  action,
252
251
  path,
253
- message: `${this.capitalizeFirst(label)} is not configured. Invoke the manager \`project-onboarding\` job and ${onboardingHint}.`
252
+ message: `Config value ${configReference} is not configured. Invoke the manager \`project-onboarding\` job and ${onboardingHint}.`
254
253
  };
255
254
  }
256
- capitalizeFirst(value) {
257
- if (!value)
258
- return value;
259
- return value.charAt(0).toUpperCase() + value.slice(1);
260
- }
261
255
  loadDeliveryTemplates() {
262
256
  if (this.deliveryTemplatesCache)
263
257
  return this.deliveryTemplatesCache;
@@ -362,24 +356,6 @@ class FraimTemplateEngine {
362
356
  }
363
357
  exports.FraimTemplateEngine = FraimTemplateEngine;
364
358
  FraimTemplateEngine.CONFIG_TEMPLATE_ACTIONS = new Set(['INFORM', 'NO_OP', 'REQUIRE']);
365
- FraimTemplateEngine.CONFIG_CONTEXT_METADATA = {
366
- 'customizations.architectureDoc': {
367
- label: 'project-specific architecture document',
368
- onboardingHint: 'provide the architecture document path in `fraim/config.json`'
369
- },
370
- 'customizations.designSystem.path': {
371
- label: 'project-specific design system',
372
- onboardingHint: 'provide the design system path in `fraim/config.json`'
373
- },
374
- compliance: {
375
- label: 'project compliance configuration',
376
- onboardingHint: 'record the project compliance requirements in `fraim/config.json`'
377
- },
378
- 'compliance.regulations': {
379
- label: 'project compliance regulations',
380
- onboardingHint: 'record the applicable compliance regulations in `fraim/config.json`'
381
- }
382
- };
383
359
  FraimTemplateEngine.PROXY_ACTION_PREFIX = 'proxy.action.';
384
360
  FraimTemplateEngine.ISSUE_ACTIONS = new Set([
385
361
  'get_issue',
@@ -403,6 +379,7 @@ class FraimLocalMCPServer {
403
379
  this.engine = null;
404
380
  this.otlpServer = null;
405
381
  this.isShutdown = false;
382
+ this.mentoringResponseCache = null;
406
383
  this.writer = writer || process.stdout.write.bind(process.stdout);
407
384
  this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
408
385
  this.apiKey = this.loadApiKey();
@@ -1710,6 +1687,14 @@ class FraimLocalMCPServer {
1710
1687
  const mentor = this.getMentor(requestSessionId);
1711
1688
  const tutoringResponse = await mentor.handleMentoringRequest(args);
1712
1689
  this.log(`✅ Local seekMentoring succeeded for ${args.jobName}:${args.currentPhase}`);
1690
+ // Store response data for usage tracking (Bug fix: capture nextPhase and jobId)
1691
+ if (!this.mentoringResponseCache) {
1692
+ this.mentoringResponseCache = new Map();
1693
+ }
1694
+ this.mentoringResponseCache.set(requestId, {
1695
+ nextPhase: tutoringResponse.nextPhase,
1696
+ jobId: args.jobId || requestSessionId // Use jobId from args, fallback to sessionId
1697
+ });
1713
1698
  // Quality enforcement (Issue #251).
1714
1699
  //
1715
1700
  // The local proxy owns seekMentoring for personalized-job support.
@@ -2015,7 +2000,7 @@ class FraimLocalMCPServer {
2015
2000
  return;
2016
2001
  }
2017
2002
  const toolName = request.params?.name;
2018
- const args = request.params?.arguments || {};
2003
+ let args = request.params?.arguments || {};
2019
2004
  const requestSessionId = this.extractSessionIdFromRequest(request);
2020
2005
  if (toolName && requestSessionId) {
2021
2006
  const success = !response.error;
@@ -2023,6 +2008,20 @@ class FraimLocalMCPServer {
2023
2008
  if (!this.repoInfo) {
2024
2009
  this.detectRepoInfo();
2025
2010
  }
2011
+ // Bug fix: Enrich seekMentoring args with response data
2012
+ if (toolName === 'seekMentoring' && this.mentoringResponseCache) {
2013
+ const requestId = request.id;
2014
+ const cachedResponse = this.mentoringResponseCache.get(requestId);
2015
+ if (cachedResponse) {
2016
+ args = {
2017
+ ...args,
2018
+ nextPhase: cachedResponse.nextPhase,
2019
+ jobId: cachedResponse.jobId
2020
+ };
2021
+ this.mentoringResponseCache.delete(requestId); // Clean up
2022
+ this.log(`📊 Enriched seekMentoring args with nextPhase=${cachedResponse.nextPhase}, jobId=${cachedResponse.jobId}`);
2023
+ }
2024
+ }
2026
2025
  // Capture the current queue size before collection
2027
2026
  const beforeCount = this.usageCollector.getEventCount();
2028
2027
  try {
@@ -82,6 +82,7 @@ class UsageCollector {
82
82
  repoIdentifier: this.repoIdentifier || undefined,
83
83
  agentName: this.agentName || undefined,
84
84
  agentModel: this.agentModel || undefined,
85
+ jobId: args.jobId || undefined, // Bug fix: capture jobId from enriched args
85
86
  args: Object.keys(analyticsArgs).length > 0 ? analyticsArgs : undefined
86
87
  };
87
88
  this.events.push(event);
@@ -117,6 +118,8 @@ class UsageCollector {
117
118
  analyticsArgs.currentPhase = args.currentPhase;
118
119
  if (args.status)
119
120
  analyticsArgs.status = args.status;
121
+ if (args.nextPhase !== undefined)
122
+ analyticsArgs.nextPhase = args.nextPhase; // Bug fix: capture nextPhase
120
123
  const mentoringJobName = UsageCollector.resolveMentoringJobName(args);
121
124
  if (mentoringJobName !== 'unknown')
122
125
  analyticsArgs.jobName = mentoringJobName;
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.129",
3
+ "version": "2.0.130",
4
4
  "description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -10,19 +10,19 @@
10
10
  "scripts": {
11
11
  "dev": "tsx --watch src/fraim-mcp-server.ts > server.log 2>&1",
12
12
  "dev:prod": "npm run build && node dist/src/fraim-mcp-server.js > server.log 2>&1",
13
- "build": "tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && node -e \"require('fs').copyFileSync('src/core/types.ts', 'registry/templates/manager/fraim-config-schema.ts')\" && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
13
+ "build": "tsx scripts/build-fraim-config-schema-template.ts && npm run typecheck:scripts && tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
14
14
  "build:stubs": "tsx scripts/build-stub-registry.ts",
15
15
  "build:fraim-brain": "node scripts/generate-fraim-brain.js",
16
16
  "test-all": "npm run test && npm run test:isolated && npm run test:ui",
17
17
  "test": "node scripts/test-with-server.js",
18
- "test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
18
+ "test:isolated": "npx tsx --test --test-concurrency=1 --test-reporter=spec tests/isolated/test-*.ts",
19
19
  "test:smoke": "node scripts/test-with-server.js --tags=smoke",
20
20
  "test:coverage": "node scripts/test-with-server.js --tags=smoke --coverage",
21
21
  "test:stripe": "node scripts/test-with-server.js tests/test-stripe-payment-complete.ts",
22
22
  "test:stripe:ui": "playwright test tests/ui/test-payment-ui.spec.ts",
23
23
  "test:perf": "node scripts/test-with-server.js tests/performance/analytics-perf.ts",
24
- "test:ui": "playwright test",
25
- "test:ui:headed": "playwright test --headed",
24
+ "test:ui": "playwright test --workers=1",
25
+ "test:ui:headed": "playwright test --headed --workers=1",
26
26
  "hub:desktop": "npm run build && electron dist/src/ai-hub/desktop-main.js",
27
27
  "start:fraim": "tsx src/fraim-mcp-server.ts",
28
28
  "dev:fraim": "tsx --watch src/fraim-mcp-server.ts",
@@ -44,6 +44,7 @@
44
44
  "publish-fraim-only": "node scripts/publish-fraim.js",
45
45
  "publish-both-manual": "node scripts/publish-both.js",
46
46
  "validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:jobs && npm run validate:skills && npm run validate:registry-references && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings && npm run validate:fidelity && npm run validate:config-tokens && npm run validate:brain-mapping && npm run validate:template-syntax",
47
+ "typecheck:scripts": "tsc -p tsconfig.scripts.json --pretty false",
47
48
  "validate:registry-references": "tsx scripts/validate-registry-references.ts",
48
49
  "validate:brain-mapping": "tsx scripts/validate-brain-mapping.ts",
49
50
  "validate:fraim-pro-assets": "tsx scripts/validate-fraim-pro-assets.ts",
@@ -56,6 +57,7 @@
56
57
  "validate:provider-action-mappings": "tsx scripts/validate-provider-action-mappings.ts",
57
58
  "validate:fidelity": "tsx scripts/validate-fidelity.ts",
58
59
  "validate:config-tokens": "tsx scripts/validate-config-tokens.ts",
60
+ "validate:workspace-config": "tsx scripts/validate-workspace-config.ts",
59
61
  "validate:template-syntax": "tsx scripts/validate-template-syntax.ts",
60
62
  "validate:backup": "bash scripts/backup/validate-pitr-restore.sh"
61
63
  },
@@ -367,12 +367,22 @@ let renderedTrackerKey = null;
367
367
  function renderTracker(conv) {
368
368
  const tracker = els['tracker'];
369
369
  if (!tracker) return;
370
- const stages = (conv.run && Array.isArray(conv.run.stages)) ? conv.run.stages : [];
370
+
371
+ // Issue #357: Defensive check - ensure conv.run exists before accessing stages
372
+ if (!conv.run) {
373
+ // Run hasn't been created yet or hasn't been folded into conversation
374
+ tracker.hidden = true;
375
+ renderedTrackerKey = null;
376
+ return;
377
+ }
378
+
379
+ const stages = Array.isArray(conv.run.stages) ? conv.run.stages : [];
371
380
  if (stages.length === 0) {
372
381
  tracker.hidden = true;
373
382
  renderedTrackerKey = null;
374
383
  return;
375
384
  }
385
+
376
386
  // Cache key so we only rebuild the DOM when the stage data changes.
377
387
  const key = stages.map((s) => `${s.phaseId}:${s.state}`).join('|') +
378
388
  '#' + (conv.run.currentPhase || '');
@@ -1240,11 +1250,22 @@ function wireEvents() {
1240
1250
  loadConversationsFromStorage();
1241
1251
  wirePopovers();
1242
1252
  wireEvents();
1253
+
1254
+ const urlParams = new URLSearchParams(window.location.search);
1255
+ const isFirstRun = urlParams.get('firstRun') === 'true';
1256
+
1243
1257
  try {
1244
1258
  await loadBootstrap();
1245
1259
  } catch (error) {
1246
1260
  showStatus(error.message, true);
1261
+ return;
1262
+ }
1263
+
1264
+ if (isFirstRun) {
1265
+ renderFirstRunLanding();
1266
+ return;
1247
1267
  }
1268
+
1248
1269
  // If an active conversation belongs to the loaded project and is still running, resume polling.
1249
1270
  const conv = activeConversation();
1250
1271
  if (conv && conv.projectPath === state.projectPath) {
@@ -1255,3 +1276,168 @@ function wireEvents() {
1255
1276
  renderActive();
1256
1277
  }
1257
1278
  })();
1279
+
1280
+ function renderFirstRunLanding() {
1281
+ // Show first-run onboarding screen over the normal Hub UI.
1282
+ // The user picks a project folder here; clicking Start fires the
1283
+ // Project Onboarding job automatically (R4.3/R4.4).
1284
+ const existing = document.getElementById('fraim-first-run-landing');
1285
+ if (existing) existing.remove();
1286
+
1287
+ const overlay = document.createElement('div');
1288
+ overlay.id = 'fraim-first-run-landing';
1289
+ overlay.style.cssText = [
1290
+ 'position:fixed;inset:0;z-index:1000;',
1291
+ 'background:var(--bg,#f4f6f2);',
1292
+ 'display:flex;align-items:center;justify-content:center;',
1293
+ 'font-family:inherit;',
1294
+ ].join('');
1295
+
1296
+ const card = document.createElement('div');
1297
+ card.style.cssText = [
1298
+ 'background:#fff;border:1px solid var(--line,#e3e8df);border-radius:14px;',
1299
+ 'padding:32px;max-width:480px;width:90%;display:flex;flex-direction:column;gap:16px;',
1300
+ 'box-shadow:0 1px 2px rgba(20,40,30,.04);',
1301
+ ].join('');
1302
+
1303
+ const title = document.createElement('h1');
1304
+ title.textContent = "Let's get started";
1305
+ title.style.cssText = 'font-size:22px;font-weight:600;margin:0;';
1306
+ card.appendChild(title);
1307
+
1308
+ const desc = document.createElement('p');
1309
+ desc.textContent = "Let's introduce your AI employees to their first project.";
1310
+ desc.style.cssText = 'color:var(--muted,#6b7a72);margin:0;font-size:15px;';
1311
+ card.appendChild(desc);
1312
+
1313
+ const folderLabel = document.createElement('label');
1314
+ folderLabel.textContent = 'Project folder';
1315
+ folderLabel.style.cssText = 'font-size:13px;color:var(--muted,#6b7a72);font-weight:600;';
1316
+ card.appendChild(folderLabel);
1317
+
1318
+ const pickerRow = document.createElement('div');
1319
+ pickerRow.style.cssText = 'display:flex;gap:8px;align-items:center;';
1320
+
1321
+ const folderInput = document.createElement('input');
1322
+ folderInput.type = 'text';
1323
+ folderInput.placeholder = 'Choose a folder…';
1324
+ folderInput.readOnly = true;
1325
+ folderInput.style.cssText = [
1326
+ 'flex:1;border:1px solid var(--line,#e3e8df);border-radius:8px;',
1327
+ 'padding:9px 12px;font-size:14px;color:var(--muted,#6b7a72);',
1328
+ 'background:var(--soft,#f7faf6);',
1329
+ ].join('');
1330
+ if (state.projectPath) {
1331
+ folderInput.value = state.projectPath;
1332
+ folderInput.style.color = 'var(--text,#1f2a24)';
1333
+ folderInput.style.background = '#fff';
1334
+ }
1335
+
1336
+ const browseBtn = document.createElement('button');
1337
+ browseBtn.type = 'button';
1338
+ browseBtn.textContent = 'Browse…';
1339
+ browseBtn.style.cssText = [
1340
+ 'border:1px solid var(--line,#e3e8df);border-radius:8px;',
1341
+ 'background:transparent;color:var(--muted,#6b7a72);',
1342
+ 'padding:9px 14px;font-size:13px;font-weight:600;cursor:pointer;',
1343
+ ].join('');
1344
+ browseBtn.addEventListener('click', async () => {
1345
+ try {
1346
+ const response = await fetch('/api/ai-hub/project-path/pick', { method: 'POST' });
1347
+ if (response.status === 204) return;
1348
+ const payload = await response.json();
1349
+ if (!response.ok) throw new Error(payload.error || 'Could not pick folder.');
1350
+ if (payload.path) {
1351
+ await loadBootstrap(payload.path);
1352
+ folderInput.value = state.projectPath;
1353
+ folderInput.style.color = 'var(--text,#1f2a24)';
1354
+ folderInput.style.background = '#fff';
1355
+ startBtn.disabled = false;
1356
+ startBtn.style.opacity = '1';
1357
+ }
1358
+ } catch (err) {
1359
+ showStatus(err.message, true);
1360
+ }
1361
+ });
1362
+
1363
+ pickerRow.appendChild(folderInput);
1364
+ pickerRow.appendChild(browseBtn);
1365
+ card.appendChild(pickerRow);
1366
+
1367
+ const startBtn = document.createElement('button');
1368
+ startBtn.type = 'button';
1369
+ startBtn.textContent = 'Start onboarding →';
1370
+ startBtn.disabled = !state.projectPath;
1371
+ startBtn.style.cssText = [
1372
+ 'background:var(--accent,#3d8a6e);color:#fff;border:none;border-radius:8px;',
1373
+ 'padding:11px 20px;font-size:14px;font-weight:600;cursor:pointer;',
1374
+ 'opacity:' + (state.projectPath ? '1' : '0.5') + ';',
1375
+ 'transition:opacity .15s;',
1376
+ ].join('');
1377
+ startBtn.addEventListener('click', async () => {
1378
+ if (!state.projectPath) return;
1379
+ startBtn.disabled = true;
1380
+ startBtn.textContent = 'Starting…';
1381
+ overlay.remove();
1382
+ renderActive();
1383
+ // Find the project-onboarding job from bootstrap, fall back to direct POST.
1384
+ const job = (state.bootstrap && state.bootstrap.jobs || []).find((j) => j.id === 'project-onboarding');
1385
+ const employeeId = state.selectedEmployeeId || 'claude';
1386
+ if (job) {
1387
+ await startRun(job, 'Onboard this project', employeeId);
1388
+ } else {
1389
+ // Job not in catalog yet (project not initialized) — POST directly.
1390
+ try {
1391
+ const run = await requestJson('/api/ai-hub/runs', {
1392
+ method: 'POST',
1393
+ headers: { 'Content-Type': 'application/json' },
1394
+ body: JSON.stringify({
1395
+ projectPath: state.projectPath,
1396
+ hostId: employeeId,
1397
+ jobId: 'project-onboarding',
1398
+ message: 'Onboard this project',
1399
+ }),
1400
+ });
1401
+ const conv = {
1402
+ id: newConversationId(),
1403
+ projectPath: state.projectPath,
1404
+ title: 'Project Onboarding',
1405
+ jobId: 'project-onboarding',
1406
+ jobTitle: 'Project Onboarding',
1407
+ employeeId,
1408
+ runId: run.id,
1409
+ sessionId: run.sessionId || null,
1410
+ status: 'running',
1411
+ messages: [{ role: 'manager', text: 'Onboard this project', at: Date.now() }],
1412
+ events: [],
1413
+ artifacts: [],
1414
+ lastUpdatedAt: Date.now(),
1415
+ };
1416
+ foldRunIntoConversation(conv, run);
1417
+ upsertConversation(conv);
1418
+ state.activeId = conv.id;
1419
+ persistConversations();
1420
+ renderRail();
1421
+ renderActive();
1422
+ startPolling();
1423
+ } catch (err) {
1424
+ showStatus(err.message, true);
1425
+ }
1426
+ }
1427
+ });
1428
+ card.appendChild(startBtn);
1429
+
1430
+ const switchLink = document.createElement('a');
1431
+ switchLink.href = '#';
1432
+ switchLink.textContent = 'Set up in my IDE instead';
1433
+ switchLink.style.cssText = 'font-size:12px;color:var(--muted,#6b7a72);text-decoration:underline;text-align:center;';
1434
+ switchLink.addEventListener('click', (e) => {
1435
+ e.preventDefault();
1436
+ overlay.remove();
1437
+ renderActive();
1438
+ });
1439
+ card.appendChild(switchLink);
1440
+
1441
+ overlay.appendChild(card);
1442
+ document.body.appendChild(overlay);
1443
+ }
@@ -1,100 +1,100 @@
1
- /**
2
- * R6.1 — unified error-frame renderer.
3
- *
4
- * Exposes a single function `renderErrorFrame(frame, onAction)` which returns
5
- * a DOM Node containing exactly:
6
- * - "What we tried" sentence
7
- * - "What happened" verbatim stderr (last 12 lines, with a Show-full toggle
8
- * that surfaces the rest on demand)
9
- * - Up to three actions in fixed order (Retry / Try alternative / Skip and continue)
10
- *
11
- * The same renderer must be lifted into public/ai-hub/ in the v2 follow-up
12
- * (issue #355, "Hub-side error-frame adoption") so bootstrap and Hub share
13
- * one frame across surfaces.
14
- */
15
- (function () {
16
- 'use strict';
17
-
18
- function escapeText(text) {
19
- if (typeof text !== 'string') return '';
20
- return text;
21
- }
22
-
23
- function truncateToLastLines(text, lineCount) {
24
- if (typeof text !== 'string') return { visible: '', hidden: '' };
25
- const lines = text.split(/\r?\n/);
26
- if (lines.length <= lineCount) {
27
- return { visible: text, hidden: '' };
28
- }
29
- const visible = lines.slice(-lineCount).join('\n');
30
- const hidden = lines.slice(0, -lineCount).join('\n');
31
- return { visible, hidden };
32
- }
33
-
34
- function renderErrorFrame(frame, onAction) {
35
- const root = document.createElement('div');
36
- root.className = 'error-frame';
37
- root.setAttribute('data-testid', 'error-frame');
38
- root.setAttribute('role', 'alert');
39
-
40
- const whatTried = document.createElement('div');
41
- whatTried.className = 'what-tried';
42
- whatTried.setAttribute('data-testid', 'error-what-tried');
43
- whatTried.textContent = escapeText(frame.whatTried || '');
44
- root.appendChild(whatTried);
45
-
46
- // Optional plain-language hint rendered ABOVE the verbatim stderr so
47
- // a non-tech user reads "your install key was rejected — get a new
48
- // one" before they parse `Request failed with status code 401`.
49
- if (frame.hint) {
50
- const hint = document.createElement('div');
51
- hint.className = 'error-hint';
52
- hint.setAttribute('data-testid', 'error-hint');
53
- hint.textContent = escapeText(frame.hint);
54
- root.appendChild(hint);
55
- }
56
-
57
- const { visible, hidden } = truncateToLastLines(frame.whatHappened || '', 12);
58
- const whatHappened = document.createElement('pre');
59
- whatHappened.className = 'what-happened';
60
- whatHappened.setAttribute('data-testid', 'error-what-happened');
61
- whatHappened.textContent = visible;
62
- root.appendChild(whatHappened);
63
-
64
- if (hidden) {
65
- const showFull = document.createElement('button');
66
- showFull.type = 'button';
67
- showFull.className = 'show-full';
68
- showFull.setAttribute('data-testid', 'error-show-full');
69
- showFull.textContent = 'Show full output';
70
- showFull.addEventListener('click', () => {
71
- whatHappened.textContent = (hidden + '\n' + visible).trimEnd();
72
- showFull.remove();
73
- });
74
- root.appendChild(showFull);
75
- }
76
-
77
- const actions = Array.isArray(frame.actions) ? frame.actions.slice(0, 3) : [];
78
- const actionRow = document.createElement('div');
79
- actionRow.className = 'actions';
80
- for (const action of actions) {
81
- const btn = document.createElement('button');
82
- btn.type = 'button';
83
- btn.className = 'action';
84
- btn.setAttribute('data-testid', 'error-action');
85
- btn.setAttribute('data-action-id', action.id);
86
- btn.setAttribute('data-variant', action.variant || 'secondary');
87
- btn.textContent = action.label || action.id;
88
- btn.addEventListener('click', () => {
89
- if (typeof onAction === 'function') {
90
- onAction(action);
91
- }
92
- });
93
- actionRow.appendChild(btn);
94
- }
95
- root.appendChild(actionRow);
96
- return root;
97
- }
98
-
99
- window.FraimErrorFrame = { render: renderErrorFrame };
100
- })();
1
+ /**
2
+ * R6.1 — unified error-frame renderer.
3
+ *
4
+ * Exposes a single function `renderErrorFrame(frame, onAction)` which returns
5
+ * a DOM Node containing exactly:
6
+ * - "What we tried" sentence
7
+ * - "What happened" verbatim stderr (last 12 lines, with a Show-full toggle
8
+ * that surfaces the rest on demand)
9
+ * - Up to three actions in fixed order (Retry / Try alternative / Skip and continue)
10
+ *
11
+ * The same renderer must be lifted into public/ai-hub/ in the v2 follow-up
12
+ * (issue #355, "Hub-side error-frame adoption") so bootstrap and Hub share
13
+ * one frame across surfaces.
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ function escapeText(text) {
19
+ if (typeof text !== 'string') return '';
20
+ return text;
21
+ }
22
+
23
+ function truncateToLastLines(text, lineCount) {
24
+ if (typeof text !== 'string') return { visible: '', hidden: '' };
25
+ const lines = text.split(/\r?\n/);
26
+ if (lines.length <= lineCount) {
27
+ return { visible: text, hidden: '' };
28
+ }
29
+ const visible = lines.slice(-lineCount).join('\n');
30
+ const hidden = lines.slice(0, -lineCount).join('\n');
31
+ return { visible, hidden };
32
+ }
33
+
34
+ function renderErrorFrame(frame, onAction) {
35
+ const root = document.createElement('div');
36
+ root.className = 'error-frame';
37
+ root.setAttribute('data-testid', 'error-frame');
38
+ root.setAttribute('role', 'alert');
39
+
40
+ const whatTried = document.createElement('div');
41
+ whatTried.className = 'what-tried';
42
+ whatTried.setAttribute('data-testid', 'error-what-tried');
43
+ whatTried.textContent = escapeText(frame.whatTried || '');
44
+ root.appendChild(whatTried);
45
+
46
+ // Optional plain-language hint rendered ABOVE the verbatim stderr so
47
+ // a non-tech user reads "your install key was rejected — get a new
48
+ // one" before they parse `Request failed with status code 401`.
49
+ if (frame.hint) {
50
+ const hint = document.createElement('div');
51
+ hint.className = 'error-hint';
52
+ hint.setAttribute('data-testid', 'error-hint');
53
+ hint.textContent = escapeText(frame.hint);
54
+ root.appendChild(hint);
55
+ }
56
+
57
+ const { visible, hidden } = truncateToLastLines(frame.whatHappened || '', 12);
58
+ const whatHappened = document.createElement('pre');
59
+ whatHappened.className = 'what-happened';
60
+ whatHappened.setAttribute('data-testid', 'error-what-happened');
61
+ whatHappened.textContent = visible;
62
+ root.appendChild(whatHappened);
63
+
64
+ if (hidden) {
65
+ const showFull = document.createElement('button');
66
+ showFull.type = 'button';
67
+ showFull.className = 'show-full';
68
+ showFull.setAttribute('data-testid', 'error-show-full');
69
+ showFull.textContent = 'Show full output';
70
+ showFull.addEventListener('click', () => {
71
+ whatHappened.textContent = (hidden + '\n' + visible).trimEnd();
72
+ showFull.remove();
73
+ });
74
+ root.appendChild(showFull);
75
+ }
76
+
77
+ const actions = Array.isArray(frame.actions) ? frame.actions.slice(0, 3) : [];
78
+ const actionRow = document.createElement('div');
79
+ actionRow.className = 'actions';
80
+ for (const action of actions) {
81
+ const btn = document.createElement('button');
82
+ btn.type = 'button';
83
+ btn.className = 'action';
84
+ btn.setAttribute('data-testid', 'error-action');
85
+ btn.setAttribute('data-action-id', action.id);
86
+ btn.setAttribute('data-variant', action.variant || 'secondary');
87
+ btn.textContent = action.label || action.id;
88
+ btn.addEventListener('click', () => {
89
+ if (typeof onAction === 'function') {
90
+ onAction(action);
91
+ }
92
+ });
93
+ actionRow.appendChild(btn);
94
+ }
95
+ root.appendChild(actionRow);
96
+ return root;
97
+ }
98
+
99
+ window.FraimErrorFrame = { render: renderErrorFrame };
100
+ })();