fraim 2.0.167 → 2.0.169
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 +28 -14
- package/dist/src/ai-hub/server.js +34 -406
- package/dist/src/cli/commands/init-project.js +1 -98
- package/dist/src/cli/commands/manager.js +40 -0
- package/dist/src/cli/commands/sync.js +17 -21
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/utils/github-workflow-sync.js +12 -146
- package/dist/src/cli/utils/manager-pack-sync.js +188 -0
- package/dist/src/cli/utils/manager-publish.js +76 -0
- package/dist/src/cli/utils/user-config.js +20 -0
- package/dist/src/core/fraim-config-schema.generated.js +85 -10
- package/dist/src/core/manager-pack.js +26 -0
- package/dist/src/first-run/install-state.js +1 -0
- package/dist/src/first-run/server.js +9 -0
- package/dist/src/first-run/session-service.js +117 -23
- package/dist/src/first-run/types.js +2 -5
- package/dist/src/local-mcp-server/learning-context-builder.js +45 -8
- package/dist/src/local-mcp-server/stdio-server.js +28 -0
- package/package.json +1 -2
- package/public/ai-hub/index.html +0 -81
- package/public/ai-hub/script.js +3 -219
- package/public/ai-hub/styles.css +8 -36
- package/public/first-run/index.html +1 -1
- package/public/first-run/script.js +459 -530
- package/public/first-run/styles.css +288 -73
- package/dist/src/config/ai-manager-hiring.js +0 -121
- package/dist/src/config/compat.js +0 -16
- package/dist/src/config/feature-flags.js +0 -25
- package/dist/src/config/persona-capability-bundles.js +0 -273
- package/dist/src/config/persona-hiring.js +0 -270
- package/dist/src/config/portfolio-slug-overrides.js +0 -17
- package/dist/src/config/pricing.js +0 -37
- package/dist/src/config/stripe.js +0 -43
|
@@ -47,10 +47,12 @@ function getLearningRoots(workspaceRoot) {
|
|
|
47
47
|
return {
|
|
48
48
|
globalPersonalBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(workspaceRoot),
|
|
49
49
|
globalPersonalDisplayBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDisplayPath)(workspaceRoot),
|
|
50
|
+
managerCacheBase: (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', 'learnings'),
|
|
51
|
+
managerCacheDisplayBase: (0, project_fraim_paths_1.getUserFraimDisplayPath)('manager/learnings'),
|
|
50
52
|
repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
|
-
function resolvePersonalLearningFile(repoBase, globalBase, globalDisplayBase, fileName) {
|
|
55
|
+
function resolvePersonalLearningFile(repoBase, managerCacheBase, managerCacheDisplayBase, globalBase, globalDisplayBase, fileName) {
|
|
54
56
|
const repoPath = (0, path_1.join)(repoBase, fileName);
|
|
55
57
|
if ((0, fs_1.existsSync)(repoPath)) {
|
|
56
58
|
return {
|
|
@@ -67,6 +69,14 @@ function resolvePersonalLearningFile(repoBase, globalBase, globalDisplayBase, fi
|
|
|
67
69
|
displayPath: `${globalDisplayBase.replace(/\/$/, '')}/${fileName}`
|
|
68
70
|
};
|
|
69
71
|
}
|
|
72
|
+
const managerCachePath = (0, path_1.join)(managerCacheBase, fileName);
|
|
73
|
+
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
74
|
+
return {
|
|
75
|
+
present: true,
|
|
76
|
+
path: managerCachePath,
|
|
77
|
+
displayPath: `${managerCacheDisplayBase.replace(/\/$/, '')}/${fileName}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
70
80
|
return { present: false, path: globalPath, displayPath: `${globalDisplayBase.replace(/\/$/, '')}/${fileName}` };
|
|
71
81
|
}
|
|
72
82
|
function buildUserIdCandidates(userId) {
|
|
@@ -132,6 +142,10 @@ function resolveLearningUserId(workspaceRoot, userId, roots) {
|
|
|
132
142
|
((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
|
|
133
143
|
((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
|
|
134
144
|
((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-validated-patterns.md`)) ? 1 : 0) +
|
|
145
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-preferences.md`)) ? 1 : 0) +
|
|
146
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
|
|
147
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
|
|
148
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-validated-patterns.md`)) ? 1 : 0) +
|
|
135
149
|
((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-preferences.md`)) ? 1 : 0) +
|
|
136
150
|
((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
|
|
137
151
|
((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
|
|
@@ -145,7 +159,7 @@ function resolveLearningUserId(workspaceRoot, userId, roots) {
|
|
|
145
159
|
}
|
|
146
160
|
if (bestScore > 0)
|
|
147
161
|
return bestCandidate;
|
|
148
|
-
const availablePrefixes = collectAvailableUserPrefixes(workspaceRoot, [roots.repoLearningsBase, roots.globalPersonalBase]);
|
|
162
|
+
const availablePrefixes = collectAvailableUserPrefixes(workspaceRoot, [roots.repoLearningsBase, roots.managerCacheBase, roots.globalPersonalBase]);
|
|
149
163
|
if (availablePrefixes.size === 1) {
|
|
150
164
|
return Array.from(availablePrefixes)[0];
|
|
151
165
|
}
|
|
@@ -375,10 +389,10 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
375
389
|
const l2ValidatedPresent = l2Validated.present;
|
|
376
390
|
const l2MistakeStats = l2MistakePresent ? scanMistakePatternFile(l2Mistake.path, threshold, 'mistake-patterns') : null;
|
|
377
391
|
const l2ValidatedStats = l2ValidatedPresent ? scanMistakePatternFile(l2Validated.path, threshold, 'validated-patterns') : null;
|
|
378
|
-
const l1Mistake = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-mistake-patterns.md`);
|
|
379
|
-
const l1Pref = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-preferences.md`);
|
|
380
|
-
const l1Coach = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-manager-coaching.md`);
|
|
381
|
-
const l1Validated = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-validated-patterns.md`);
|
|
392
|
+
const l1Mistake = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-mistake-patterns.md`);
|
|
393
|
+
const l1Pref = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-preferences.md`);
|
|
394
|
+
const l1Coach = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-manager-coaching.md`);
|
|
395
|
+
const l1Validated = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-validated-patterns.md`);
|
|
382
396
|
const l1MistakeStats = l1Mistake.present ? scanMistakePatternFile(l1Mistake.path, threshold, 'mistake-patterns') : null;
|
|
383
397
|
const l1ValidatedStats = l1Validated.present ? scanMistakePatternFile(l1Validated.path, threshold, 'validated-patterns') : null;
|
|
384
398
|
let l0CoachingCount = 0;
|
|
@@ -509,6 +523,15 @@ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = t
|
|
|
509
523
|
};
|
|
510
524
|
}
|
|
511
525
|
}
|
|
526
|
+
if (!orgCacheEligible) {
|
|
527
|
+
const managerCachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', relativePath);
|
|
528
|
+
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
529
|
+
return {
|
|
530
|
+
present: true,
|
|
531
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`manager/${relativePath}`)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
512
535
|
const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
|
|
513
536
|
if ((0, fs_1.existsSync)(userPath)) {
|
|
514
537
|
return {
|
|
@@ -675,6 +698,19 @@ function resolveTeamContextFile(workspaceRoot, key) {
|
|
|
675
698
|
};
|
|
676
699
|
}
|
|
677
700
|
}
|
|
701
|
+
if (key === 'manager' || key === 'managerRules') {
|
|
702
|
+
const managerCachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', relativePath);
|
|
703
|
+
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
704
|
+
return {
|
|
705
|
+
present: true,
|
|
706
|
+
readPath: managerCachePath,
|
|
707
|
+
writePath: '',
|
|
708
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`manager/${relativePath}`),
|
|
709
|
+
scope,
|
|
710
|
+
managedByManagerSync: true
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
678
714
|
const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
|
|
679
715
|
const userDisplay = (0, project_fraim_paths_1.getUserFraimDisplayPath)(`personalized-employee/${relativePath}`);
|
|
680
716
|
return {
|
|
@@ -728,7 +764,7 @@ function countPreservedLearnings(workspaceRoot, userId) {
|
|
|
728
764
|
countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-preferences.md').path) +
|
|
729
765
|
countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-manager-coaching.md').path) +
|
|
730
766
|
countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-validated-patterns.md').path);
|
|
731
|
-
const resolve = (fileName) => resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
|
|
767
|
+
const resolve = (fileName) => resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
|
|
732
768
|
// L1 manager-facing reverse-mentoring file.
|
|
733
769
|
const manager = countLearningEntries(resolve(`${resolvedUserId}-manager-coaching.md`).path);
|
|
734
770
|
// L1 personal work patterns (project scope).
|
|
@@ -818,8 +854,9 @@ function parseLearningEntries(filePath, displayPath, category, level) {
|
|
|
818
854
|
return out;
|
|
819
855
|
}
|
|
820
856
|
function levelDir(roots, level) {
|
|
821
|
-
if (level === 'machine')
|
|
857
|
+
if (level === 'machine') {
|
|
822
858
|
return { dir: roots.globalPersonalBase, displayBase: roots.globalPersonalDisplayBase.replace(/\/$/, '') };
|
|
859
|
+
}
|
|
823
860
|
return { dir: roots.repoLearningsBase, displayBase: REPO_LEARNINGS_REL };
|
|
824
861
|
}
|
|
825
862
|
function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine') {
|
|
@@ -416,6 +416,7 @@ class FraimLocalMCPServer {
|
|
|
416
416
|
this.connectSyncInFlight = null;
|
|
417
417
|
this.latestConnectSyncWarning = null;
|
|
418
418
|
this.orgCacheRefreshInFlight = false;
|
|
419
|
+
this.managerCacheRefreshInFlight = false;
|
|
419
420
|
this.writer = writer || process.stdout.write.bind(process.stdout);
|
|
420
421
|
this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
421
422
|
this.apiKey = this.loadApiKey();
|
|
@@ -750,6 +751,7 @@ class FraimLocalMCPServer {
|
|
|
750
751
|
this.connectSyncInFlight = null;
|
|
751
752
|
}
|
|
752
753
|
this.maybeRefreshOrgCache(String(requestId));
|
|
754
|
+
this.maybeRefreshManagerCache(String(requestId));
|
|
753
755
|
}
|
|
754
756
|
/**
|
|
755
757
|
* Issue #563 (R4.2): opportunistically refresh the managed org cache
|
|
@@ -779,6 +781,32 @@ class FraimLocalMCPServer {
|
|
|
779
781
|
this.orgCacheRefreshInFlight = false;
|
|
780
782
|
});
|
|
781
783
|
}
|
|
784
|
+
/**
|
|
785
|
+
* Issue #580: opportunistically refresh the managed manager cache
|
|
786
|
+
* (~/.fraim/manager/) at session bootstrap when it is missing or older than
|
|
787
|
+
* 24 hours. Fire-and-forget; a failed refresh leaves existing cache available.
|
|
788
|
+
*/
|
|
789
|
+
maybeRefreshManagerCache(requestId) {
|
|
790
|
+
if (this.managerCacheRefreshInFlight)
|
|
791
|
+
return;
|
|
792
|
+
this.managerCacheRefreshInFlight = true;
|
|
793
|
+
void Promise.resolve().then(() => __importStar(require('../cli/utils/manager-pack-sync'))).then(async ({ getManagerCacheAgeHours, syncManagerCache }) => {
|
|
794
|
+
const ageHours = getManagerCacheAgeHours();
|
|
795
|
+
if (ageHours !== null && ageHours < 24)
|
|
796
|
+
return;
|
|
797
|
+
const outcome = await syncManagerCache();
|
|
798
|
+
if (outcome.status !== 'disabled') {
|
|
799
|
+
const version = outcome.metadata ? ` (version ${outcome.metadata.version.slice(0, 12)})` : '';
|
|
800
|
+
this.log(`[req:${requestId}] Manager cache refresh at connect: ${outcome.status}${version}`);
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
.catch((error) => {
|
|
804
|
+
this.logError(`[req:${requestId}] Manager cache refresh failed: ${error?.message || error}`);
|
|
805
|
+
})
|
|
806
|
+
.finally(() => {
|
|
807
|
+
this.managerCacheRefreshInFlight = false;
|
|
808
|
+
});
|
|
809
|
+
}
|
|
782
810
|
/**
|
|
783
811
|
* Automatically detect machine information
|
|
784
812
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.169",
|
|
4
4
|
"description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -122,7 +122,6 @@
|
|
|
122
122
|
"dist/src/ai-hub/",
|
|
123
123
|
"dist/src/first-run/",
|
|
124
124
|
"dist/src/core/",
|
|
125
|
-
"dist/src/config/",
|
|
126
125
|
"bin/fraim.js",
|
|
127
126
|
"bin/fraim-mcp.js",
|
|
128
127
|
"public/ai-hub/",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -194,18 +194,6 @@
|
|
|
194
194
|
<span class="ca-note">— from <strong>sleep-on-learnings</strong> · promote to a scope</span></summary>
|
|
195
195
|
<div class="ctx-acc-body"><div id="project-learnings"></div></div>
|
|
196
196
|
</details>
|
|
197
|
-
<!-- Issue #578: Scheduled + Reactive Employees — deployment roster -->
|
|
198
|
-
<details class="ctx-acc" id="proj-deployments-acc">
|
|
199
|
-
<summary><span class="ca-chev">▸</span> <span>⏱ Deployments</span>
|
|
200
|
-
<span class="ca-note">— scheduled and webhook triggers for this project</span></summary>
|
|
201
|
-
<div class="ctx-acc-body">
|
|
202
|
-
<div id="proj-deployments-list"></div>
|
|
203
|
-
<div class="dep-actions">
|
|
204
|
-
<button class="dep-add-btn" id="dep-add-schedule-btn" type="button">+ Schedule</button>
|
|
205
|
-
<button class="dep-add-btn" id="dep-add-webhook-btn" type="button">+ Webhook</button>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
</details>
|
|
209
197
|
</div>
|
|
210
198
|
|
|
211
199
|
|
|
@@ -680,75 +668,6 @@
|
|
|
680
668
|
</div>
|
|
681
669
|
</div>
|
|
682
670
|
|
|
683
|
-
<!-- Issue #578: Add Schedule deployment modal -->
|
|
684
|
-
<div id="dep-schedule-modal" class="modal-overlay" hidden>
|
|
685
|
-
<div class="modal-card" style="max-width:480px;">
|
|
686
|
-
<div class="modal-hdr">
|
|
687
|
-
<button class="modal-close" id="dep-schedule-close" type="button" aria-label="Close">×</button>
|
|
688
|
-
<h2>New Scheduled Deployment</h2>
|
|
689
|
-
</div>
|
|
690
|
-
<div class="modal-body">
|
|
691
|
-
<div class="hm-field">
|
|
692
|
-
<label for="dep-sch-label">Label</label>
|
|
693
|
-
<input type="text" id="dep-sch-label" placeholder="e.g. Nightly standup" />
|
|
694
|
-
</div>
|
|
695
|
-
<div class="hm-field">
|
|
696
|
-
<label for="dep-sch-job">Job</label>
|
|
697
|
-
<select id="dep-sch-job"></select>
|
|
698
|
-
</div>
|
|
699
|
-
<div class="hm-field">
|
|
700
|
-
<label for="dep-sch-cron">Cron expression</label>
|
|
701
|
-
<input type="text" id="dep-sch-cron" placeholder="e.g. 0 9 * * 1-5 (9am Mon-Fri)" />
|
|
702
|
-
</div>
|
|
703
|
-
<div class="hm-field">
|
|
704
|
-
<label for="dep-sch-instructions">Initial instructions (optional)</label>
|
|
705
|
-
<textarea id="dep-sch-instructions" rows="2" placeholder="Optional coaching message to send at run start"></textarea>
|
|
706
|
-
</div>
|
|
707
|
-
<div class="dep-modal-actions">
|
|
708
|
-
<button type="button" id="dep-sch-save-btn" class="send-button">Create schedule</button>
|
|
709
|
-
<button type="button" id="dep-sch-cancel-btn" class="cancel-button">Cancel</button>
|
|
710
|
-
</div>
|
|
711
|
-
<p id="dep-sch-error" class="dep-error" hidden></p>
|
|
712
|
-
</div>
|
|
713
|
-
</div>
|
|
714
|
-
</div>
|
|
715
|
-
|
|
716
|
-
<!-- Issue #578: Add Webhook deployment modal -->
|
|
717
|
-
<div id="dep-webhook-modal" class="modal-overlay" hidden>
|
|
718
|
-
<div class="modal-card" style="max-width:480px;">
|
|
719
|
-
<div class="modal-hdr">
|
|
720
|
-
<button class="modal-close" id="dep-webhook-close" type="button" aria-label="Close">×</button>
|
|
721
|
-
<h2>New Webhook Deployment</h2>
|
|
722
|
-
</div>
|
|
723
|
-
<div class="modal-body">
|
|
724
|
-
<div class="hm-field">
|
|
725
|
-
<label for="dep-wh-label">Label</label>
|
|
726
|
-
<input type="text" id="dep-wh-label" placeholder="e.g. ServiceNow escalation" />
|
|
727
|
-
</div>
|
|
728
|
-
<div class="hm-field">
|
|
729
|
-
<label for="dep-wh-job">Job</label>
|
|
730
|
-
<select id="dep-wh-job"></select>
|
|
731
|
-
</div>
|
|
732
|
-
<div class="hm-field">
|
|
733
|
-
<label for="dep-wh-instructions">Initial instructions (optional)</label>
|
|
734
|
-
<textarea id="dep-wh-instructions" rows="2" placeholder="Optional coaching message to prepend at run start"></textarea>
|
|
735
|
-
</div>
|
|
736
|
-
<div class="dep-modal-actions">
|
|
737
|
-
<button type="button" id="dep-wh-save-btn" class="send-button">Create webhook</button>
|
|
738
|
-
<button type="button" id="dep-wh-cancel-btn" class="cancel-button">Cancel</button>
|
|
739
|
-
</div>
|
|
740
|
-
<p id="dep-wh-error" class="dep-error" hidden></p>
|
|
741
|
-
<div id="dep-wh-inbound-row" hidden>
|
|
742
|
-
<label>Inbound URL (copy to your system)</label>
|
|
743
|
-
<div class="dep-inbound-url-row">
|
|
744
|
-
<code id="dep-wh-inbound-url" class="dep-inbound-url"></code>
|
|
745
|
-
<button type="button" id="dep-wh-copy-btn" class="hm-copy-btn">Copy</button>
|
|
746
|
-
</div>
|
|
747
|
-
</div>
|
|
748
|
-
</div>
|
|
749
|
-
</div>
|
|
750
|
-
</div>
|
|
751
|
-
|
|
752
671
|
<!-- Issue #539: Command palette overlay — replaces 2-step modal as job-launch surface -->
|
|
753
672
|
<div id="cp-modal" class="cp-backdrop" role="dialog" aria-modal="true" aria-label="Start a job" hidden>
|
|
754
673
|
<div class="cp-palette">
|
package/public/ai-hub/script.js
CHANGED
|
@@ -774,16 +774,6 @@ function renderRail() {
|
|
|
774
774
|
statusSpan.textContent = conversationStateLabel(conv);
|
|
775
775
|
statusSpan.classList.add(conversationUiState(conv));
|
|
776
776
|
btn.appendChild(statusSpan);
|
|
777
|
-
// Issue #578: sourceTrigger chip — show scheduled/webhook badge on automated runs.
|
|
778
|
-
if (conv.sourceTrigger && conv.sourceTrigger !== 'manager') {
|
|
779
|
-
const trigBadge = document.createElement('span');
|
|
780
|
-
trigBadge.className = 'trigger-badge trigger-badge--' + conv.sourceTrigger;
|
|
781
|
-
trigBadge.textContent = conv.sourceTrigger === 'scheduled' ? 'Scheduled' : 'Webhook';
|
|
782
|
-
trigBadge.title = conv.sourceTrigger === 'scheduled'
|
|
783
|
-
? 'This run was started by the built-in cron scheduler'
|
|
784
|
-
: 'This run was triggered by an inbound webhook';
|
|
785
|
-
btn.appendChild(trigBadge);
|
|
786
|
-
}
|
|
787
777
|
// Issue #442: A/B badge on rail entry.
|
|
788
778
|
if (conv.compareMode === 'ab') {
|
|
789
779
|
const badge = document.createElement('span');
|
|
@@ -3814,7 +3804,7 @@ function renderAgentInstallPanel() {
|
|
|
3814
3804
|
|
|
3815
3805
|
const skipBtn = document.createElement('button');
|
|
3816
3806
|
skipBtn.className = 'ghost small';
|
|
3817
|
-
skipBtn.textContent = '
|
|
3807
|
+
skipBtn.textContent = 'Choose another agent';
|
|
3818
3808
|
skipBtn.style.marginLeft = '6px';
|
|
3819
3809
|
skipBtn.addEventListener('click', () => {
|
|
3820
3810
|
delete agentInstallState[emp.id];
|
|
@@ -4255,18 +4245,7 @@ function startPolling() {
|
|
|
4255
4245
|
}
|
|
4256
4246
|
try {
|
|
4257
4247
|
if (!fraimDone) {
|
|
4258
|
-
const
|
|
4259
|
-
if (runResp.status === 404) {
|
|
4260
|
-
// Run no longer in server registry (e.g. after restart). Stop polling.
|
|
4261
|
-
window.clearInterval(state.pollHandle);
|
|
4262
|
-
state.pollHandle = null;
|
|
4263
|
-
conv.status = 'done';
|
|
4264
|
-
upsertConversation(conv);
|
|
4265
|
-
renderRail();
|
|
4266
|
-
renderActive();
|
|
4267
|
-
return;
|
|
4268
|
-
}
|
|
4269
|
-
const run = await runResp.json();
|
|
4248
|
+
const run = await requestJson(`/api/ai-hub/runs/${conv.runId}`);
|
|
4270
4249
|
foldRunIntoConversation(conv, run);
|
|
4271
4250
|
}
|
|
4272
4251
|
// Issue #442: also poll the compare run when present and not yet terminal.
|
|
@@ -4958,7 +4937,7 @@ function wireEvents() {
|
|
|
4958
4937
|
}
|
|
4959
4938
|
|
|
4960
4939
|
if (isFirstRun) {
|
|
4961
|
-
|
|
4940
|
+
renderActive();
|
|
4962
4941
|
return;
|
|
4963
4942
|
}
|
|
4964
4943
|
|
|
@@ -5372,8 +5351,6 @@ function tfSelectProjectView(view, projectId) {
|
|
|
5372
5351
|
tfRenderTree();
|
|
5373
5352
|
tfRenderProjectContextTop();
|
|
5374
5353
|
tfApplyWorkspaceMode();
|
|
5375
|
-
// Issue #578: reload deployment roster when entering workspace.
|
|
5376
|
-
loadDeployments();
|
|
5377
5354
|
}
|
|
5378
5355
|
}
|
|
5379
5356
|
|
|
@@ -5855,197 +5832,6 @@ function tfShowProjectBrief() { const a = document.getElementById('proj-brief-ac
|
|
|
5855
5832
|
function tfShowProjectLearnings() { const a = document.getElementById('proj-learnings-acc'); if (a) a.open = true; }
|
|
5856
5833
|
function tfHideProjectLearnings() { /* panels removed in #521; brief/learnings are inline top sections */ }
|
|
5857
5834
|
|
|
5858
|
-
// ---------------------------------------------------------------------------
|
|
5859
|
-
// Issue #578: Deployment roster (scheduled + webhook triggers)
|
|
5860
|
-
// ---------------------------------------------------------------------------
|
|
5861
|
-
|
|
5862
|
-
async function loadDeployments() {
|
|
5863
|
-
const list = document.getElementById('proj-deployments-list');
|
|
5864
|
-
if (!list) return;
|
|
5865
|
-
try {
|
|
5866
|
-
const [schResp, whResp] = await Promise.all([
|
|
5867
|
-
fetch('/api/ai-hub/schedules'),
|
|
5868
|
-
fetch('/api/ai-hub/webhooks'),
|
|
5869
|
-
]);
|
|
5870
|
-
const schedules = schResp.ok ? await schResp.json() : [];
|
|
5871
|
-
const webhooks = whResp.ok ? await whResp.json() : [];
|
|
5872
|
-
renderDeploymentList(list, [...schedules, ...webhooks]);
|
|
5873
|
-
} catch {
|
|
5874
|
-
list.textContent = 'Could not load deployments.';
|
|
5875
|
-
}
|
|
5876
|
-
}
|
|
5877
|
-
|
|
5878
|
-
function renderDeploymentList(container, deployments) {
|
|
5879
|
-
container.innerHTML = '';
|
|
5880
|
-
if (!deployments.length) {
|
|
5881
|
-
const empty = document.createElement('p');
|
|
5882
|
-
empty.className = 'dep-empty';
|
|
5883
|
-
empty.textContent = 'No deployments yet. Use + Schedule or + Webhook above to add automated triggers.';
|
|
5884
|
-
container.appendChild(empty);
|
|
5885
|
-
return;
|
|
5886
|
-
}
|
|
5887
|
-
for (const dep of deployments) {
|
|
5888
|
-
const card = document.createElement('div');
|
|
5889
|
-
card.className = 'dep-card';
|
|
5890
|
-
const top = document.createElement('div');
|
|
5891
|
-
top.className = 'dep-card-top';
|
|
5892
|
-
const icon = document.createElement('span');
|
|
5893
|
-
icon.className = 'dep-type-icon';
|
|
5894
|
-
icon.textContent = dep.type === 'scheduled' ? '⏱' : '🔗';
|
|
5895
|
-
const label = document.createElement('span');
|
|
5896
|
-
label.className = 'dep-label';
|
|
5897
|
-
label.textContent = dep.label;
|
|
5898
|
-
const typeBadge = document.createElement('span');
|
|
5899
|
-
typeBadge.className = 'dep-type-badge dep-type-badge--' + dep.type;
|
|
5900
|
-
typeBadge.textContent = dep.type === 'scheduled' ? 'Scheduled' : 'Webhook';
|
|
5901
|
-
top.appendChild(icon);
|
|
5902
|
-
top.appendChild(label);
|
|
5903
|
-
top.appendChild(typeBadge);
|
|
5904
|
-
card.appendChild(top);
|
|
5905
|
-
|
|
5906
|
-
if (dep.cronExpr) {
|
|
5907
|
-
const cron = document.createElement('div');
|
|
5908
|
-
cron.className = 'dep-detail';
|
|
5909
|
-
cron.textContent = dep.cronExpr;
|
|
5910
|
-
card.appendChild(cron);
|
|
5911
|
-
}
|
|
5912
|
-
if (dep.inboundUrl) {
|
|
5913
|
-
const urlRow = document.createElement('div');
|
|
5914
|
-
urlRow.className = 'dep-detail dep-detail--url';
|
|
5915
|
-
const urlCode = document.createElement('code');
|
|
5916
|
-
urlCode.textContent = dep.inboundUrl;
|
|
5917
|
-
urlCode.className = 'dep-inbound-url dep-inbound-url--inline';
|
|
5918
|
-
const copyBtn = document.createElement('button');
|
|
5919
|
-
copyBtn.type = 'button';
|
|
5920
|
-
copyBtn.className = 'hm-copy-btn';
|
|
5921
|
-
copyBtn.textContent = 'Copy';
|
|
5922
|
-
copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(dep.inboundUrl).catch(() => {}); });
|
|
5923
|
-
urlRow.appendChild(urlCode);
|
|
5924
|
-
urlRow.appendChild(copyBtn);
|
|
5925
|
-
card.appendChild(urlRow);
|
|
5926
|
-
}
|
|
5927
|
-
|
|
5928
|
-
const delBtn = document.createElement('button');
|
|
5929
|
-
delBtn.type = 'button';
|
|
5930
|
-
delBtn.className = 'dep-del-btn';
|
|
5931
|
-
delBtn.title = 'Remove this deployment';
|
|
5932
|
-
delBtn.textContent = 'Remove';
|
|
5933
|
-
delBtn.addEventListener('click', async () => {
|
|
5934
|
-
const endpoint = dep.type === 'scheduled' ? 'schedules' : 'webhooks';
|
|
5935
|
-
await fetch('/api/ai-hub/' + endpoint + '/' + dep.id, { method: 'DELETE' });
|
|
5936
|
-
await loadDeployments();
|
|
5937
|
-
});
|
|
5938
|
-
card.appendChild(delBtn);
|
|
5939
|
-
container.appendChild(card);
|
|
5940
|
-
}
|
|
5941
|
-
}
|
|
5942
|
-
|
|
5943
|
-
function initDeploymentButtons() {
|
|
5944
|
-
const addSchBtn = document.getElementById('dep-add-schedule-btn');
|
|
5945
|
-
const addWhBtn = document.getElementById('dep-add-webhook-btn');
|
|
5946
|
-
if (addSchBtn) addSchBtn.addEventListener('click', () => openDeploymentModal('schedule'));
|
|
5947
|
-
if (addWhBtn) addWhBtn.addEventListener('click', () => openDeploymentModal('webhook'));
|
|
5948
|
-
|
|
5949
|
-
// Close buttons
|
|
5950
|
-
const schClose = document.getElementById('dep-schedule-close');
|
|
5951
|
-
const whClose = document.getElementById('dep-webhook-close');
|
|
5952
|
-
if (schClose) schClose.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
|
|
5953
|
-
if (whClose) whClose.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
|
|
5954
|
-
|
|
5955
|
-
// Cancel buttons
|
|
5956
|
-
const schCancel = document.getElementById('dep-sch-cancel-btn');
|
|
5957
|
-
const whCancel = document.getElementById('dep-wh-cancel-btn');
|
|
5958
|
-
if (schCancel) schCancel.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
|
|
5959
|
-
if (whCancel) whCancel.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
|
|
5960
|
-
|
|
5961
|
-
// Save buttons
|
|
5962
|
-
const schSave = document.getElementById('dep-sch-save-btn');
|
|
5963
|
-
if (schSave) schSave.addEventListener('click', saveScheduleDeployment);
|
|
5964
|
-
const whSave = document.getElementById('dep-wh-save-btn');
|
|
5965
|
-
if (whSave) whSave.addEventListener('click', saveWebhookDeployment);
|
|
5966
|
-
|
|
5967
|
-
// Copy inbound URL button
|
|
5968
|
-
const copyBtn = document.getElementById('dep-wh-copy-btn');
|
|
5969
|
-
if (copyBtn) copyBtn.addEventListener('click', () => {
|
|
5970
|
-
const url = document.getElementById('dep-wh-inbound-url').textContent;
|
|
5971
|
-
navigator.clipboard.writeText(url).catch(() => {});
|
|
5972
|
-
});
|
|
5973
|
-
}
|
|
5974
|
-
|
|
5975
|
-
function openDeploymentModal(type) {
|
|
5976
|
-
const jobs = state.bootstrap?.jobs ?? [];
|
|
5977
|
-
if (type === 'schedule') {
|
|
5978
|
-
const modal = document.getElementById('dep-schedule-modal');
|
|
5979
|
-
const jobSel = document.getElementById('dep-sch-job');
|
|
5980
|
-
jobSel.innerHTML = '';
|
|
5981
|
-
for (const j of jobs) {
|
|
5982
|
-
const opt = document.createElement('option');
|
|
5983
|
-
opt.value = j.id;
|
|
5984
|
-
opt.textContent = j.title;
|
|
5985
|
-
jobSel.appendChild(opt);
|
|
5986
|
-
}
|
|
5987
|
-
document.getElementById('dep-sch-error').hidden = true;
|
|
5988
|
-
document.getElementById('dep-sch-label').value = '';
|
|
5989
|
-
document.getElementById('dep-sch-cron').value = '';
|
|
5990
|
-
document.getElementById('dep-sch-instructions').value = '';
|
|
5991
|
-
modal.hidden = false;
|
|
5992
|
-
} else {
|
|
5993
|
-
const modal = document.getElementById('dep-webhook-modal');
|
|
5994
|
-
const jobSel = document.getElementById('dep-wh-job');
|
|
5995
|
-
jobSel.innerHTML = '';
|
|
5996
|
-
for (const j of jobs) {
|
|
5997
|
-
const opt = document.createElement('option');
|
|
5998
|
-
opt.value = j.id;
|
|
5999
|
-
opt.textContent = j.title;
|
|
6000
|
-
jobSel.appendChild(opt);
|
|
6001
|
-
}
|
|
6002
|
-
document.getElementById('dep-wh-error').hidden = true;
|
|
6003
|
-
document.getElementById('dep-wh-inbound-row').hidden = true;
|
|
6004
|
-
document.getElementById('dep-wh-label').value = '';
|
|
6005
|
-
document.getElementById('dep-wh-instructions').value = '';
|
|
6006
|
-
modal.hidden = false;
|
|
6007
|
-
}
|
|
6008
|
-
}
|
|
6009
|
-
|
|
6010
|
-
async function saveScheduleDeployment() {
|
|
6011
|
-
const label = document.getElementById('dep-sch-label').value.trim();
|
|
6012
|
-
const jobId = document.getElementById('dep-sch-job').value;
|
|
6013
|
-
const cronExpr = document.getElementById('dep-sch-cron').value.trim();
|
|
6014
|
-
const instructions = document.getElementById('dep-sch-instructions').value.trim();
|
|
6015
|
-
const errEl = document.getElementById('dep-sch-error');
|
|
6016
|
-
if (!label || !cronExpr) { errEl.textContent = 'Label and cron expression are required.'; errEl.hidden = false; return; }
|
|
6017
|
-
try {
|
|
6018
|
-
const resp = await fetch('/api/ai-hub/schedules', {
|
|
6019
|
-
method: 'POST',
|
|
6020
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6021
|
-
body: JSON.stringify({ label, jobId, cronExpr, instructions: instructions || undefined }),
|
|
6022
|
-
});
|
|
6023
|
-
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to create schedule.'; errEl.hidden = false; return; }
|
|
6024
|
-
document.getElementById('dep-schedule-modal').hidden = true;
|
|
6025
|
-
await loadDeployments();
|
|
6026
|
-
} catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
|
|
6027
|
-
}
|
|
6028
|
-
|
|
6029
|
-
async function saveWebhookDeployment() {
|
|
6030
|
-
const label = document.getElementById('dep-wh-label').value.trim();
|
|
6031
|
-
const jobId = document.getElementById('dep-wh-job').value;
|
|
6032
|
-
const instructions = document.getElementById('dep-wh-instructions').value.trim();
|
|
6033
|
-
const errEl = document.getElementById('dep-wh-error');
|
|
6034
|
-
if (!label) { errEl.textContent = 'Label is required.'; errEl.hidden = false; return; }
|
|
6035
|
-
try {
|
|
6036
|
-
const resp = await fetch('/api/ai-hub/webhooks', {
|
|
6037
|
-
method: 'POST',
|
|
6038
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6039
|
-
body: JSON.stringify({ label, jobId, instructions: instructions || undefined }),
|
|
6040
|
-
});
|
|
6041
|
-
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to create webhook.'; errEl.hidden = false; return; }
|
|
6042
|
-
const dep = await resp.json();
|
|
6043
|
-
document.getElementById('dep-wh-inbound-url').textContent = dep.inboundUrl;
|
|
6044
|
-
document.getElementById('dep-wh-inbound-row').hidden = false;
|
|
6045
|
-
await loadDeployments();
|
|
6046
|
-
} catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
|
|
6047
|
-
}
|
|
6048
|
-
|
|
6049
5835
|
// ---------------------------------------------------------------------------
|
|
6050
5836
|
// Company / Manager / Brain content
|
|
6051
5837
|
// ---------------------------------------------------------------------------
|
|
@@ -8019,8 +7805,6 @@ function tfInitShell() {
|
|
|
8019
7805
|
tfRenderRail();
|
|
8020
7806
|
tfRenderProjectTabs();
|
|
8021
7807
|
tfShowArea('projects');
|
|
8022
|
-
// Issue #578: wire deployment modal buttons.
|
|
8023
|
-
initDeploymentButtons();
|
|
8024
7808
|
// Open the workspace by default so the conversation surface (#conversation,
|
|
8025
7809
|
// #new-conv-btn, #empty, the welcome line, the project button) is visible on
|
|
8026
7810
|
// load — preserving the contract the shipped #339/#347/#429/#442 Hub UI
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -1731,6 +1731,14 @@ button.small { padding: 4px 10px; font-size: 12px; }
|
|
|
1731
1731
|
keep the journey visible. */
|
|
1732
1732
|
@media (max-width: 640px) {
|
|
1733
1733
|
.tracker .stage .stage-label { display: none; }
|
|
1734
|
+
.tracker .stage { min-width: 24px; }
|
|
1735
|
+
.tracker .stage-circle {
|
|
1736
|
+
width: 20px;
|
|
1737
|
+
height: 20px;
|
|
1738
|
+
border-width: 1px;
|
|
1739
|
+
font-size: 10px;
|
|
1740
|
+
}
|
|
1741
|
+
.tracker .stage::before { top: 10px; }
|
|
1734
1742
|
.tracker-active-label { display: block; }
|
|
1735
1743
|
}
|
|
1736
1744
|
|
|
@@ -3985,39 +3993,3 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
3985
3993
|
.hs-btn--primary:hover { opacity:.85; }
|
|
3986
3994
|
.hs-btn--ghost { background:transparent; color:var(--muted); border:1px solid var(--line); }
|
|
3987
3995
|
.hs-btn--ghost:hover { color:var(--text); border-color:var(--text); }
|
|
3988
|
-
|
|
3989
|
-
/* ── Issue #578: Scheduled + Reactive Employees ── */
|
|
3990
|
-
|
|
3991
|
-
/* sourceTrigger chip on run rail items */
|
|
3992
|
-
.trigger-badge { display:inline-block; padding:1px 6px; border-radius:4px; font-size:10px; font-weight:600; letter-spacing:.02em; flex-shrink:0; }
|
|
3993
|
-
.trigger-badge--scheduled { background:rgba(0,122,255,.12); color:#0071e3; }
|
|
3994
|
-
.trigger-badge--webhook { background:rgba(88,86,214,.12); color:#5856d6; }
|
|
3995
|
-
|
|
3996
|
-
/* Deployment roster accordion */
|
|
3997
|
-
#proj-deployments-acc { margin-top:0; }
|
|
3998
|
-
.dep-actions { display:flex; gap:8px; margin-top:8px; }
|
|
3999
|
-
.dep-add-btn { padding:4px 12px; background:var(--accent-soft,rgba(0,113,227,.08)); color:var(--accent,#0071e3); border:1px solid rgba(0,113,227,.2); border-radius:8px; font-size:12px; font-weight:600; cursor:pointer; }
|
|
4000
|
-
.dep-add-btn:hover { background:var(--accent,#0071e3); color:#fff; }
|
|
4001
|
-
|
|
4002
|
-
/* Deployment cards */
|
|
4003
|
-
.dep-card { display:flex; flex-wrap:wrap; align-items:baseline; gap:6px; padding:8px 10px; background:var(--surface,#fff); border:1px solid var(--line,#e5e5e5); border-radius:10px; margin-bottom:6px; }
|
|
4004
|
-
.dep-card-top { display:flex; align-items:center; gap:6px; flex:1; min-width:0; }
|
|
4005
|
-
.dep-type-icon { font-size:14px; flex-shrink:0; }
|
|
4006
|
-
.dep-label { font-size:13px; font-weight:600; color:var(--text,#1d1d1f); flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
4007
|
-
.dep-type-badge { display:inline-block; padding:1px 7px; border-radius:4px; font-size:10px; font-weight:600; flex-shrink:0; }
|
|
4008
|
-
.dep-type-badge--scheduled { background:rgba(0,122,255,.12); color:#0071e3; }
|
|
4009
|
-
.dep-type-badge--webhook { background:rgba(88,86,214,.12); color:#5856d6; }
|
|
4010
|
-
.dep-detail { width:100%; font-size:11px; color:var(--muted,#888); margin-left:20px; }
|
|
4011
|
-
.dep-detail--url { display:flex; align-items:center; gap:6px; }
|
|
4012
|
-
.dep-inbound-url { font-size:11px; word-break:break-all; color:var(--muted,#888); flex:1; }
|
|
4013
|
-
.dep-inbound-url--inline { display:inline; font-size:11px; }
|
|
4014
|
-
.dep-del-btn { margin-left:auto; padding:2px 8px; background:transparent; border:1px solid var(--line,#e5e5e5); border-radius:6px; font-size:11px; color:var(--muted,#888); cursor:pointer; flex-shrink:0; }
|
|
4015
|
-
.dep-del-btn:hover { border-color:#ff3b30; color:#ff3b30; }
|
|
4016
|
-
.dep-empty { color:var(--muted,#888); font-size:12px; margin:4px 0 8px; line-height:1.5; }
|
|
4017
|
-
|
|
4018
|
-
/* Deployment modals */
|
|
4019
|
-
.dep-modal-actions { display:flex; gap:8px; margin-top:12px; }
|
|
4020
|
-
.dep-error { color:#ff3b30; font-size:12px; margin-top:4px; }
|
|
4021
|
-
.cancel-button { padding:7px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; background:transparent; color:var(--muted,#888); border:1px solid var(--line,#e5e5e5); }
|
|
4022
|
-
.cancel-button:hover { border-color:var(--text,#1d1d1f); color:var(--text,#1d1d1f); }
|
|
4023
|
-
.dep-inbound-url-row { display:flex; align-items:center; gap:8px; padding:6px 10px; background:var(--bg,#f5f5f7); border-radius:8px; margin-top:6px; }
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
Replaced wholesale by script.js once /api/first-run/session returns. -->
|
|
21
21
|
<li class="row skeleton-row" data-row-id="node" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Node.js</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
22
22
|
<li class="row skeleton-row" data-row-id="git" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">git</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
23
|
+
<li class="row skeleton-row" data-row-id="agent" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Locally installed AI agents</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
23
24
|
<li class="row skeleton-row" data-row-id="fraim" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">FRAIM</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
24
|
-
<li class="row skeleton-row" data-row-id="agent" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Execution surfaces</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
25
25
|
</ul>
|
|
26
26
|
<div class="actions">
|
|
27
27
|
<button id="primary-button" class="primary-button" data-testid="primary-button">Set up FRAIM</button>
|