fraim 2.0.128 → 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.
- package/dist/src/ai-hub/catalog.js +7 -4
- package/dist/src/cli/commands/first-run.js +14 -1
- package/dist/src/cli/commands/init-project.js +55 -121
- package/dist/src/cli/commands/setup.js +68 -43
- package/dist/src/cli/commands/sync.js +8 -1
- package/dist/src/cli/commands/workspace-config.js +31 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/ide-global-integration.js +19 -0
- package/dist/src/cli/setup/user-level-sync.js +5 -0
- package/dist/src/cli/utils/project-bootstrap.js +3 -3
- package/dist/src/core/fraim-config-contract.js +145 -0
- package/dist/src/core/fraim-config-schema.generated.js +296 -0
- package/dist/src/core/utils/setup-preferences.js +41 -0
- package/dist/src/first-run/server.js +118 -18
- package/dist/src/first-run/session-service.js +282 -364
- package/dist/src/first-run/types.js +10 -21
- package/dist/src/local-mcp-server/stdio-server.js +28 -29
- package/dist/src/local-mcp-server/usage-collector.js +3 -0
- package/index.js +1 -1
- package/package.json +7 -5
- package/public/ai-hub/script.js +187 -1
- package/public/first-run/error-frame.js +100 -89
- package/public/first-run/index.html +5 -6
- package/public/first-run/script.js +275 -227
- package/public/first-run/styles.css +603 -386
|
@@ -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: '
|
|
64
|
-
{ id: 'agent
|
|
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
|
-
* - "
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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 '
|
|
90
|
-
//
|
|
91
|
-
if (
|
|
92
|
-
return '
|
|
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
|
|
241
|
-
const
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
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 &&
|
|
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
|
},
|
package/public/ai-hub/script.js
CHANGED
|
@@ -367,12 +367,22 @@ let renderedTrackerKey = null;
|
|
|
367
367
|
function renderTracker(conv) {
|
|
368
368
|
const tracker = els['tracker'];
|
|
369
369
|
if (!tracker) return;
|
|
370
|
-
|
|
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,89 +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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
})();
|