fraim-framework 2.0.146 → 2.0.148
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/hosts.js +36 -18
- package/dist/src/ai-hub/server.js +50 -16
- package/index.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/index.html +31 -8
- package/public/ai-hub/script.js +256 -19
- package/public/ai-hub/styles.css +389 -92
- package/public/first-run/index.html +35 -34
- package/public/first-run/script.js +667 -655
- package/public/first-run/styles.css +46 -22
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
package/public/ai-hub/script.js
CHANGED
|
@@ -37,10 +37,10 @@ function gatherElements() {
|
|
|
37
37
|
'new-conv-btn', 'conv-list',
|
|
38
38
|
// Issue #385: team roster
|
|
39
39
|
'team-roster',
|
|
40
|
-
'empty', 'active-conv', 'active-title', 'active-job',
|
|
40
|
+
'empty', 'active-conv', 'active-title', 'active-job', 'active-identity', 'run-state-pill', 'summary-strip',
|
|
41
41
|
'progress', 'stage', 'latest', 'artifact-slot', 'messages',
|
|
42
42
|
'coach-text', 'send', 'micro-manage', 'micro-log',
|
|
43
|
-
'status-line',
|
|
43
|
+
'status-line', 'coach-note',
|
|
44
44
|
'modal', 'step1', 'step2',
|
|
45
45
|
'cancel1', 'next1', 'back2', 'start',
|
|
46
46
|
'job-search', 'job-catalog', 'job-pick-status',
|
|
@@ -312,6 +312,44 @@ function statusLabel(s) {
|
|
|
312
312
|
return 'Done';
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
function personaMap() {
|
|
316
|
+
const map = new Map();
|
|
317
|
+
for (const persona of state.bootstrap?.personas || []) {
|
|
318
|
+
map.set(persona.key, persona);
|
|
319
|
+
}
|
|
320
|
+
return map;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getConversationPersona(conv) {
|
|
324
|
+
if (!conv || !conv.personaKey) return null;
|
|
325
|
+
return personaMap().get(conv.personaKey) || null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getEmployeeStatus(employeeId) {
|
|
329
|
+
return (state.bootstrap?.employees || []).find((employee) => employee.id === employeeId) || null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getEmployeeTitle(conv) {
|
|
333
|
+
const persona = getConversationPersona(conv);
|
|
334
|
+
if (persona) return persona.role;
|
|
335
|
+
const employee = getEmployeeStatus(conv?.employeeId);
|
|
336
|
+
return employee ? employee.label : 'AI Employee';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function roleLabel(role, conv) {
|
|
340
|
+
if (role === 'manager') return 'Manager';
|
|
341
|
+
if (role === 'employee') {
|
|
342
|
+
const persona = getConversationPersona(conv);
|
|
343
|
+
return persona ? persona.displayName : 'Employee';
|
|
344
|
+
}
|
|
345
|
+
return 'System';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function initialBadge(text) {
|
|
349
|
+
const cleaned = String(text || '').replace(/[^A-Za-z]/g, '').toUpperCase();
|
|
350
|
+
return (cleaned.slice(0, 2) || 'FH');
|
|
351
|
+
}
|
|
352
|
+
|
|
315
353
|
// ---------------------------------------------------------------------------
|
|
316
354
|
// Issue #385 — Persona UI (R3 + R4)
|
|
317
355
|
// ---------------------------------------------------------------------------
|
|
@@ -343,9 +381,16 @@ function renderTeamRoster() {
|
|
|
343
381
|
roster.innerHTML = '';
|
|
344
382
|
const allChip = document.createElement('button');
|
|
345
383
|
allChip.type = 'button';
|
|
346
|
-
allChip.className = 'roster-chip' + (!state.selectedPersonaKey ? ' active' : '');
|
|
384
|
+
allChip.className = 'roster-chip roster-chip--all' + (!state.selectedPersonaKey ? ' active' : '');
|
|
347
385
|
allChip.title = 'All employees';
|
|
348
|
-
allChip.
|
|
386
|
+
allChip.setAttribute('aria-label', 'All employees');
|
|
387
|
+
allChip.innerHTML = `
|
|
388
|
+
<span class="roster-avatar">All</span>
|
|
389
|
+
<span class="roster-copy">
|
|
390
|
+
<strong>All employees</strong>
|
|
391
|
+
<small>Show every hired employee</small>
|
|
392
|
+
</span>
|
|
393
|
+
`;
|
|
349
394
|
allChip.addEventListener('click', () => setSelectedPersona(null));
|
|
350
395
|
roster.appendChild(allChip);
|
|
351
396
|
for (const persona of personas) {
|
|
@@ -354,14 +399,26 @@ function renderTeamRoster() {
|
|
|
354
399
|
chip.className = 'roster-chip' + (state.selectedPersonaKey === persona.key ? ' active' : '');
|
|
355
400
|
chip.title = persona.displayName;
|
|
356
401
|
chip.setAttribute('aria-label', persona.displayName);
|
|
402
|
+
const avatar = document.createElement('span');
|
|
403
|
+
avatar.className = 'roster-avatar';
|
|
357
404
|
if (persona.avatarUrl) {
|
|
358
405
|
const img = document.createElement('img');
|
|
359
406
|
img.src = persona.avatarUrl;
|
|
360
407
|
img.alt = persona.displayName;
|
|
361
|
-
|
|
408
|
+
avatar.appendChild(img);
|
|
362
409
|
} else {
|
|
363
|
-
|
|
410
|
+
avatar.textContent = persona.displayName.slice(0, 2).toUpperCase();
|
|
364
411
|
}
|
|
412
|
+
const copy = document.createElement('span');
|
|
413
|
+
copy.className = 'roster-copy';
|
|
414
|
+
const name = document.createElement('strong');
|
|
415
|
+
name.textContent = persona.displayName;
|
|
416
|
+
const role = document.createElement('small');
|
|
417
|
+
role.textContent = persona.role || 'AI Employee';
|
|
418
|
+
copy.appendChild(name);
|
|
419
|
+
copy.appendChild(role);
|
|
420
|
+
chip.appendChild(avatar);
|
|
421
|
+
chip.appendChild(copy);
|
|
365
422
|
chip.addEventListener('click', () => setSelectedPersona(persona.key));
|
|
366
423
|
roster.appendChild(chip);
|
|
367
424
|
}
|
|
@@ -487,6 +544,7 @@ let renderedConvId = null;
|
|
|
487
544
|
let renderedMessageCount = 0;
|
|
488
545
|
let renderedEventCount = 0;
|
|
489
546
|
let renderedArtifactKey = null;
|
|
547
|
+
let renderedStatus = null;
|
|
490
548
|
|
|
491
549
|
function renderActive() {
|
|
492
550
|
const conv = activeConversation();
|
|
@@ -497,12 +555,20 @@ function renderActive() {
|
|
|
497
555
|
renderedMessageCount = 0;
|
|
498
556
|
renderedEventCount = 0;
|
|
499
557
|
renderedArtifactKey = null;
|
|
558
|
+
renderedStatus = null;
|
|
500
559
|
return;
|
|
501
560
|
}
|
|
502
561
|
els['empty'].hidden = true;
|
|
503
562
|
els['active-conv'].hidden = false;
|
|
504
563
|
els['active-title'].textContent = conversationTitle(conv);
|
|
505
|
-
els['active-job'].textContent =
|
|
564
|
+
els['active-job'].textContent = `${conv.jobTitle} · ${friendlyProjectName(conv.projectPath || state.projectPath)} · ${getEmployeeStatus(conv.employeeId)?.label || conv.employeeId}`;
|
|
565
|
+
renderConversationIdentity(conv);
|
|
566
|
+
els['active-job'].textContent = `${conv.jobTitle} · ${friendlyProjectName(conv.projectPath || state.projectPath)}`;
|
|
567
|
+
renderRunStatePill(conv);
|
|
568
|
+
els['summary-strip'].textContent = buildConversationSummary(conv);
|
|
569
|
+
els['coach-note'].textContent = conv.status === 'running'
|
|
570
|
+
? 'The employee is still working. Add coaching here to tighten the next step without losing context.'
|
|
571
|
+
: 'The employee is waiting on you. Send the next instruction to continue this run.';
|
|
506
572
|
|
|
507
573
|
// Progress section. Plain text updates are cheap and don't animate.
|
|
508
574
|
const stage = derivedStage(conv);
|
|
@@ -523,6 +589,7 @@ function renderActive() {
|
|
|
523
589
|
renderedEventCount = 0;
|
|
524
590
|
renderedArtifactKey = null;
|
|
525
591
|
}
|
|
592
|
+
const statusChanged = renderedStatus !== conv.status;
|
|
526
593
|
|
|
527
594
|
// Artifact callout — only re-render when the latest artifact actually
|
|
528
595
|
// changed. Avoids the 'pulse' animation re-firing on every poll tick.
|
|
@@ -555,13 +622,18 @@ function renderActive() {
|
|
|
555
622
|
renderedMessageCount = 0;
|
|
556
623
|
}
|
|
557
624
|
for (let i = renderedMessageCount; i < messages.length; i += 1) {
|
|
558
|
-
appendMessageDom(messages[i].role, messages[i].text);
|
|
625
|
+
appendMessageDom(messages[i].role, messages[i].text, conv);
|
|
559
626
|
}
|
|
627
|
+
const appendedMessages = messages.length - renderedMessageCount;
|
|
560
628
|
renderedMessageCount = messages.length;
|
|
561
|
-
//
|
|
629
|
+
// Running threads stay pinned near the newest work. Once the employee
|
|
630
|
+
// is done, snap to the review point instead of leaving the manager at
|
|
631
|
+
// a stale scroll offset.
|
|
562
632
|
const m = els['messages'];
|
|
563
|
-
if (m.scrollHeight - m.scrollTop - m.clientHeight < 80) {
|
|
633
|
+
if (conv.status === 'running' && m.scrollHeight - m.scrollTop - m.clientHeight < 80) {
|
|
564
634
|
m.scrollTop = m.scrollHeight;
|
|
635
|
+
} else if (switchedConv || statusChanged || appendedMessages > 0) {
|
|
636
|
+
scrollThreadForReview(conv);
|
|
565
637
|
}
|
|
566
638
|
|
|
567
639
|
// Micro-manage — only append new events. textContent assignment on the
|
|
@@ -592,6 +664,53 @@ function renderActive() {
|
|
|
592
664
|
syncTemplatePickerVisibility();
|
|
593
665
|
// Browser-tab title mirrors the active conversation (R3).
|
|
594
666
|
document.title = conv.title ? conv.title : 'AI Hub';
|
|
667
|
+
renderedStatus = conv.status;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function renderConversationIdentity(conv) {
|
|
671
|
+
const host = els['active-identity'];
|
|
672
|
+
if (!host) return;
|
|
673
|
+
const persona = getConversationPersona(conv);
|
|
674
|
+
const employee = getEmployeeStatus(conv.employeeId);
|
|
675
|
+
host.innerHTML = '';
|
|
676
|
+
|
|
677
|
+
const avatar = document.createElement(persona && persona.avatarUrl ? 'img' : 'span');
|
|
678
|
+
avatar.className = 'identity-avatar';
|
|
679
|
+
if (persona && persona.avatarUrl) {
|
|
680
|
+
avatar.src = persona.avatarUrl;
|
|
681
|
+
avatar.alt = persona.displayName;
|
|
682
|
+
} else {
|
|
683
|
+
avatar.textContent = initialBadge(employee?.label || 'Hub');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const text = document.createElement('span');
|
|
687
|
+
text.className = 'identity-copy';
|
|
688
|
+
|
|
689
|
+
const name = document.createElement('strong');
|
|
690
|
+
name.textContent = persona ? persona.displayName : (employee ? employee.label : 'AI Employee');
|
|
691
|
+
|
|
692
|
+
const title = document.createElement('small');
|
|
693
|
+
title.textContent = getEmployeeTitle(conv);
|
|
694
|
+
|
|
695
|
+
text.appendChild(name);
|
|
696
|
+
text.appendChild(title);
|
|
697
|
+
host.appendChild(avatar);
|
|
698
|
+
host.appendChild(text);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function renderRunStatePill(conv) {
|
|
702
|
+
const pill = els['run-state-pill'];
|
|
703
|
+
if (!pill) return;
|
|
704
|
+
pill.textContent = statusLabel(conv.status).toUpperCase();
|
|
705
|
+
pill.className = `run-state-pill ${conv.status}`;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function buildConversationSummary(conv) {
|
|
709
|
+
const employeeReply = latestEmployeeSurfaceText(conv);
|
|
710
|
+
if (employeeReply) return employeeReply;
|
|
711
|
+
if (conv.status === 'running') return 'The employee is working through your request.';
|
|
712
|
+
if (conv.status === 'failed') return 'This run needs your attention before it can continue.';
|
|
713
|
+
return 'The latest work is ready for review.';
|
|
595
714
|
}
|
|
596
715
|
|
|
597
716
|
// Render the inline employee selector shown in the coach section of an
|
|
@@ -929,23 +1048,141 @@ function derivedStage(conv) {
|
|
|
929
1048
|
}
|
|
930
1049
|
|
|
931
1050
|
function derivedLatest(conv) {
|
|
1051
|
+
if (conv.status !== 'running') return '';
|
|
1052
|
+
const employeeReply = latestEmployeeSurfaceText(conv);
|
|
1053
|
+
if (employeeReply) return employeeReply;
|
|
1054
|
+
if (conv.status === 'running') return 'Working on it…';
|
|
1055
|
+
return '';
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function humanizeSlug(slug) {
|
|
1059
|
+
return String(slug || '')
|
|
1060
|
+
.split('-')
|
|
1061
|
+
.filter(Boolean)
|
|
1062
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1063
|
+
.join(' ');
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function stripStubReference(text) {
|
|
1067
|
+
return String(text || '')
|
|
1068
|
+
.replace(/\n?\[Job stub:[^\]]+\]/gi, '')
|
|
1069
|
+
.replace(/\s+/g, ' ')
|
|
1070
|
+
.trim();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function surfaceText(role, text, conv) {
|
|
1074
|
+
const raw = stripStubReference(text);
|
|
1075
|
+
if (!raw) return '';
|
|
1076
|
+
|
|
1077
|
+
if (role === 'manager') {
|
|
1078
|
+
const invocationOnly = raw.match(/^(?:[$/]fraim)\s+([a-z0-9-]+)\s*$/i);
|
|
1079
|
+
if (invocationOnly) return `Run ${humanizeSlug(invocationOnly[1])}.`;
|
|
1080
|
+
const slugPattern = '[a-z0-9]+(?:-[a-z0-9]+)+';
|
|
1081
|
+
const invocationWithSlug = new RegExp(`^(?:[$/]fraim)\\s+(${slugPattern})\\s*`, 'i');
|
|
1082
|
+
return raw
|
|
1083
|
+
.replace(invocationWithSlug, '')
|
|
1084
|
+
.replace(/^(?:[$/]fraim)\s*/i, '')
|
|
1085
|
+
.trim();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (role === 'employee') {
|
|
1089
|
+
const startedMatch = raw.match(/^Started\s+\w+:\s*(.*)$/i);
|
|
1090
|
+
if (startedMatch) {
|
|
1091
|
+
const cleaned = surfaceText('manager', startedMatch[1], conv);
|
|
1092
|
+
if (conv.status === 'completed') return 'Done — please review.';
|
|
1093
|
+
return cleaned ? `Working on: ${cleaned}` : 'Working on it…';
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return raw;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function latestEmployeeSurfaceText(conv) {
|
|
932
1101
|
const messages = conv.messages || [];
|
|
933
1102
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
934
|
-
if (messages[i].role
|
|
1103
|
+
if (messages[i].role !== 'employee') continue;
|
|
1104
|
+
const cleaned = surfaceText('employee', messages[i].text, conv);
|
|
1105
|
+
if (cleaned) return cleaned;
|
|
935
1106
|
}
|
|
936
|
-
if (conv.status === 'running') return 'Loading the work and getting started.';
|
|
937
1107
|
return '';
|
|
938
1108
|
}
|
|
939
1109
|
|
|
940
|
-
function appendMessageDom(role, text) {
|
|
941
|
-
const
|
|
942
|
-
|
|
1110
|
+
function appendMessageDom(role, text, conv) {
|
|
1111
|
+
const article = document.createElement('article');
|
|
1112
|
+
article.className = 'message ' + role;
|
|
1113
|
+
|
|
1114
|
+
const meta = document.createElement('div');
|
|
1115
|
+
meta.className = 'message-meta';
|
|
1116
|
+
|
|
1117
|
+
const avatar = document.createElement('span');
|
|
1118
|
+
avatar.className = `message-avatar ${role}`;
|
|
1119
|
+
|
|
1120
|
+
if (role === 'employee') {
|
|
1121
|
+
const persona = getConversationPersona(conv);
|
|
1122
|
+
if (persona && persona.avatarUrl) {
|
|
1123
|
+
const img = document.createElement('img');
|
|
1124
|
+
img.src = persona.avatarUrl;
|
|
1125
|
+
img.alt = persona.displayName;
|
|
1126
|
+
avatar.appendChild(img);
|
|
1127
|
+
} else {
|
|
1128
|
+
avatar.textContent = initialBadge(roleLabel(role, conv));
|
|
1129
|
+
}
|
|
1130
|
+
} else if (role === 'manager') {
|
|
1131
|
+
avatar.textContent = 'M';
|
|
1132
|
+
} else {
|
|
1133
|
+
avatar.textContent = '•';
|
|
1134
|
+
}
|
|
1135
|
+
|
|
943
1136
|
const who = document.createElement('span');
|
|
944
1137
|
who.className = 'who';
|
|
945
|
-
who.textContent =
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1138
|
+
who.textContent = roleLabel(role, conv);
|
|
1139
|
+
|
|
1140
|
+
const lane = document.createElement('span');
|
|
1141
|
+
lane.className = 'lane-label';
|
|
1142
|
+
lane.textContent = role === 'manager'
|
|
1143
|
+
? 'Manager direction'
|
|
1144
|
+
: role === 'employee'
|
|
1145
|
+
? 'Employee response'
|
|
1146
|
+
: 'System update';
|
|
1147
|
+
|
|
1148
|
+
meta.appendChild(avatar);
|
|
1149
|
+
meta.appendChild(who);
|
|
1150
|
+
meta.appendChild(lane);
|
|
1151
|
+
article.appendChild(meta);
|
|
1152
|
+
|
|
1153
|
+
const bubble = document.createElement('div');
|
|
1154
|
+
bubble.className = 'bubble';
|
|
1155
|
+
bubble.textContent = surfaceText(role, text, conv) || (
|
|
1156
|
+
role === 'employee'
|
|
1157
|
+
? (conv.status === 'completed' ? 'Done — please review.' : 'Working on it…')
|
|
1158
|
+
: text
|
|
1159
|
+
);
|
|
1160
|
+
if (role === 'manager') {
|
|
1161
|
+
const raw = document.createElement('span');
|
|
1162
|
+
raw.className = 'transport-raw';
|
|
1163
|
+
raw.textContent = text;
|
|
1164
|
+
article.appendChild(raw);
|
|
1165
|
+
}
|
|
1166
|
+
article.appendChild(bubble);
|
|
1167
|
+
els['messages'].appendChild(article);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function scrollThreadForReview(conv) {
|
|
1171
|
+
const host = els['messages'];
|
|
1172
|
+
if (!host) return;
|
|
1173
|
+
const nodes = [...host.querySelectorAll('.message')];
|
|
1174
|
+
if (nodes.length === 0) return;
|
|
1175
|
+
|
|
1176
|
+
if (conv.status === 'running') {
|
|
1177
|
+
host.scrollTop = host.scrollHeight;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const target = [...nodes].reverse().find((node) =>
|
|
1182
|
+
node.classList.contains('employee') || node.classList.contains('system')
|
|
1183
|
+
) || nodes[nodes.length - 1];
|
|
1184
|
+
|
|
1185
|
+
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
949
1186
|
}
|
|
950
1187
|
|
|
951
1188
|
function syncSendButton() {
|