fraim-framework 2.0.170 → 2.0.173
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 +227 -6
- package/dist/src/ai-hub/server.js +1014 -35
- package/dist/src/cli/commands/add-ide.js +4 -2
- package/dist/src/cli/commands/cleanup-artifacts.js +38 -0
- package/dist/src/cli/commands/init-project.js +12 -5
- package/dist/src/cli/commands/setup.js +1 -1
- package/dist/src/cli/commands/sync.js +74 -7
- package/dist/src/cli/doctor/checks/ide-config-checks.js +2 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +10 -2
- package/dist/src/cli/setup/auto-mcp-setup.js +4 -2
- package/dist/src/cli/setup/ide-detector.js +26 -0
- package/dist/src/cli/setup/ide-global-integration.js +6 -2
- package/dist/src/cli/setup/ide-invocation-surfaces.js +12 -4
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +42 -17
- package/dist/src/cli/utils/fraim-gitignore.js +13 -0
- package/dist/src/cli/utils/remote-sync.js +129 -53
- package/dist/src/cli/utils/user-config.js +12 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/compat.js +16 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +273 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/fraim-config-schema.generated.js +8 -2
- package/dist/src/core/utils/local-registry-resolver.js +26 -0
- package/dist/src/core/utils/project-fraim-paths.js +89 -2
- package/dist/src/first-run/session-service.js +12 -3
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +255 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
- package/dist/src/local-mcp-server/stdio-server.js +42 -7
- package/package.json +5 -1
- package/public/ai-hub/index.html +205 -89
- package/public/ai-hub/review.css +12 -0
- package/public/ai-hub/script.js +1734 -253
- package/public/ai-hub/styles.css +473 -6
package/public/ai-hub/script.js
CHANGED
|
@@ -15,6 +15,8 @@ const TREE_WIDTH_MIN = 176;
|
|
|
15
15
|
const TREE_WIDTH_MAX = 380;
|
|
16
16
|
const TREE_WIDTH_DEFAULT = 216;
|
|
17
17
|
const PAGE_SCOPED_JOBS = new Set(['organization-onboarding', 'manager-agreements', 'project-onboarding', 'organizational-learning-synthesis']);
|
|
18
|
+
// Jobs scoped to the Company/Manager area — never shown in the Projects workspace rail.
|
|
19
|
+
const AREA_SCOPED_JOBS = new Set(['organization-onboarding', 'organizational-learning-synthesis', 'manager-agreements']);
|
|
18
20
|
|
|
19
21
|
const state = {
|
|
20
22
|
bootstrap: null,
|
|
@@ -36,6 +38,7 @@ const state = {
|
|
|
36
38
|
pendingCoachingJobId: null,
|
|
37
39
|
pendingCoachingLabel: null,
|
|
38
40
|
conversationPersistTimer: null,
|
|
41
|
+
conversationRefreshHandle: null,
|
|
39
42
|
conversationDiskAvailable: true,
|
|
40
43
|
// Issue #539: command palette state
|
|
41
44
|
cpSelectedJob: null, // {id, title, intent, requiredPersonaKey}
|
|
@@ -424,16 +427,92 @@ async function hydrateConversationsFromServer() {
|
|
|
424
427
|
for (const conv of conversations) normalizeGeminiConversationMessages(conv);
|
|
425
428
|
state.conversations[state.projectPath] = conversations;
|
|
426
429
|
const activeCandidates = [state.activeId, payload.activeId].filter(Boolean);
|
|
427
|
-
|
|
430
|
+
// Area-scoped jobs must not become the Projects workspace active conversation —
|
|
431
|
+
// they are shown in their own Company/Manager panels via tfActiveOrgConv/tfActiveMgrConv.
|
|
432
|
+
const projectConv = (id) => conversations.find((c) => c.id === id && !AREA_SCOPED_JOBS.has(c.jobId));
|
|
433
|
+
const firstProjectConv = conversations.find((c) => !AREA_SCOPED_JOBS.has(c.jobId));
|
|
434
|
+
const bestActive = activeCandidates.map(projectConv).find(Boolean) || firstProjectConv || null;
|
|
435
|
+
state.activeId = bestActive ? bestActive.id : null;
|
|
428
436
|
state.conversationDiskAvailable = true;
|
|
429
437
|
renderRail();
|
|
430
438
|
renderActive();
|
|
439
|
+
syncConversationRefreshPolling();
|
|
431
440
|
} catch (error) {
|
|
432
441
|
state.conversationDiskAvailable = false;
|
|
433
442
|
console.warn('Could not hydrate conversations from server:', error);
|
|
434
443
|
}
|
|
435
444
|
}
|
|
436
445
|
|
|
446
|
+
// Background poll: pick up conversations created by scheduled/webhook runs
|
|
447
|
+
// without requiring the user to reload. Merges new/updated conversations into
|
|
448
|
+
// the existing list, preserving the active selection.
|
|
449
|
+
async function bgRefreshConversations() {
|
|
450
|
+
if (!state.projectPath) return;
|
|
451
|
+
try {
|
|
452
|
+
const payload = await requestJson(`/api/ai-hub/conversations?projectPath=${encodeURIComponent(state.projectPath)}`);
|
|
453
|
+
const incoming = Array.isArray(payload.conversations) ? payload.conversations : [];
|
|
454
|
+
if (!incoming.length) return;
|
|
455
|
+
const existing = state.conversations[state.projectPath] || [];
|
|
456
|
+
const existingIds = new Set(existing.map((c) => c.id));
|
|
457
|
+
let changed = false;
|
|
458
|
+
for (const conv of incoming) {
|
|
459
|
+
normalizeGeminiConversationMessages(conv);
|
|
460
|
+
if (!existingIds.has(conv.id)) {
|
|
461
|
+
existing.push(conv);
|
|
462
|
+
changed = true;
|
|
463
|
+
} else {
|
|
464
|
+
// Update status/messages on known conversations (e.g. running → completed)
|
|
465
|
+
const idx = existing.findIndex((c) => c.id === conv.id);
|
|
466
|
+
if (idx !== -1 && existing[idx].status !== conv.status) {
|
|
467
|
+
existing[idx] = conv;
|
|
468
|
+
changed = true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (changed) {
|
|
473
|
+
state.conversations[state.projectPath] = existing;
|
|
474
|
+
renderRail();
|
|
475
|
+
}
|
|
476
|
+
} catch (_) { /* silent — background poll, not critical */ }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let _bgConvPollHandle = null;
|
|
480
|
+
function startBgConvPoll() {
|
|
481
|
+
if (_bgConvPollHandle) return;
|
|
482
|
+
_bgConvPollHandle = window.setInterval(bgRefreshConversations, 30000);
|
|
483
|
+
}
|
|
484
|
+
function stopBgConvPoll() {
|
|
485
|
+
if (_bgConvPollHandle) { window.clearInterval(_bgConvPollHandle); _bgConvPollHandle = null; }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function needsConversationRefresh() {
|
|
489
|
+
const active = activeConversation();
|
|
490
|
+
if (!active) return false;
|
|
491
|
+
if (isManagedDelegationChild(active) && active.status === 'running') return true;
|
|
492
|
+
const ledger = normalizeDelegationLedger(active.delegation);
|
|
493
|
+
if (!ledger) return false;
|
|
494
|
+
return ledger.tasks.some((task) => {
|
|
495
|
+
const child = delegationTaskConversation(task, ledger);
|
|
496
|
+
if (!child) return true;
|
|
497
|
+
if (child.status === 'running') return true;
|
|
498
|
+
if (child.status === 'completed' && child.managedReviewStatus !== 'reviewed') return true;
|
|
499
|
+
return false;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function syncConversationRefreshPolling() {
|
|
504
|
+
const shouldPoll = needsConversationRefresh();
|
|
505
|
+
if (shouldPoll && !state.conversationRefreshHandle) {
|
|
506
|
+
state.conversationRefreshHandle = window.setInterval(() => {
|
|
507
|
+
hydrateConversationsFromServer().catch((error) =>
|
|
508
|
+
console.warn('Could not refresh delegated conversations:', error));
|
|
509
|
+
}, 1500);
|
|
510
|
+
} else if (!shouldPoll && state.conversationRefreshHandle) {
|
|
511
|
+
window.clearInterval(state.conversationRefreshHandle);
|
|
512
|
+
state.conversationRefreshHandle = null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
437
516
|
function conversationTimestamp(conv) {
|
|
438
517
|
const value = conv && conv.lastUpdatedAt;
|
|
439
518
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
@@ -476,6 +555,7 @@ function panelStateFor(convId) {
|
|
|
476
555
|
}
|
|
477
556
|
|
|
478
557
|
function defaultCoachOpen(conv) {
|
|
558
|
+
if (isManagerOversightConversation(conv)) return false;
|
|
479
559
|
return true;
|
|
480
560
|
}
|
|
481
561
|
|
|
@@ -535,6 +615,7 @@ function syncQuickCoachButtons(conv) {
|
|
|
535
615
|
function stripReviewHandoffBlocks(text) {
|
|
536
616
|
return String(text || '')
|
|
537
617
|
.replace(/<review_handoff>\s*[\s\S]*?\s*<\/review_handoff>/gi, '')
|
|
618
|
+
.replace(/\[Thought:\s*true\]\s*/gi, '')
|
|
538
619
|
.replace(/\n{3,}/g, '\n\n')
|
|
539
620
|
.trim();
|
|
540
621
|
}
|
|
@@ -707,11 +788,22 @@ function renderRail() {
|
|
|
707
788
|
// #521: in the project workspace, project-lifecycle jobs (onboarding +
|
|
708
789
|
// sleep-on-learnings) have their own "Project Updates" home, so keep them out
|
|
709
790
|
// of the general Runs list to avoid showing the same run twice.
|
|
710
|
-
const inWorkspace = !!document.querySelector('.workspace-conv');
|
|
791
|
+
const inWorkspace = !!document.querySelector('#proj-workspace .workspace-conv');
|
|
711
792
|
const projectUpdateJobs = new Set(['project-onboarding', 'sleep-on-learnings']);
|
|
712
|
-
const
|
|
793
|
+
const allProjectConversations = projectConversations();
|
|
794
|
+
const managedChildren = allProjectConversations.filter(isManagedDelegationChild);
|
|
795
|
+
const childrenByManager = new Map();
|
|
796
|
+
for (const child of managedChildren) {
|
|
797
|
+
const parentId = child.managedByRunId;
|
|
798
|
+
if (!parentId) continue;
|
|
799
|
+
if (!childrenByManager.has(parentId)) childrenByManager.set(parentId, []);
|
|
800
|
+
childrenByManager.get(parentId).push(child);
|
|
801
|
+
}
|
|
802
|
+
const list = allProjectConversations.filter((conv) =>
|
|
713
803
|
(!state.selectedPersonaKey || conv.personaKey === state.selectedPersonaKey) &&
|
|
714
|
-
!(
|
|
804
|
+
!isManagedDelegationChild(conv) &&
|
|
805
|
+
!(inWorkspace && projectUpdateJobs.has(conv.jobId)) &&
|
|
806
|
+
!AREA_SCOPED_JOBS.has(conv.jobId)
|
|
715
807
|
);
|
|
716
808
|
|
|
717
809
|
// Issue #550: Two-path routing for ad-hoc (freeform) conversations.
|
|
@@ -740,50 +832,80 @@ function renderRail() {
|
|
|
740
832
|
groups.get(key).conversations.push(conv);
|
|
741
833
|
}
|
|
742
834
|
|
|
835
|
+
function buildRunButton(conv, options = {}) {
|
|
836
|
+
const btn = document.createElement('button');
|
|
837
|
+
btn.type = 'button';
|
|
838
|
+
btn.className = (options.child ? 'conv-item conv-item--delegated-child' : 'conv-item') + (state.activeId === conv.id ? ' active' : '');
|
|
839
|
+
btn.dataset.conv = conv.id;
|
|
840
|
+
const bodyDiv = document.createElement('span');
|
|
841
|
+
bodyDiv.className = 'conv-body';
|
|
842
|
+
const titleSpan = document.createElement('span');
|
|
843
|
+
titleSpan.className = 'conv-title';
|
|
844
|
+
titleSpan.textContent = conv.title || '';
|
|
845
|
+
bodyDiv.appendChild(titleSpan);
|
|
846
|
+
btn.appendChild(bodyDiv);
|
|
847
|
+
// Issue #566 R7: mark runs of a personalized (taught/customized) job.
|
|
848
|
+
if (isTaughtJob(conv.jobId)) {
|
|
849
|
+
const taught = document.createElement('span');
|
|
850
|
+
taught.className = 'taught-badge';
|
|
851
|
+
taught.textContent = 'Personalized';
|
|
852
|
+
taught.title = 'Personalized for this project (taught or customized in your fraim/personalized-employee layer)';
|
|
853
|
+
btn.appendChild(taught);
|
|
854
|
+
}
|
|
855
|
+
const dotClass = conversationStateDotClass(conv);
|
|
856
|
+
const statusDot = document.createElement('span');
|
|
857
|
+
statusDot.className = 'state-dot conv-state-dot dot-' + dotClass;
|
|
858
|
+
statusDot.title = tfDotTitle(dotClass);
|
|
859
|
+
btn.appendChild(statusDot);
|
|
860
|
+
const statusSpan = document.createElement('span');
|
|
861
|
+
statusSpan.className = 'conv-status';
|
|
862
|
+
statusSpan.textContent = conversationStateLabel(conv);
|
|
863
|
+
statusSpan.classList.add(conversationUiState(conv));
|
|
864
|
+
btn.appendChild(statusSpan);
|
|
865
|
+
// Issue #578: sourceTrigger chip - show scheduled/webhook badge on automated runs.
|
|
866
|
+
if (conv.sourceTrigger && conv.sourceTrigger !== 'manager') {
|
|
867
|
+
const trigBadge = document.createElement('span');
|
|
868
|
+
trigBadge.className = 'trigger-badge trigger-badge--' + conv.sourceTrigger;
|
|
869
|
+
trigBadge.textContent = conv.sourceTrigger === 'scheduled' ? 'Scheduled' : 'Webhook';
|
|
870
|
+
trigBadge.title = conv.sourceTrigger === 'scheduled'
|
|
871
|
+
? 'This run was started by the built-in cron scheduler'
|
|
872
|
+
: 'This run was triggered by an inbound webhook';
|
|
873
|
+
btn.appendChild(trigBadge);
|
|
874
|
+
}
|
|
875
|
+
if (conv.compareMode === 'ab') {
|
|
876
|
+
const badge = document.createElement('span');
|
|
877
|
+
badge.className = 'ab-badge';
|
|
878
|
+
badge.textContent = 'A/B';
|
|
879
|
+
btn.appendChild(badge);
|
|
880
|
+
}
|
|
881
|
+
if (!options.child) tfAttachRunDelete(btn, conv, { tree: false }); // #533 R6: delete a run
|
|
882
|
+
btn.addEventListener('click', () => switchToConversation(conv.id));
|
|
883
|
+
return btn;
|
|
884
|
+
}
|
|
885
|
+
|
|
743
886
|
// Helper: build the run-item buttons shared by employee groups and Watercooler.
|
|
744
887
|
function buildGroupList(conversations) {
|
|
745
888
|
const groupList = document.createElement('div');
|
|
746
889
|
groupList.className = 'conv-employee-list';
|
|
747
890
|
for (const conv of conversations) {
|
|
748
|
-
const btn =
|
|
749
|
-
btn.type = 'button';
|
|
750
|
-
btn.className = 'conv-item' + (state.activeId === conv.id ? ' active' : '');
|
|
751
|
-
btn.dataset.conv = conv.id;
|
|
752
|
-
const bodyDiv = document.createElement('span');
|
|
753
|
-
bodyDiv.className = 'conv-body';
|
|
754
|
-
const titleSpan = document.createElement('span');
|
|
755
|
-
titleSpan.className = 'conv-title';
|
|
756
|
-
titleSpan.textContent = conv.title || '';
|
|
757
|
-
bodyDiv.appendChild(titleSpan);
|
|
758
|
-
btn.appendChild(bodyDiv);
|
|
759
|
-
// Issue #566 R7: mark runs of a personalized (taught/customized) job.
|
|
760
|
-
if (isTaughtJob(conv.jobId)) {
|
|
761
|
-
const taught = document.createElement('span');
|
|
762
|
-
taught.className = 'taught-badge';
|
|
763
|
-
taught.textContent = 'Personalized';
|
|
764
|
-
taught.title = 'Personalized for this project (taught or customized in your fraim/personalized-employee layer)';
|
|
765
|
-
btn.appendChild(taught);
|
|
766
|
-
}
|
|
767
|
-
const dotClass = conversationStateDotClass(conv);
|
|
768
|
-
const statusDot = document.createElement('span');
|
|
769
|
-
statusDot.className = 'state-dot conv-state-dot dot-' + dotClass;
|
|
770
|
-
statusDot.title = tfDotTitle(dotClass);
|
|
771
|
-
btn.appendChild(statusDot);
|
|
772
|
-
const statusSpan = document.createElement('span');
|
|
773
|
-
statusSpan.className = 'conv-status';
|
|
774
|
-
statusSpan.textContent = conversationStateLabel(conv);
|
|
775
|
-
statusSpan.classList.add(conversationUiState(conv));
|
|
776
|
-
btn.appendChild(statusSpan);
|
|
777
|
-
// Issue #442: A/B badge on rail entry.
|
|
778
|
-
if (conv.compareMode === 'ab') {
|
|
779
|
-
const badge = document.createElement('span');
|
|
780
|
-
badge.className = 'ab-badge';
|
|
781
|
-
badge.textContent = 'A/B';
|
|
782
|
-
btn.appendChild(badge);
|
|
783
|
-
}
|
|
784
|
-
tfAttachRunDelete(btn, conv, { tree: false }); // #533 R6: delete a run
|
|
785
|
-
btn.addEventListener('click', () => switchToConversation(conv.id));
|
|
891
|
+
const btn = buildRunButton(conv);
|
|
786
892
|
groupList.appendChild(btn);
|
|
893
|
+
const children = [
|
|
894
|
+
...(childrenByManager.get(conv.id) || []),
|
|
895
|
+
...(conv.runId && conv.runId !== conv.id ? (childrenByManager.get(conv.runId) || []) : []),
|
|
896
|
+
];
|
|
897
|
+
if (children.length) {
|
|
898
|
+
const delegatedList = document.createElement('div');
|
|
899
|
+
delegatedList.className = 'conv-delegated-list';
|
|
900
|
+
const label = document.createElement('div');
|
|
901
|
+
label.className = 'conv-delegated-label';
|
|
902
|
+
label.textContent = 'Mandy-managed workstreams';
|
|
903
|
+
delegatedList.appendChild(label);
|
|
904
|
+
for (const child of children) {
|
|
905
|
+
delegatedList.appendChild(buildRunButton(child, { child: true }));
|
|
906
|
+
}
|
|
907
|
+
groupList.appendChild(delegatedList);
|
|
908
|
+
}
|
|
787
909
|
}
|
|
788
910
|
return groupList;
|
|
789
911
|
}
|
|
@@ -887,8 +1009,18 @@ function conversationUiState(conv) {
|
|
|
887
1009
|
// not stay stuck as stopped even if the stopped flag was not cleared.
|
|
888
1010
|
if (conv.stopped && conv.status === 'failed') return 'stopped';
|
|
889
1011
|
if (conv.status === 'failed') return 'waiting';
|
|
1012
|
+
if (isManagedDelegationChild(conv) && conv.status === 'completed' && conv.managedReviewStatus === 'reviewed') return 'complete';
|
|
890
1013
|
if (conv.status === 'completed' && conv.reviewApproved) return 'complete';
|
|
891
|
-
if (conv.status === 'completed')
|
|
1014
|
+
if (conv.status === 'completed') {
|
|
1015
|
+
// For fully-delegate runs: if child workstreams are still active, the manager
|
|
1016
|
+
// is waiting for them — show 'working' not 'waiting' so the user isn't prompted
|
|
1017
|
+
// to act when the Hub will auto-route child deliverables back to Mandy.
|
|
1018
|
+
if (isManagerOversightConversation(conv)) {
|
|
1019
|
+
const hasActiveChildren = projectConversations().some(c => c.managedByRunId === conv.runId && c.status === 'running');
|
|
1020
|
+
if (hasActiveChildren) return 'working';
|
|
1021
|
+
}
|
|
1022
|
+
return 'waiting';
|
|
1023
|
+
}
|
|
892
1024
|
return 'idle';
|
|
893
1025
|
}
|
|
894
1026
|
|
|
@@ -903,6 +1035,12 @@ function conversationStateDotClass(conv) {
|
|
|
903
1035
|
}
|
|
904
1036
|
|
|
905
1037
|
function conversationStateLabel(conv) {
|
|
1038
|
+
if (isManagedDelegationChild(conv)) {
|
|
1039
|
+
if (conv.status === 'running') return 'Working for Mandy';
|
|
1040
|
+
if (conv.status === 'completed' && conv.managedReviewStatus === 'reviewed') return 'Reviewed by Mandy';
|
|
1041
|
+
if (conv.status === 'completed') return 'Submitted to Mandy';
|
|
1042
|
+
if (conv.status === 'failed') return 'Needs Mandy';
|
|
1043
|
+
}
|
|
906
1044
|
const uiState = conversationUiState(conv);
|
|
907
1045
|
if (uiState === 'working') return 'Working';
|
|
908
1046
|
if (uiState === 'waiting') return 'Waiting on you';
|
|
@@ -912,6 +1050,17 @@ function conversationStateLabel(conv) {
|
|
|
912
1050
|
return 'Idle';
|
|
913
1051
|
}
|
|
914
1052
|
|
|
1053
|
+
function isManagedDelegationChild(conv) {
|
|
1054
|
+
return !!(conv && (conv.managedByRunId || conv.delegationTaskId || conv.humanCoachingDisabled));
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function managedByPersonaLabel(conv) {
|
|
1058
|
+
const key = conv && (conv.managedByPersonaKey || 'mandy');
|
|
1059
|
+
if (key === 'mandy') return 'Mandy';
|
|
1060
|
+
const persona = key ? personaMap().get(key) : null;
|
|
1061
|
+
return persona ? persona.displayName : 'Mandy';
|
|
1062
|
+
}
|
|
1063
|
+
|
|
915
1064
|
function personaMap() {
|
|
916
1065
|
const map = new Map();
|
|
917
1066
|
for (const persona of state.bootstrap?.personas || []) {
|
|
@@ -1183,7 +1332,7 @@ function showHireStrip(job, conversationId, instructions, employeeId) {
|
|
|
1183
1332
|
els['hire-strip'].hidden = false;
|
|
1184
1333
|
// Switch workspace to conv mode so the hire strip (inside .page) is visible
|
|
1185
1334
|
// even when there is no active conversation (which normally keeps it in mode-brief).
|
|
1186
|
-
const wc = document.querySelector('.workspace-conv');
|
|
1335
|
+
const wc = document.querySelector('#proj-workspace .workspace-conv');
|
|
1187
1336
|
if (wc) { wc.classList.add('mode-conv'); wc.classList.remove('mode-brief'); }
|
|
1188
1337
|
}
|
|
1189
1338
|
|
|
@@ -1289,7 +1438,7 @@ let threadMessageViewportObserver = null;
|
|
|
1289
1438
|
// coach, and micro-manage all have room. Toggle the mode class the CSS keys off,
|
|
1290
1439
|
// and reflect the state in the sidebar's Brief nav entry.
|
|
1291
1440
|
function tfApplyWorkspaceMode() {
|
|
1292
|
-
const wc = document.querySelector('.workspace-conv');
|
|
1441
|
+
const wc = document.querySelector('#proj-workspace .workspace-conv');
|
|
1293
1442
|
if (!wc) return;
|
|
1294
1443
|
const hasConv = typeof activeConversation === 'function' && !!activeConversation();
|
|
1295
1444
|
wc.classList.toggle('mode-conv', hasConv);
|
|
@@ -1317,12 +1466,15 @@ function renderActive() {
|
|
|
1317
1466
|
renderedEventCount = 0;
|
|
1318
1467
|
renderedStatus = null;
|
|
1319
1468
|
renderedDirectEventCount = 0;
|
|
1469
|
+
syncConversationRefreshPolling();
|
|
1320
1470
|
return;
|
|
1321
1471
|
}
|
|
1322
1472
|
els['empty'].hidden = true;
|
|
1323
1473
|
els['active-conv'].hidden = false;
|
|
1324
1474
|
els['active-conv'].dataset.runId = conv.runId || '';
|
|
1325
1475
|
els['active-title'].textContent = conversationTitle(conv);
|
|
1476
|
+
els['active-conv'].classList.toggle('has-delegation-board', !!normalizeDelegationLedger(conv.delegation));
|
|
1477
|
+
els['active-conv'].classList.toggle('has-manager-oversight', isManagerOversightConversation(conv));
|
|
1326
1478
|
renderConversationIdentity(conv);
|
|
1327
1479
|
renderRunStatePill(conv);
|
|
1328
1480
|
syncCoachEmployeeLabel(conv);
|
|
@@ -1348,6 +1500,7 @@ function renderActive() {
|
|
|
1348
1500
|
syncThreadUiState(conv);
|
|
1349
1501
|
syncConversationPanels(conv, switchedConv);
|
|
1350
1502
|
syncQuickCoachButtons(conv);
|
|
1503
|
+
syncManagedDelegationAccess(conv);
|
|
1351
1504
|
ensureThreadMessageViewportObserver();
|
|
1352
1505
|
|
|
1353
1506
|
// Messages — append only the rows that aren't already in the DOM, so
|
|
@@ -1431,6 +1584,7 @@ function renderActive() {
|
|
|
1431
1584
|
// Issue #566 R4 — after the manager confirms success, offer to teach this as a
|
|
1432
1585
|
// job (ad-hoc) or remember the coached steps for the job (structured).
|
|
1433
1586
|
renderGrowthOffer(conv);
|
|
1587
|
+
syncManagedDelegationAccess(conv);
|
|
1434
1588
|
syncThreadMessageViewport();
|
|
1435
1589
|
const m = els['messages'];
|
|
1436
1590
|
const runningShouldStickToBottom = !!m && conv.status === 'running'
|
|
@@ -1456,6 +1610,9 @@ function renderActive() {
|
|
|
1456
1610
|
// simply hide the surfaces.
|
|
1457
1611
|
renderTracker(conv);
|
|
1458
1612
|
renderTotals(conv);
|
|
1613
|
+
renderManagerOversightBoard(conv);
|
|
1614
|
+
renderDelegationLedger(conv);
|
|
1615
|
+
syncConversationRefreshPolling();
|
|
1459
1616
|
syncTemplatePickerVisibility();
|
|
1460
1617
|
// Issue #442: Direct panel status pill, simple progress indicator, and totals.
|
|
1461
1618
|
if (isABConv) {
|
|
@@ -1903,6 +2060,371 @@ function renderTotals(conv) {
|
|
|
1903
2060
|
pushTotalsSpan(totals, costLabel, '', "cost: derived from token totals and the agent's published per-million rate");
|
|
1904
2061
|
}
|
|
1905
2062
|
|
|
2063
|
+
function ensureDelegationHost() {
|
|
2064
|
+
const active = els['active-conv'];
|
|
2065
|
+
if (!active) return null;
|
|
2066
|
+
let host = document.getElementById('delegation-ledger');
|
|
2067
|
+
if (host) return host;
|
|
2068
|
+
host = document.createElement('details');
|
|
2069
|
+
host.id = 'delegation-ledger';
|
|
2070
|
+
host.className = 'delegation-ledger panel-details';
|
|
2071
|
+
host.addEventListener('toggle', () => {
|
|
2072
|
+
const conv = activeConversation();
|
|
2073
|
+
if (!conv) return;
|
|
2074
|
+
panelStateFor(conv.id).delegation = host.open;
|
|
2075
|
+
});
|
|
2076
|
+
const status = active.querySelector('.conversation-status');
|
|
2077
|
+
const oversight = document.getElementById('manager-oversight-board');
|
|
2078
|
+
if (oversight && oversight.parentNode) {
|
|
2079
|
+
oversight.parentNode.insertBefore(host, oversight.nextSibling);
|
|
2080
|
+
} else if (status && status.parentNode) {
|
|
2081
|
+
status.parentNode.insertBefore(host, status.nextSibling);
|
|
2082
|
+
} else {
|
|
2083
|
+
active.insertBefore(host, active.firstChild);
|
|
2084
|
+
}
|
|
2085
|
+
return host;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
const MANAGER_OVERSIGHT_JOB_IDS = new Set([
|
|
2089
|
+
'fully-delegate',
|
|
2090
|
+
'delivery-governance-review',
|
|
2091
|
+
'project-plan-creation',
|
|
2092
|
+
'stakeholder-status-reporting',
|
|
2093
|
+
'experiment-tracking',
|
|
2094
|
+
'cross-functional-dependency-management',
|
|
2095
|
+
]);
|
|
2096
|
+
|
|
2097
|
+
function isManagerOversightConversation(conv) {
|
|
2098
|
+
return !!conv && (conv.personaKey === 'mandy' || MANAGER_OVERSIGHT_JOB_IDS.has(conv.jobId) || !!delegationLedgerForConversation(conv));
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function ensureManagerOversightHost() {
|
|
2102
|
+
const active = els['active-conv'];
|
|
2103
|
+
if (!active) return null;
|
|
2104
|
+
let host = document.getElementById('manager-oversight-board');
|
|
2105
|
+
if (host) return host;
|
|
2106
|
+
host = document.createElement('details');
|
|
2107
|
+
host.id = 'manager-oversight-board';
|
|
2108
|
+
host.className = 'manager-oversight-board panel-details';
|
|
2109
|
+
host.addEventListener('toggle', () => {
|
|
2110
|
+
const conv = activeConversation();
|
|
2111
|
+
if (!conv) return;
|
|
2112
|
+
panelStateFor(conv.id).managerOversight = host.open;
|
|
2113
|
+
});
|
|
2114
|
+
const status = active.querySelector('.conversation-status');
|
|
2115
|
+
if (status && status.parentNode) {
|
|
2116
|
+
status.parentNode.insertBefore(host, status.nextSibling);
|
|
2117
|
+
} else {
|
|
2118
|
+
active.insertBefore(host, active.firstChild);
|
|
2119
|
+
}
|
|
2120
|
+
return host;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function renderManagerOversightBoard(conv) {
|
|
2124
|
+
const host = ensureManagerOversightHost();
|
|
2125
|
+
if (!host) return;
|
|
2126
|
+
if (!isManagerOversightConversation(conv)) {
|
|
2127
|
+
host.hidden = true;
|
|
2128
|
+
host.innerHTML = '';
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const ledger = delegationLedgerForConversation(conv);
|
|
2132
|
+
const panelState = panelStateFor(conv.id);
|
|
2133
|
+
const shouldOpen = panelState.managerOversight ?? true;
|
|
2134
|
+
if (host.open !== shouldOpen) host.open = shouldOpen;
|
|
2135
|
+
const childCount = ledger?.tasks?.length || 0;
|
|
2136
|
+
const completedChildren = ledger?.tasks?.filter((task) => {
|
|
2137
|
+
const child = delegationTaskConversation(task, ledger);
|
|
2138
|
+
return child?.status === 'completed' || task.status === 'submitted' || task.status === 'reviewed' || task.status === 'completed';
|
|
2139
|
+
}).length || 0;
|
|
2140
|
+
const reviewedChildren = ledger?.tasks?.filter((task) => {
|
|
2141
|
+
const child = delegationTaskConversation(task, ledger);
|
|
2142
|
+
return child?.managedReviewStatus === 'reviewed' || task.status === 'reviewed';
|
|
2143
|
+
}).length || 0;
|
|
2144
|
+
const artifactCount = Array.isArray(conv.artifacts) ? conv.artifacts.length : 0;
|
|
2145
|
+
const reviewReady = !!reviewHandoffForConversation(conv) || artifactCount > 0;
|
|
2146
|
+
const planRows = ledger?.tasks?.map((task, index) => managerPlanRow(task, ledger, index)) || [];
|
|
2147
|
+
|
|
2148
|
+
host.hidden = false;
|
|
2149
|
+
host.innerHTML = '';
|
|
2150
|
+
const summary = document.createElement('summary');
|
|
2151
|
+
const header = document.createElement('div');
|
|
2152
|
+
header.className = 'manager-oversight-header';
|
|
2153
|
+
const copy = document.createElement('div');
|
|
2154
|
+
copy.className = 'manager-oversight-copy';
|
|
2155
|
+
const kicker = document.createElement('span');
|
|
2156
|
+
kicker.className = 'manager-oversight-kicker';
|
|
2157
|
+
kicker.textContent = 'Execution plan';
|
|
2158
|
+
const title = document.createElement('strong');
|
|
2159
|
+
title.textContent = childCount
|
|
2160
|
+
? `${childCount} delegated job${childCount === 1 ? '' : 's'} with Mandy review gates`
|
|
2161
|
+
: conv.status === 'running' ? 'Mandy is planning the delegation' : `${conv.jobTitle || conv.title || 'Manager job'} — no delegation emitted`;
|
|
2162
|
+
copy.appendChild(kicker);
|
|
2163
|
+
copy.appendChild(title);
|
|
2164
|
+
const meta = document.createElement('span');
|
|
2165
|
+
meta.className = 'manager-oversight-meta';
|
|
2166
|
+
meta.textContent = childCount
|
|
2167
|
+
? `${completedChildren}/${childCount} submitted, ${reviewedChildren}/${childCount} reviewed`
|
|
2168
|
+
: conv.status === 'running' ? 'setting up team…' : reviewReady ? `${artifactCount} artifact${artifactCount === 1 ? '' : 's'} surfaced, no delegation graph` : 'delegation graph not returned';
|
|
2169
|
+
header.appendChild(copy);
|
|
2170
|
+
header.appendChild(meta);
|
|
2171
|
+
summary.appendChild(header);
|
|
2172
|
+
host.appendChild(summary);
|
|
2173
|
+
|
|
2174
|
+
const list = document.createElement('div');
|
|
2175
|
+
list.className = 'manager-oversight-list';
|
|
2176
|
+
if (planRows.length) {
|
|
2177
|
+
for (const item of planRows) {
|
|
2178
|
+
const row = document.createElement('div');
|
|
2179
|
+
row.className = `manager-plan-row status-${item.status}`;
|
|
2180
|
+
const number = document.createElement('span');
|
|
2181
|
+
number.className = 'manager-plan-step';
|
|
2182
|
+
number.textContent = String(item.index + 1);
|
|
2183
|
+
const body = document.createElement('div');
|
|
2184
|
+
body.className = 'manager-plan-body';
|
|
2185
|
+
const top = document.createElement('div');
|
|
2186
|
+
top.className = 'manager-plan-top';
|
|
2187
|
+
const titleEl = document.createElement('strong');
|
|
2188
|
+
titleEl.textContent = item.title;
|
|
2189
|
+
const status = document.createElement('span');
|
|
2190
|
+
status.className = `manager-plan-status status-${item.status}`;
|
|
2191
|
+
status.textContent = item.statusLabel;
|
|
2192
|
+
top.appendChild(titleEl);
|
|
2193
|
+
top.appendChild(status);
|
|
2194
|
+
const metaLine = document.createElement('div');
|
|
2195
|
+
metaLine.className = 'manager-plan-meta';
|
|
2196
|
+
metaLine.textContent = `${item.employee} • ${item.dependencyText}`;
|
|
2197
|
+
const review = document.createElement('div');
|
|
2198
|
+
review.className = 'manager-plan-review';
|
|
2199
|
+
review.textContent = item.reviewText;
|
|
2200
|
+
body.appendChild(top);
|
|
2201
|
+
body.appendChild(metaLine);
|
|
2202
|
+
body.appendChild(review);
|
|
2203
|
+
if (item.instructions) {
|
|
2204
|
+
const instructions = document.createElement('div');
|
|
2205
|
+
instructions.className = 'manager-plan-instructions';
|
|
2206
|
+
instructions.textContent = item.instructions;
|
|
2207
|
+
body.appendChild(instructions);
|
|
2208
|
+
}
|
|
2209
|
+
row.appendChild(number);
|
|
2210
|
+
row.appendChild(body);
|
|
2211
|
+
list.appendChild(row);
|
|
2212
|
+
}
|
|
2213
|
+
} else {
|
|
2214
|
+
const empty = document.createElement('div');
|
|
2215
|
+
empty.className = 'manager-plan-empty';
|
|
2216
|
+
const isRunning = conv.status === 'running';
|
|
2217
|
+
const titleEl = document.createElement('strong');
|
|
2218
|
+
titleEl.textContent = isRunning ? 'Delegation plan incoming…' : 'No delegation sequence returned';
|
|
2219
|
+
const detail = document.createElement('span');
|
|
2220
|
+
detail.textContent = isRunning
|
|
2221
|
+
? 'Mandy is working through the task and will lay out the employee assignments and review gates here once the plan is ready.'
|
|
2222
|
+
: reviewReady
|
|
2223
|
+
? `This run completed with ${artifactCount} surfaced artifact${artifactCount === 1 ? '' : 's'} but did not emit a delegation sequence. Start a new run to retry.`
|
|
2224
|
+
: 'This run finished without returning a delegation sequence. Start a new run to retry.';
|
|
2225
|
+
empty.appendChild(titleEl);
|
|
2226
|
+
empty.appendChild(detail);
|
|
2227
|
+
list.appendChild(empty);
|
|
2228
|
+
}
|
|
2229
|
+
host.appendChild(list);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function managerPlanRow(task, ledger, index) {
|
|
2233
|
+
const child = delegationTaskConversation(task, ledger);
|
|
2234
|
+
const status = delegatedTaskStatus(task, child);
|
|
2235
|
+
const dependsOn = Array.isArray(task.dependsOn) ? task.dependsOn.filter(Boolean) : [];
|
|
2236
|
+
const dependencyText = dependsOn.length
|
|
2237
|
+
? `starts after ${dependsOn.join(', ')}`
|
|
2238
|
+
: 'can start immediately';
|
|
2239
|
+
const reviewJob = task.reviewJobId ? ` via ${task.reviewJobId}` : '';
|
|
2240
|
+
const reviewText = task.reviewType || task.reviewHandoff?.reviewRequired || child?.reviewHandoff?.reviewRequired
|
|
2241
|
+
? `Mandy validates this output${reviewJob} before synthesis`
|
|
2242
|
+
: 'Mandy validates this output before synthesis';
|
|
2243
|
+
return {
|
|
2244
|
+
index,
|
|
2245
|
+
title: task.title || task.jobId || task.taskId || `Delegated job ${index + 1}`,
|
|
2246
|
+
employee: delegationPersonaLabel(task.personaKey),
|
|
2247
|
+
dependencyText,
|
|
2248
|
+
reviewText,
|
|
2249
|
+
instructions: task.instructions || task.latestSummary || '',
|
|
2250
|
+
status,
|
|
2251
|
+
statusLabel: statusLabel(status),
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function delegationPersonaLabel(personaKey) {
|
|
2256
|
+
if (!personaKey) return 'Unassigned';
|
|
2257
|
+
const persona = personaMap().get(personaKey);
|
|
2258
|
+
return persona ? persona.displayName : personaKey;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
function delegationTaskConversation(task, ledger) {
|
|
2262
|
+
if (!task || !ledger) return null;
|
|
2263
|
+
const conversations = projectConversations();
|
|
2264
|
+
const ids = new Set([task.conversationId, task.runId].filter(Boolean));
|
|
2265
|
+
const managerIds = new Set([ledger.managerRunId, ledger.rootRunId].filter(Boolean));
|
|
2266
|
+
return conversations.find((conv) =>
|
|
2267
|
+
ids.has(conv.id) ||
|
|
2268
|
+
ids.has(conv.runId) ||
|
|
2269
|
+
(conv.delegationTaskId === task.taskId && (!conv.managedByRunId || managerIds.has(conv.managedByRunId)))
|
|
2270
|
+
) || null;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
function delegatedTaskStatus(task, childConv) {
|
|
2274
|
+
if (!childConv) return task.status;
|
|
2275
|
+
if (childConv.status === 'running') return 'running';
|
|
2276
|
+
if (childConv.status === 'failed') return 'blocked';
|
|
2277
|
+
if (childConv.status === 'completed' && childConv.managedReviewStatus === 'reviewed') return 'reviewed';
|
|
2278
|
+
if (childConv.status === 'completed') return 'submitted';
|
|
2279
|
+
return task.status || 'planned';
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function delegatedTaskSummary(task, childConv) {
|
|
2283
|
+
if (childConv) {
|
|
2284
|
+
const latest = latestEmployeeSurfaceText(childConv);
|
|
2285
|
+
if (latest) return latest;
|
|
2286
|
+
}
|
|
2287
|
+
return task.latestSummary || task.instructions || 'Waiting for an update.';
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
function delegatedTaskArtifacts(task, childConv) {
|
|
2291
|
+
if (childConv?.reviewHandoff?.artifacts?.length) return childConv.reviewHandoff.artifacts;
|
|
2292
|
+
if (childConv?.artifacts?.length) return childConv.artifacts;
|
|
2293
|
+
if (task.reviewHandoff?.artifacts?.length) return task.reviewHandoff.artifacts;
|
|
2294
|
+
return task.artifacts || [];
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
function renderDelegationLedger(conv) {
|
|
2298
|
+
const host = ensureDelegationHost();
|
|
2299
|
+
if (!host) return;
|
|
2300
|
+
const ledger = delegationLedgerForConversation(conv);
|
|
2301
|
+
if (!ledger) {
|
|
2302
|
+
host.hidden = true;
|
|
2303
|
+
host.innerHTML = '';
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
host.hidden = false;
|
|
2308
|
+
const panelState = panelStateFor(conv.id);
|
|
2309
|
+
const shouldOpen = panelState.delegation ?? !isManagerOversightConversation(conv);
|
|
2310
|
+
if (host.open !== shouldOpen) host.open = shouldOpen;
|
|
2311
|
+
host.innerHTML = '';
|
|
2312
|
+
const summaryRow = document.createElement('summary');
|
|
2313
|
+
const header = document.createElement('div');
|
|
2314
|
+
header.className = 'delegation-ledger-header';
|
|
2315
|
+
const copy = document.createElement('div');
|
|
2316
|
+
copy.className = 'delegation-ledger-copy';
|
|
2317
|
+
const kicker = document.createElement('span');
|
|
2318
|
+
kicker.className = 'delegation-ledger-kicker';
|
|
2319
|
+
kicker.textContent = 'Delegation board';
|
|
2320
|
+
const title = document.createElement('strong');
|
|
2321
|
+
title.textContent = ledger.objective;
|
|
2322
|
+
copy.appendChild(kicker);
|
|
2323
|
+
copy.appendChild(title);
|
|
2324
|
+
const meta = document.createElement('span');
|
|
2325
|
+
meta.className = 'delegation-ledger-meta';
|
|
2326
|
+
const orchestrator = delegationPersonaLabel(ledger.orchestratorPersonaKey || conv.personaKey);
|
|
2327
|
+
meta.textContent = `${orchestrator} coordinating ${ledger.tasks.length} workstream${ledger.tasks.length === 1 ? '' : 's'}`;
|
|
2328
|
+
header.appendChild(copy);
|
|
2329
|
+
header.appendChild(meta);
|
|
2330
|
+
summaryRow.appendChild(header);
|
|
2331
|
+
host.appendChild(summaryRow);
|
|
2332
|
+
|
|
2333
|
+
const body = document.createElement('div');
|
|
2334
|
+
body.className = 'delegation-ledger-body';
|
|
2335
|
+
|
|
2336
|
+
if (ledger.latestSummary) {
|
|
2337
|
+
const summary = document.createElement('p');
|
|
2338
|
+
summary.className = 'delegation-ledger-summary';
|
|
2339
|
+
summary.textContent = ledger.latestSummary;
|
|
2340
|
+
body.appendChild(summary);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
const list = document.createElement('div');
|
|
2344
|
+
list.className = 'delegation-task-list';
|
|
2345
|
+
for (const task of ledger.tasks) {
|
|
2346
|
+
const childConv = delegationTaskConversation(task, ledger);
|
|
2347
|
+
const taskStatus = delegatedTaskStatus(task, childConv);
|
|
2348
|
+
const row = document.createElement('article');
|
|
2349
|
+
row.className = `delegation-task delegation-task--${taskStatus}`;
|
|
2350
|
+
const top = document.createElement('div');
|
|
2351
|
+
top.className = 'delegation-task-top';
|
|
2352
|
+
const assignee = document.createElement('span');
|
|
2353
|
+
assignee.className = 'delegation-task-assignee';
|
|
2354
|
+
assignee.textContent = delegationPersonaLabel(task.personaKey);
|
|
2355
|
+
const status = document.createElement('span');
|
|
2356
|
+
status.className = `delegation-task-status status-${taskStatus}`;
|
|
2357
|
+
status.textContent = childConv ? conversationStateLabel(childConv) : taskStatus;
|
|
2358
|
+
top.appendChild(assignee);
|
|
2359
|
+
top.appendChild(status);
|
|
2360
|
+
|
|
2361
|
+
const taskTitle = document.createElement('strong');
|
|
2362
|
+
taskTitle.className = 'delegation-task-title';
|
|
2363
|
+
taskTitle.textContent = task.title;
|
|
2364
|
+
const detail = document.createElement('div');
|
|
2365
|
+
detail.className = 'delegation-task-detail';
|
|
2366
|
+
detail.textContent = [
|
|
2367
|
+
childConv?.jobId || task.jobId,
|
|
2368
|
+
childConv?.sessionId || task.hostSessionId ? `session ${childConv?.sessionId || task.hostSessionId}` : '',
|
|
2369
|
+
task.hostThreadId ? `thread ${task.hostThreadId}` : '',
|
|
2370
|
+
].filter(Boolean).join(' · ');
|
|
2371
|
+
const summary = document.createElement('p');
|
|
2372
|
+
summary.className = 'delegation-task-summary';
|
|
2373
|
+
summary.textContent = delegatedTaskSummary(task, childConv);
|
|
2374
|
+
|
|
2375
|
+
row.appendChild(top);
|
|
2376
|
+
row.appendChild(taskTitle);
|
|
2377
|
+
if (detail.textContent) row.appendChild(detail);
|
|
2378
|
+
row.appendChild(summary);
|
|
2379
|
+
|
|
2380
|
+
const artifacts = delegatedTaskArtifacts(task, childConv);
|
|
2381
|
+
if (artifacts.length) {
|
|
2382
|
+
const artifactStrip = document.createElement('div');
|
|
2383
|
+
artifactStrip.className = 'delegation-artifacts';
|
|
2384
|
+
for (const artifact of artifacts) {
|
|
2385
|
+
const chip = document.createElement('button');
|
|
2386
|
+
chip.type = 'button';
|
|
2387
|
+
chip.className = 'delegation-artifact';
|
|
2388
|
+
chip.textContent = artifactLabel(artifact);
|
|
2389
|
+
chip.title = artifactLocalPath(artifact) || artifactExternalUrl(artifact) || artifactLabel(artifact);
|
|
2390
|
+
chip.addEventListener('click', async () => {
|
|
2391
|
+
try {
|
|
2392
|
+
await openReviewArtifact(artifact);
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
showStatus(err instanceof Error ? err.message : 'Could not open artifact.', true);
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
artifactStrip.appendChild(chip);
|
|
2398
|
+
}
|
|
2399
|
+
row.appendChild(artifactStrip);
|
|
2400
|
+
} else if (childConv?.status === 'completed' || taskStatus === 'submitted' || taskStatus === 'reviewed') {
|
|
2401
|
+
const inline = document.createElement('div');
|
|
2402
|
+
inline.className = 'delegation-review-note';
|
|
2403
|
+
inline.textContent = 'Inline deliverable; no file artifact reported';
|
|
2404
|
+
row.appendChild(inline);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
if (childConv) {
|
|
2408
|
+
const view = document.createElement('button');
|
|
2409
|
+
view.type = 'button';
|
|
2410
|
+
view.className = 'delegation-view-work';
|
|
2411
|
+
view.textContent = 'View work';
|
|
2412
|
+
view.addEventListener('click', () => switchToConversation(childConv.id));
|
|
2413
|
+
row.appendChild(view);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
if (task.reviewHandoff?.reviewRequired || childConv?.reviewHandoff?.reviewRequired) {
|
|
2417
|
+
const review = document.createElement('div');
|
|
2418
|
+
review.className = 'delegation-review-note';
|
|
2419
|
+
review.textContent = 'Ready for Mandy review';
|
|
2420
|
+
row.appendChild(review);
|
|
2421
|
+
}
|
|
2422
|
+
list.appendChild(row);
|
|
2423
|
+
}
|
|
2424
|
+
body.appendChild(list);
|
|
2425
|
+
host.appendChild(body);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
1906
2428
|
// Issue #442: render Direct totals row using the same pushTotalsSpan helper as FRAIM.
|
|
1907
2429
|
function renderDirectTotals(conv) {
|
|
1908
2430
|
const dtotals = els['ab-direct-totals'];
|
|
@@ -2098,6 +2620,7 @@ function humanizeSlug(slug) {
|
|
|
2098
2620
|
function stripStubReference(text) {
|
|
2099
2621
|
return String(text || '')
|
|
2100
2622
|
.replace(/\n?\[Job stub:[^\]]+\]/gi, '')
|
|
2623
|
+
.replace(/\[Thought:\s*true\]\s*/gi, '')
|
|
2101
2624
|
.replace(/\s+/g, ' ')
|
|
2102
2625
|
.trim();
|
|
2103
2626
|
}
|
|
@@ -2278,6 +2801,10 @@ function scrollThreadForReview(conv) {
|
|
|
2278
2801
|
function syncSendButton() {
|
|
2279
2802
|
const conv = activeConversation();
|
|
2280
2803
|
const hasText = els['coach-text'].value.trim().length > 0;
|
|
2804
|
+
if (isManagedDelegationChild(conv)) {
|
|
2805
|
+
els['send'].disabled = true;
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2281
2808
|
// Send is enabled as soon as the host session exists. We deliberately
|
|
2282
2809
|
// do NOT gate on status='running' — the manager should be able to
|
|
2283
2810
|
// coach mid-run (which is exactly what /api/ai-hub/runs/:id/messages
|
|
@@ -2291,6 +2818,9 @@ function syncSendButton() {
|
|
|
2291
2818
|
}
|
|
2292
2819
|
|
|
2293
2820
|
function defaultCoachNote(conv) {
|
|
2821
|
+
if (isManagedDelegationChild(conv)) {
|
|
2822
|
+
return `${managedByPersonaLabel(conv)} manages coaching and review for this delegated workstream. You can inspect the work, but coach the manager job instead.`;
|
|
2823
|
+
}
|
|
2294
2824
|
return conv && conv.status === 'running'
|
|
2295
2825
|
? 'The employee is still working. Add coaching here to tighten the next step without losing context.'
|
|
2296
2826
|
: 'The employee is waiting on you. Send the next instruction to continue this run.';
|
|
@@ -2308,6 +2838,34 @@ function syncCoachNote(conv = activeConversation()) {
|
|
|
2308
2838
|
els['coach-note'].textContent = defaultCoachNote(conv);
|
|
2309
2839
|
}
|
|
2310
2840
|
|
|
2841
|
+
function syncManagedDelegationAccess(conv = activeConversation()) {
|
|
2842
|
+
const managed = isManagedDelegationChild(conv);
|
|
2843
|
+
const textarea = els['coach-text'];
|
|
2844
|
+
if (textarea) {
|
|
2845
|
+
textarea.disabled = managed;
|
|
2846
|
+
textarea.placeholder = managed
|
|
2847
|
+
? `${managedByPersonaLabel(conv)} owns coaching for this delegated workstream`
|
|
2848
|
+
: 'Tell the employee what to do next...';
|
|
2849
|
+
}
|
|
2850
|
+
if (els['send']) els['send'].disabled = managed || els['send'].disabled;
|
|
2851
|
+
if (els['coach-panel']) els['coach-panel'].classList.toggle('coach-panel--managed-readonly', managed);
|
|
2852
|
+
for (const selector of ['#quick-coach-btns button', '#mark-complete-btn', '#other-manager-jobs-btn', '#review-actions button']) {
|
|
2853
|
+
for (const button of document.querySelectorAll(selector)) {
|
|
2854
|
+
if (managed) {
|
|
2855
|
+
if (button.dataset.managedPrevDisabled === undefined) {
|
|
2856
|
+
button.dataset.managedPrevDisabled = button.disabled ? '1' : '0';
|
|
2857
|
+
}
|
|
2858
|
+
button.disabled = true;
|
|
2859
|
+
} else if (button.dataset.managedPrevDisabled !== undefined) {
|
|
2860
|
+
button.disabled = button.dataset.managedPrevDisabled === '1';
|
|
2861
|
+
delete button.dataset.managedPrevDisabled;
|
|
2862
|
+
}
|
|
2863
|
+
button.classList.toggle('managed-disabled', managed);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
if (managed) closeTemplatePopover();
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2311
2869
|
function appendEditableCoachingPrompt(promptText) {
|
|
2312
2870
|
const textarea = els['coach-text'];
|
|
2313
2871
|
if (!textarea) return;
|
|
@@ -2366,6 +2924,11 @@ function convAwaitingReview(conv) {
|
|
|
2366
2924
|
const handoff = reviewHandoffForConversation(conv);
|
|
2367
2925
|
if (handoff) return handoff.reviewRequired === true;
|
|
2368
2926
|
if (reviewHandoffIssueForConversation(conv)) return true;
|
|
2927
|
+
// Delegation orchestration runs (fully-delegate with an active ledger) only enter review
|
|
2928
|
+
// via an explicit review_handoff — Mandy's delegation artifacts are orchestration working
|
|
2929
|
+
// output, not deliverables submitted to the human manager. Other manager jobs (e.g.,
|
|
2930
|
+
// stakeholder-status-reporting) may legitimately surface artifacts for human review.
|
|
2931
|
+
if (delegationLedgerForConversation(conv)) return false;
|
|
2369
2932
|
if (conv.status === 'completed' && Array.isArray(conv.artifacts) && conv.artifacts.length > 0) return true;
|
|
2370
2933
|
// #521: a plain completed turn is NOT awaiting review — the approve/reject card
|
|
2371
2934
|
// only belongs when the employee actually submits for review (a review_handoff,
|
|
@@ -2475,6 +3038,98 @@ function extractReviewHandoffFromText(text) {
|
|
|
2475
3038
|
return null;
|
|
2476
3039
|
}
|
|
2477
3040
|
|
|
3041
|
+
const DELEGATION_TASK_STATUSES = new Set(['planned', 'running', 'submitted', 'reviewed', 'blocked', 'completed', 'failed']);
|
|
3042
|
+
|
|
3043
|
+
function cleanString(value) {
|
|
3044
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
function cleanNullableString(value) {
|
|
3048
|
+
const cleaned = cleanString(value);
|
|
3049
|
+
return cleaned ? cleaned : null;
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
function normalizeDelegationLedger(raw) {
|
|
3053
|
+
if (!raw || typeof raw !== 'object' || raw.delegationRequired !== true) return null;
|
|
3054
|
+
const objective = cleanString(raw.objective);
|
|
3055
|
+
if (!objective) return null;
|
|
3056
|
+
const tasks = Array.isArray(raw.tasks)
|
|
3057
|
+
? raw.tasks.map((task, index) => {
|
|
3058
|
+
if (!task || typeof task !== 'object') return null;
|
|
3059
|
+
const title = cleanString(task.title);
|
|
3060
|
+
if (!title) return null;
|
|
3061
|
+
const fallbackTaskId = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || `task-${index + 1}`;
|
|
3062
|
+
const status = DELEGATION_TASK_STATUSES.has(cleanString(task.status)) ? cleanString(task.status) : 'planned';
|
|
3063
|
+
const artifacts = Array.isArray(task.artifacts)
|
|
3064
|
+
? task.artifacts.map(normalizeReviewArtifact).filter(Boolean)
|
|
3065
|
+
: [];
|
|
3066
|
+
return {
|
|
3067
|
+
taskId: cleanString(task.taskId) || fallbackTaskId,
|
|
3068
|
+
title,
|
|
3069
|
+
status,
|
|
3070
|
+
personaKey: cleanNullableString(task.personaKey),
|
|
3071
|
+
jobId: cleanNullableString(task.jobId),
|
|
3072
|
+
instructions: cleanString(task.instructions),
|
|
3073
|
+
latestSummary: cleanString(task.latestSummary),
|
|
3074
|
+
hostThreadId: cleanNullableString(task.hostThreadId),
|
|
3075
|
+
hostSessionId: cleanNullableString(task.hostSessionId),
|
|
3076
|
+
runId: cleanNullableString(task.runId),
|
|
3077
|
+
conversationId: cleanNullableString(task.conversationId),
|
|
3078
|
+
dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.map(cleanString).filter(Boolean) : [],
|
|
3079
|
+
artifacts,
|
|
3080
|
+
reviewHandoff: normalizeReviewHandoff(task.reviewHandoff),
|
|
3081
|
+
};
|
|
3082
|
+
}).filter(Boolean)
|
|
3083
|
+
: [];
|
|
3084
|
+
if (!tasks.length) return null;
|
|
3085
|
+
return {
|
|
3086
|
+
delegationRequired: true,
|
|
3087
|
+
objective,
|
|
3088
|
+
orchestratorPersonaKey: cleanNullableString(raw.orchestratorPersonaKey),
|
|
3089
|
+
rootRunId: cleanNullableString(raw.rootRunId),
|
|
3090
|
+
managerRunId: cleanNullableString(raw.managerRunId),
|
|
3091
|
+
latestSummary: cleanString(raw.latestSummary),
|
|
3092
|
+
tasks,
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
function extractDelegationLedgerFromText(text) {
|
|
3097
|
+
if (!text || !/delegationRequired|delegation_ledger|delegationLedger/i.test(text)) return null;
|
|
3098
|
+
const candidates = [];
|
|
3099
|
+
const fenced = String(text).matchAll(/```(?:json)?\s*([\s\S]*?)```/gi);
|
|
3100
|
+
for (const match of fenced) candidates.push(match[1]);
|
|
3101
|
+
const tagged = String(text).match(/<delegation_ledger>\s*([\s\S]*?)\s*<\/delegation_ledger>/i);
|
|
3102
|
+
if (tagged) candidates.push(tagged[1]);
|
|
3103
|
+
const inline = String(text).match(/(\{\s*"delegationRequired"[\s\S]*\})/i);
|
|
3104
|
+
if (inline) candidates.push(inline[1]);
|
|
3105
|
+
|
|
3106
|
+
for (const candidate of candidates) {
|
|
3107
|
+
try {
|
|
3108
|
+
const ledger = normalizeDelegationLedger(JSON.parse(candidate.trim()));
|
|
3109
|
+
if (ledger) return ledger;
|
|
3110
|
+
} catch {
|
|
3111
|
+
// Ignore malformed snippets; the raw transcript is still available.
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
return null;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
function delegationLedgerForConversation(conv) {
|
|
3118
|
+
if (!conv) return null;
|
|
3119
|
+
const existing = normalizeDelegationLedger(conv.delegation);
|
|
3120
|
+
if (existing) return existing;
|
|
3121
|
+
const messages = conv.messages || [];
|
|
3122
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
3123
|
+
if (messages[i].role !== 'employee') continue;
|
|
3124
|
+
const parsed = extractDelegationLedgerFromText(messages[i].text);
|
|
3125
|
+
if (parsed) {
|
|
3126
|
+
conv.delegation = parsed;
|
|
3127
|
+
return parsed;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
2478
3133
|
function hasReviewHandoffSignal(text) {
|
|
2479
3134
|
return !!(text && /reviewRequired|reviewTarget|review_handoff/i.test(text));
|
|
2480
3135
|
}
|
|
@@ -2622,8 +3277,22 @@ function deriveDeliverableFormat(conv) {
|
|
|
2622
3277
|
|
|
2623
3278
|
const hay = ((conv && (conv.jobId || '')) + ' ' + (conv && (conv.jobTitle || ''))).toLowerCase();
|
|
2624
3279
|
// Prefer an explicit artifact path captured from the run when present.
|
|
2625
|
-
const
|
|
2626
|
-
|
|
3280
|
+
const artifacts = Array.isArray(conv && conv.artifacts) ? conv.artifacts : [];
|
|
3281
|
+
if (artifacts.length > 1) {
|
|
3282
|
+
return {
|
|
3283
|
+
key: 'artifact_set',
|
|
3284
|
+
label: `${artifacts.length} artifacts`,
|
|
3285
|
+
actions: artifacts.map((artifact, index) => ({
|
|
3286
|
+
id: `open-artifact-${index}`,
|
|
3287
|
+
label: artifactLabel(artifact),
|
|
3288
|
+
primary: index === 0,
|
|
3289
|
+
kind: artifactActionKind(artifact),
|
|
3290
|
+
artifact,
|
|
3291
|
+
})),
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
const artifact = artifacts[0] || null;
|
|
3295
|
+
const artifactName = artifact ? (artifact.name || artifact.path || artifact.url || artifact.label || '') : '';
|
|
2627
3296
|
const ext = artifactName.includes('.') ? artifactName.split('.').pop().toLowerCase() : '';
|
|
2628
3297
|
|
|
2629
3298
|
const isCode = /implementation|fix|refactor|\bpr\b|pull-request|code|bug|engineering/.test(hay)
|
|
@@ -3059,7 +3728,7 @@ function tfShowDoneReviewing(conv, docxPath) {
|
|
|
3059
3728
|
wrap.className = 'rc-done-reviewing';
|
|
3060
3729
|
const label = document.createElement('div');
|
|
3061
3730
|
label.className = 'rdr-label';
|
|
3062
|
-
label.textContent = 'Open the
|
|
3731
|
+
label.textContent = 'Open the doc in your preferred editor (Word, Google Docs, etc.), add comments, then save. When ready:';
|
|
3063
3732
|
const btn = document.createElement('button');
|
|
3064
3733
|
btn.type = 'button';
|
|
3065
3734
|
btn.className = 'rdr-done-btn';
|
|
@@ -3073,15 +3742,15 @@ function tfShowDoneReviewing(conv, docxPath) {
|
|
|
3073
3742
|
? docxPath.replace((state.projectPath || '').replace(/\\/g, '/').replace(/\/$/, '') + '/', '')
|
|
3074
3743
|
: '';
|
|
3075
3744
|
const message = rel
|
|
3076
|
-
? `I've reviewed the document. My feedback is in the
|
|
3077
|
-
: `I've finished reviewing the .
|
|
3745
|
+
? `I've reviewed the document. My feedback is in the doc comments and tracked changes inside \`${rel}\`. Please read every one and address them all in a revision.`
|
|
3746
|
+
: `I've finished reviewing the doc. My feedback is in the doc comments and tracked changes — please read them all and address them in a revision.`;
|
|
3078
3747
|
conv.pendingDocxReview = docxPath || null;
|
|
3079
3748
|
conv.reviewApproved = false;
|
|
3080
3749
|
upsertConversation(conv);
|
|
3081
3750
|
try {
|
|
3082
3751
|
await continueRun(message);
|
|
3083
3752
|
wrap.remove();
|
|
3084
|
-
showStatus('Comments sent — the employee will read your
|
|
3753
|
+
showStatus('Comments sent — the employee will read your doc annotations and come back with a revision.', false);
|
|
3085
3754
|
} catch (err) {
|
|
3086
3755
|
btn.disabled = false;
|
|
3087
3756
|
btn.textContent = '✓ Done reviewing — send my comments to the employee';
|
|
@@ -3105,7 +3774,7 @@ function tfShowDocxUpload(conv) {
|
|
|
3105
3774
|
// is exactly why "Mark complete" left the Project Onboarding dot stale.
|
|
3106
3775
|
function refreshStatusSurfaces() {
|
|
3107
3776
|
if (typeof renderRail === 'function') renderRail();
|
|
3108
|
-
if (document.querySelector('.workspace-conv')) {
|
|
3777
|
+
if (document.querySelector('#proj-workspace .workspace-conv')) {
|
|
3109
3778
|
if (typeof tfRenderTree === 'function') tfRenderTree();
|
|
3110
3779
|
if (typeof tfRenderProjectContextTop === 'function') tfRenderProjectContextTop();
|
|
3111
3780
|
}
|
|
@@ -3225,8 +3894,17 @@ function wirePopovers() {
|
|
|
3225
3894
|
|
|
3226
3895
|
if (e.key === 'Escape') {
|
|
3227
3896
|
const cpModal = document.getElementById('cp-modal');
|
|
3228
|
-
|
|
3897
|
+
const schedModal = document.getElementById('dep-schedule-modal');
|
|
3898
|
+
const webhookModal = document.getElementById('dep-webhook-modal');
|
|
3899
|
+
const aomModal = document.getElementById('area-onboard-modal');
|
|
3900
|
+
if (aomModal && !aomModal.hidden) {
|
|
3901
|
+
tfCloseAreaOnboardModal();
|
|
3902
|
+
} else if (cpModal && !cpModal.hidden) {
|
|
3229
3903
|
closePalette();
|
|
3904
|
+
} else if (schedModal && !schedModal.hidden) {
|
|
3905
|
+
schedModal.hidden = true;
|
|
3906
|
+
} else if (webhookModal && !webhookModal.hidden) {
|
|
3907
|
+
webhookModal.hidden = true;
|
|
3230
3908
|
} else if (els['modal'].classList.contains('open')) {
|
|
3231
3909
|
closeModal();
|
|
3232
3910
|
} else {
|
|
@@ -3529,8 +4207,9 @@ function rerunLastJob() {
|
|
|
3529
4207
|
openPalette();
|
|
3530
4208
|
return;
|
|
3531
4209
|
}
|
|
3532
|
-
|
|
3533
|
-
startRun(
|
|
4210
|
+
const lastRun = state.lastRun;
|
|
4211
|
+
startRun(lastRun.job, lastRun.instructions, lastRun.employeeId);
|
|
4212
|
+
showRerunToast('Re-running: ' + (lastRun.job.title || lastRun.job.id));
|
|
3534
4213
|
}
|
|
3535
4214
|
|
|
3536
4215
|
function showRerunToast(msg) {
|
|
@@ -3538,7 +4217,7 @@ function showRerunToast(msg) {
|
|
|
3538
4217
|
t.className = 'cp-rerun-toast';
|
|
3539
4218
|
t.textContent = msg;
|
|
3540
4219
|
document.body.appendChild(t);
|
|
3541
|
-
setTimeout(() => { if (t.parentNode) t.parentNode.removeChild(t); },
|
|
4220
|
+
setTimeout(() => { if (t.parentNode) t.parentNode.removeChild(t); }, 5000);
|
|
3542
4221
|
}
|
|
3543
4222
|
|
|
3544
4223
|
// ---------------------------------------------------------------------------
|
|
@@ -3976,14 +4655,14 @@ async function startRun(job, instructions, employeeId, preassignedConvId) {
|
|
|
3976
4655
|
// Issue #489: capture selection state at job-start so write-back knows insert-after vs append.
|
|
3977
4656
|
wordStartedWithSelection: document.body.dataset.surface === 'task-pane' && !!(state.wordContext && state.wordContext.hasSelection),
|
|
3978
4657
|
};
|
|
3979
|
-
upsertConversation(conv);
|
|
3980
|
-
state.activeId = conv.id;
|
|
3981
|
-
// Issue #539: make Cmd/Ctrl+Shift+R available as soon as the run is queued,
|
|
3982
|
-
// not only after the server responds with the run id.
|
|
3983
|
-
state.lastRun = { job, instructions, employeeId };
|
|
3984
|
-
persistConversations();
|
|
3985
|
-
renderRail();
|
|
3986
|
-
renderActive();
|
|
4658
|
+
upsertConversation(conv);
|
|
4659
|
+
state.activeId = conv.id;
|
|
4660
|
+
// Issue #539: make Cmd/Ctrl+Shift+R available as soon as the run is queued,
|
|
4661
|
+
// not only after the server responds with the run id.
|
|
4662
|
+
state.lastRun = { job, instructions, employeeId };
|
|
4663
|
+
persistConversations();
|
|
4664
|
+
renderRail();
|
|
4665
|
+
renderActive();
|
|
3987
4666
|
|
|
3988
4667
|
try {
|
|
3989
4668
|
const run = await requestJson('/api/ai-hub/runs', {
|
|
@@ -4011,8 +4690,8 @@ async function startRun(job, instructions, employeeId, preassignedConvId) {
|
|
|
4011
4690
|
renderRail();
|
|
4012
4691
|
renderActive();
|
|
4013
4692
|
startPolling();
|
|
4014
|
-
// Issue #539: update in-memory preferences so the next palette open shows
|
|
4015
|
-
// the correct recent section.
|
|
4693
|
+
// Issue #539: update in-memory preferences so the next palette open shows
|
|
4694
|
+
// the correct recent section.
|
|
4016
4695
|
if (state.bootstrap && state.bootstrap.preferences) {
|
|
4017
4696
|
const prefs = state.bootstrap.preferences;
|
|
4018
4697
|
const nextIds = [job.id, ...(prefs.recentJobIds || []).filter((id) => id !== job.id)].slice(0, 8);
|
|
@@ -4174,6 +4853,9 @@ function foldRunIntoConversation(conv, run) {
|
|
|
4174
4853
|
const runHandoff = normalizeReviewHandoff(run.reviewHandoff);
|
|
4175
4854
|
if (runHandoff) conv.reviewHandoff = runHandoff;
|
|
4176
4855
|
else reviewHandoffForConversation(conv);
|
|
4856
|
+
const runDelegation = normalizeDelegationLedger(run.delegation);
|
|
4857
|
+
if (runDelegation) conv.delegation = runDelegation;
|
|
4858
|
+
else delegationLedgerForConversation(conv);
|
|
4177
4859
|
if (Array.isArray(run.artifacts)) {
|
|
4178
4860
|
conv.artifacts = run.artifacts;
|
|
4179
4861
|
}
|
|
@@ -4185,9 +4867,10 @@ function foldRunIntoConversation(conv, run) {
|
|
|
4185
4867
|
// The browser only parses output text when that structured field is absent.
|
|
4186
4868
|
if (!Array.isArray(run.artifacts)) {
|
|
4187
4869
|
for (const e of conv.events) {
|
|
4188
|
-
const found
|
|
4189
|
-
|
|
4190
|
-
|
|
4870
|
+
for (const found of extractArtifacts(e.text)) {
|
|
4871
|
+
if (!conv.artifacts.some((a) => a.name === found.name && a.where === found.where)) {
|
|
4872
|
+
conv.artifacts.push(found);
|
|
4873
|
+
}
|
|
4191
4874
|
}
|
|
4192
4875
|
}
|
|
4193
4876
|
}
|
|
@@ -4209,27 +4892,40 @@ function foldCompareRunIntoConversation(conv, compareRun) {
|
|
|
4209
4892
|
};
|
|
4210
4893
|
}
|
|
4211
4894
|
|
|
4212
|
-
const ARTIFACT_PATH_RE = /([A-Za-z0-9_\-\.\/]*?(?:docs|public|src|tests)\/[A-Za-z0-9_\-\.\/]+\.[A-Za-z0-9]+)/;
|
|
4213
4895
|
// Paths under these directories are FRAIM lifecycle bookkeeping (RCAs,
|
|
4214
4896
|
// raw learnings, evidence dumps, mock files), not deliverables the
|
|
4215
4897
|
// manager should be drawn to. Excluding them keeps the artifact callout
|
|
4216
4898
|
// meaningful — it should mean "the employee produced this file for you".
|
|
4217
4899
|
const ARTIFACT_EXCLUDE_RE = /(^|\/)(retrospectives|evidence|learnings|mocks|raw|archive)\//i;
|
|
4218
4900
|
function extractArtifact(text) {
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
if (
|
|
4224
|
-
const
|
|
4225
|
-
const
|
|
4226
|
-
const
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
const
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4901
|
+
return extractArtifacts(text)[0] || null;
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
function extractArtifacts(text) {
|
|
4905
|
+
if (!text) return [];
|
|
4906
|
+
const artifacts = [];
|
|
4907
|
+
const seen = new Set();
|
|
4908
|
+
const matches = String(text)
|
|
4909
|
+
.split(/[\s`"'()<>{}\[\],;:]+/)
|
|
4910
|
+
.filter((token) => /^(docs|public|src|tests)\//.test(token));
|
|
4911
|
+
for (const candidate of matches) {
|
|
4912
|
+
const fullPath = candidate.replace(/[.)]+$/g, '');
|
|
4913
|
+
if (!/\.[A-Za-z0-9]+$/.test(fullPath)) continue;
|
|
4914
|
+
if (ARTIFACT_EXCLUDE_RE.test(fullPath)) continue;
|
|
4915
|
+
const segments = fullPath.split('/');
|
|
4916
|
+
const name = segments[segments.length - 1];
|
|
4917
|
+
const where = segments.slice(0, -1).join('/') + '/';
|
|
4918
|
+
// Store the absolute path so the agent can re-read the artifact (and any
|
|
4919
|
+
// .docx export written alongside it) without guessing the project root.
|
|
4920
|
+
const absPath = state.projectPath
|
|
4921
|
+
? state.projectPath.replace(/\\/g, '/').replace(/\/$/, '') + '/' + fullPath
|
|
4922
|
+
: null;
|
|
4923
|
+
const key = absPath || fullPath;
|
|
4924
|
+
if (seen.has(key)) continue;
|
|
4925
|
+
seen.add(key);
|
|
4926
|
+
artifacts.push({ name, where, path: absPath });
|
|
4927
|
+
}
|
|
4928
|
+
return artifacts;
|
|
4233
4929
|
}
|
|
4234
4930
|
|
|
4235
4931
|
function startPolling() {
|
|
@@ -4247,7 +4943,18 @@ function startPolling() {
|
|
|
4247
4943
|
}
|
|
4248
4944
|
try {
|
|
4249
4945
|
if (!fraimDone) {
|
|
4250
|
-
const
|
|
4946
|
+
const runResp = await fetch(`/api/ai-hub/runs/${conv.runId}`, { headers: { 'x-api-key': 'local-dev' } });
|
|
4947
|
+
if (runResp.status === 404) {
|
|
4948
|
+
// Run no longer in server registry (e.g. after restart). Stop polling.
|
|
4949
|
+
window.clearInterval(state.pollHandle);
|
|
4950
|
+
state.pollHandle = null;
|
|
4951
|
+
conv.status = 'done';
|
|
4952
|
+
upsertConversation(conv);
|
|
4953
|
+
renderRail();
|
|
4954
|
+
renderActive();
|
|
4955
|
+
return;
|
|
4956
|
+
}
|
|
4957
|
+
const run = await runResp.json();
|
|
4251
4958
|
foldRunIntoConversation(conv, run);
|
|
4252
4959
|
}
|
|
4253
4960
|
// Issue #442: also poll the compare run when present and not yet terminal.
|
|
@@ -4258,6 +4965,9 @@ function startPolling() {
|
|
|
4258
4965
|
upsertConversation(conv);
|
|
4259
4966
|
renderRail();
|
|
4260
4967
|
renderActive();
|
|
4968
|
+
// #594 R2: keep the Company/Manager area conv panel in sync while polling.
|
|
4969
|
+
if (tf.area === 'company') tfRenderCompany();
|
|
4970
|
+
else if (tf.area === 'manager') tfRenderManager();
|
|
4261
4971
|
// #521: when a project-onboarding run posts its understanding (reaches the
|
|
4262
4972
|
// review gate) or completes, refresh the Brief once so the team's captured
|
|
4263
4973
|
// context appears there for the manager to review/edit and then approve.
|
|
@@ -4310,7 +5020,7 @@ async function tryWordWriteBack(conv) {
|
|
|
4310
5020
|
const action = conv.wordStartedWithSelection ? 'insert-after' : 'append-to-doc';
|
|
4311
5021
|
await requestWordContext(action, { text: outputText });
|
|
4312
5022
|
await requestWordContext('track-changes-off');
|
|
4313
|
-
showStatus('Result written to document — review tracked changes
|
|
5023
|
+
showStatus('Result written to document — review tracked changes.');
|
|
4314
5024
|
} catch (e) {
|
|
4315
5025
|
console.warn('Word write-back failed:', e);
|
|
4316
5026
|
}
|
|
@@ -4326,6 +5036,15 @@ function convNeedsPolling(conv) {
|
|
|
4326
5036
|
function switchToConversation(id) {
|
|
4327
5037
|
state.activeId = id;
|
|
4328
5038
|
persistConversations();
|
|
5039
|
+
// For Company/Manager: move the shared .page into this area's conv-host and
|
|
5040
|
+
// switch from the info view to the full coaching panel.
|
|
5041
|
+
if (tf.area === 'company' || tf.area === 'manager') {
|
|
5042
|
+
tfEnsurePageInArea(tf.area);
|
|
5043
|
+
const host = document.getElementById(tf.area + '-conv-host');
|
|
5044
|
+
const info = document.getElementById(tf.area + '-info-view');
|
|
5045
|
+
if (host) host.hidden = false;
|
|
5046
|
+
if (info) info.hidden = true;
|
|
5047
|
+
}
|
|
4329
5048
|
renderRail();
|
|
4330
5049
|
renderActive();
|
|
4331
5050
|
const conv = activeConversation();
|
|
@@ -4466,9 +5185,22 @@ function renderManagerTeamPool() {
|
|
|
4466
5185
|
const personas = (state.bootstrap?.personas || []).filter((p) => (p.seatCount || 0) > 0);
|
|
4467
5186
|
const managerTeamKeys = new Set((state.bootstrap?.managerTeam || []).map((e) => e.personaKey));
|
|
4468
5187
|
container.innerHTML = '';
|
|
5188
|
+
if (personas.length === 0) {
|
|
5189
|
+
const msg = document.createElement('p');
|
|
5190
|
+
msg.className = 'manager-no-team-msg';
|
|
5191
|
+
msg.textContent = "You haven't hired any specialists yet. Go to Company to see the full roster and hire the team you need.";
|
|
5192
|
+
container.appendChild(msg);
|
|
5193
|
+
const btn = document.createElement('button');
|
|
5194
|
+
btn.className = 'manager-go-company-btn';
|
|
5195
|
+
btn.type = 'button';
|
|
5196
|
+
btn.textContent = 'Go to Company to hire';
|
|
5197
|
+
btn.addEventListener('click', () => tfShowArea('company'));
|
|
5198
|
+
container.appendChild(btn);
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
4469
5201
|
for (const persona of personas) {
|
|
4470
5202
|
const row = document.createElement('div');
|
|
4471
|
-
row.className = 'team-row';
|
|
5203
|
+
row.className = 'team-row emp-tile';
|
|
4472
5204
|
|
|
4473
5205
|
const dot = document.createElement('span');
|
|
4474
5206
|
const oos = (persona.seatsInUse || 0) >= (persona.seatCount || 0);
|
|
@@ -4921,6 +5653,7 @@ function wireEvents() {
|
|
|
4921
5653
|
try {
|
|
4922
5654
|
await loadBootstrap(null, docUrl);
|
|
4923
5655
|
await hydrateConversationsFromServer();
|
|
5656
|
+
startBgConvPoll();
|
|
4924
5657
|
} catch (error) {
|
|
4925
5658
|
showStatus(error.message, true);
|
|
4926
5659
|
return;
|
|
@@ -5201,7 +5934,6 @@ function renderFirstRunLanding(mode) {
|
|
|
5201
5934
|
|
|
5202
5935
|
const STORAGE_KEY_PROJECTS_512 = 'fraim.aiHub.projects.v1';
|
|
5203
5936
|
const STORAGE_KEY_ASSIGNMENTS_512 = 'fraim.aiHub.assignments.v1';
|
|
5204
|
-
const STORAGE_KEY_RAIL_512 = 'fraim.aiHub.railCollapsed.v1';
|
|
5205
5937
|
|
|
5206
5938
|
const tf = {
|
|
5207
5939
|
enabled: false,
|
|
@@ -5214,8 +5946,6 @@ const tf = {
|
|
|
5214
5946
|
npSelectedEmployees: [],
|
|
5215
5947
|
};
|
|
5216
5948
|
|
|
5217
|
-
const TF_STEP_LABELS = { install: 'Install', company: 'Set up company', hire: 'Hire your team', project: 'Start a project' };
|
|
5218
|
-
const TF_STEP_ORDER = ['install', 'company', 'hire', 'project'];
|
|
5219
5949
|
const TF_AVATAR_COLORS = ['#4a7fd4', '#7c6f61', '#5a9e6e', '#b06a4f', '#6b4c92', '#b07a2e', '#2e8b78', '#a24747'];
|
|
5220
5950
|
function tfAvatarFor(key, index) {
|
|
5221
5951
|
const color = TF_AVATAR_COLORS[(index || 0) % TF_AVATAR_COLORS.length];
|
|
@@ -5298,7 +6028,25 @@ function tfEmployeeDot(projectId, employeeKey) {
|
|
|
5298
6028
|
// Area + sub-tab routing
|
|
5299
6029
|
// ---------------------------------------------------------------------------
|
|
5300
6030
|
function tfShowArea(area) {
|
|
6031
|
+
if (!state.areaActiveId) state.areaActiveId = {};
|
|
6032
|
+
// Save Projects' active conv when leaving it, so we can restore on return.
|
|
6033
|
+
if (tf.area === 'projects') state.areaActiveId.projects = state.activeId;
|
|
6034
|
+
|
|
5301
6035
|
tf.area = area;
|
|
6036
|
+
|
|
6037
|
+
if (area === 'projects') {
|
|
6038
|
+
// Restore the saved Projects conv; discard it if it turned out to be area-scoped.
|
|
6039
|
+
const savedId = state.areaActiveId.projects || null;
|
|
6040
|
+
const savedConv = savedId && Object.values(state.conversations || {}).flat().find((c) => c.id === savedId);
|
|
6041
|
+
state.activeId = (savedConv && !AREA_SCOPED_JOBS.has(savedConv.jobId)) ? savedId : null;
|
|
6042
|
+
// Return the shared .page to the Projects workspace and reset Company/Manager hosts.
|
|
6043
|
+
tfEnsurePageInArea('projects');
|
|
6044
|
+
for (const el of document.querySelectorAll('.area-conv-host')) el.hidden = true;
|
|
6045
|
+
for (const el of document.querySelectorAll('.area-info-view')) el.hidden = false;
|
|
6046
|
+
if (typeof renderRail === 'function') renderRail();
|
|
6047
|
+
if (typeof renderActive === 'function') renderActive();
|
|
6048
|
+
}
|
|
6049
|
+
|
|
5302
6050
|
for (const el of document.querySelectorAll('.hub-area')) {
|
|
5303
6051
|
const match = el.id === 'area-' + area;
|
|
5304
6052
|
el.classList.toggle('on', match);
|
|
@@ -5353,6 +6101,8 @@ function tfSelectProjectView(view, projectId) {
|
|
|
5353
6101
|
tfRenderTree();
|
|
5354
6102
|
tfRenderProjectContextTop();
|
|
5355
6103
|
tfApplyWorkspaceMode();
|
|
6104
|
+
// Issue #578: reload deployment roster when entering workspace.
|
|
6105
|
+
loadDeployments();
|
|
5356
6106
|
}
|
|
5357
6107
|
}
|
|
5358
6108
|
|
|
@@ -5834,6 +6584,393 @@ function tfShowProjectBrief() { const a = document.getElementById('proj-brief-ac
|
|
|
5834
6584
|
function tfShowProjectLearnings() { const a = document.getElementById('proj-learnings-acc'); if (a) a.open = true; }
|
|
5835
6585
|
function tfHideProjectLearnings() { /* panels removed in #521; brief/learnings are inline top sections */ }
|
|
5836
6586
|
|
|
6587
|
+
// ---------------------------------------------------------------------------
|
|
6588
|
+
// Issue #578: Deployment roster (scheduled + webhook triggers)
|
|
6589
|
+
// ---------------------------------------------------------------------------
|
|
6590
|
+
|
|
6591
|
+
async function loadDeployments() {
|
|
6592
|
+
const list = document.getElementById('proj-deployments-list');
|
|
6593
|
+
if (!list) return;
|
|
6594
|
+
try {
|
|
6595
|
+
const [schResp, whResp] = await Promise.all([
|
|
6596
|
+
fetch('/api/ai-hub/schedules'),
|
|
6597
|
+
fetch('/api/ai-hub/webhooks'),
|
|
6598
|
+
]);
|
|
6599
|
+
const schedules = schResp.ok ? await schResp.json() : [];
|
|
6600
|
+
const webhooks = whResp.ok ? await whResp.json() : [];
|
|
6601
|
+
renderDeploymentList(list, [...schedules, ...webhooks]);
|
|
6602
|
+
} catch {
|
|
6603
|
+
list.textContent = 'Could not load deployments.';
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
let _editingDepId = null;
|
|
6608
|
+
let _activeSchPreset = null;
|
|
6609
|
+
|
|
6610
|
+
function detectPreset(expr) {
|
|
6611
|
+
if (!expr) return null;
|
|
6612
|
+
const parts = expr.trim().split(/\s+/);
|
|
6613
|
+
if (parts.length !== 5) return 'custom';
|
|
6614
|
+
const [min, hour, dom, month, dow] = parts;
|
|
6615
|
+
if (dom !== '*' || month !== '*') return 'custom';
|
|
6616
|
+
if (/^\d+$/.test(min) && /^\d+$/.test(hour)) {
|
|
6617
|
+
if (dow === '*') return 'daily';
|
|
6618
|
+
if (dow === '1-5') return 'weekdays';
|
|
6619
|
+
if (/^\d+$/.test(dow)) return 'weekly';
|
|
6620
|
+
}
|
|
6621
|
+
return 'custom';
|
|
6622
|
+
}
|
|
6623
|
+
|
|
6624
|
+
function updateSchCronFromPreset(preset, timeValue, dayValue) {
|
|
6625
|
+
const [hStr, mStr] = (timeValue || '09:00').split(':');
|
|
6626
|
+
const h = parseInt(hStr, 10) || 9;
|
|
6627
|
+
const m = parseInt(mStr, 10) || 0;
|
|
6628
|
+
if (preset === 'daily') return `${m} ${h} * * *`;
|
|
6629
|
+
if (preset === 'weekdays') return `${m} ${h} * * 1-5`;
|
|
6630
|
+
if (preset === 'weekly') return `${m} ${h} * * ${dayValue || 1}`;
|
|
6631
|
+
return '';
|
|
6632
|
+
}
|
|
6633
|
+
|
|
6634
|
+
function applySchPreset(preset) {
|
|
6635
|
+
_activeSchPreset = preset;
|
|
6636
|
+
document.querySelectorAll('#dep-sch-presets .dep-preset-chip').forEach(c => {
|
|
6637
|
+
c.classList.toggle('active', c.dataset.preset === preset);
|
|
6638
|
+
});
|
|
6639
|
+
const timeRow = document.getElementById('dep-sch-time-row');
|
|
6640
|
+
const dayField = document.getElementById('dep-sch-day-field');
|
|
6641
|
+
const customRow = document.getElementById('dep-sch-custom-row');
|
|
6642
|
+
const timeInput = document.getElementById('dep-sch-time');
|
|
6643
|
+
const dayInput = document.getElementById('dep-sch-day');
|
|
6644
|
+
const cronPresetInput = document.getElementById('dep-sch-cron-preset');
|
|
6645
|
+
const preview = document.getElementById('dep-sch-cron-preview');
|
|
6646
|
+
const isCustom = preset === 'custom';
|
|
6647
|
+
const hasTime = preset !== null && !isCustom;
|
|
6648
|
+
if (timeRow) timeRow.hidden = !hasTime;
|
|
6649
|
+
if (dayField) dayField.hidden = preset !== 'weekly';
|
|
6650
|
+
if (customRow) customRow.hidden = !isCustom;
|
|
6651
|
+
if (hasTime && cronPresetInput && timeInput) {
|
|
6652
|
+
const cron = updateSchCronFromPreset(preset, timeInput.value, dayInput ? dayInput.value : '1');
|
|
6653
|
+
cronPresetInput.value = cron;
|
|
6654
|
+
if (preview) preview.textContent = cronToHuman(cron);
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
|
|
6658
|
+
function cronToHuman(expr) {
|
|
6659
|
+
if (!expr) return '';
|
|
6660
|
+
const parts = expr.trim().split(/\s+/);
|
|
6661
|
+
if (parts.length < 5) return expr;
|
|
6662
|
+
const [min, hour, dom, month, dow] = parts;
|
|
6663
|
+
if (min.startsWith('*/') && hour === '*' && dom === '*' && month === '*' && dow === '*') {
|
|
6664
|
+
const n = parseInt(min.slice(2), 10);
|
|
6665
|
+
return 'Every ' + n + ' minute' + (n === 1 ? '' : 's');
|
|
6666
|
+
}
|
|
6667
|
+
if (!min.includes('/') && !min.includes(',') && hour === '*' && dom === '*' && month === '*' && dow === '*') {
|
|
6668
|
+
return 'Every hour at :' + min.padStart(2, '0');
|
|
6669
|
+
}
|
|
6670
|
+
const minuteN = parseInt(min, 10);
|
|
6671
|
+
const hourN = parseInt(hour, 10);
|
|
6672
|
+
const hasSpecificTime = !isNaN(minuteN) && !isNaN(hourN) && !min.includes('/') && !min.includes(',') && !hour.includes('/') && !hour.includes(',');
|
|
6673
|
+
if (!hasSpecificTime) return expr;
|
|
6674
|
+
const ampm = hourN >= 12 ? 'PM' : 'AM';
|
|
6675
|
+
const h12 = hourN % 12 === 0 ? 12 : hourN % 12;
|
|
6676
|
+
const timeStr = h12 + ':' + String(minuteN).padStart(2, '0') + ' ' + ampm;
|
|
6677
|
+
const dayNames = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
|
|
6678
|
+
if (dom === '*' && month === '*') {
|
|
6679
|
+
if (dow === '*') return 'Daily at ' + timeStr;
|
|
6680
|
+
if (dow === '1-5' || dow === 'MON-FRI' || dow === 'mon-fri') return 'Weekdays at ' + timeStr;
|
|
6681
|
+
if (dow === '0,6' || dow === '6,0' || dow === 'SAT,SUN' || dow === 'SUN,SAT') return 'Weekends at ' + timeStr;
|
|
6682
|
+
const dowN = parseInt(dow, 10);
|
|
6683
|
+
if (!isNaN(dowN) && dowN >= 0 && dowN <= 6) return dayNames[dowN] + ' at ' + timeStr;
|
|
6684
|
+
return 'On ' + dow + ' at ' + timeStr;
|
|
6685
|
+
}
|
|
6686
|
+
return expr;
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
function renderDeploymentList(container, deployments) {
|
|
6690
|
+
container.innerHTML = '';
|
|
6691
|
+
if (!deployments.length) {
|
|
6692
|
+
const empty = document.createElement('p');
|
|
6693
|
+
empty.className = 'dep-empty';
|
|
6694
|
+
empty.textContent = 'No assignments yet. Use + Schedule or + Trigger above to set up scheduled and triggered assignments.';
|
|
6695
|
+
container.appendChild(empty);
|
|
6696
|
+
return;
|
|
6697
|
+
}
|
|
6698
|
+
for (const dep of deployments) {
|
|
6699
|
+
const card = document.createElement('div');
|
|
6700
|
+
card.className = 'dep-card';
|
|
6701
|
+
const top = document.createElement('div');
|
|
6702
|
+
top.className = 'dep-card-top';
|
|
6703
|
+
const icon = document.createElement('span');
|
|
6704
|
+
icon.className = 'dep-type-icon';
|
|
6705
|
+
icon.textContent = dep.type === 'scheduled' ? '⏱' : '🔗';
|
|
6706
|
+
const label = document.createElement('span');
|
|
6707
|
+
label.className = 'dep-label';
|
|
6708
|
+
label.textContent = dep.label;
|
|
6709
|
+
const typeBadge = document.createElement('span');
|
|
6710
|
+
typeBadge.className = 'dep-type-badge dep-type-badge--' + dep.type;
|
|
6711
|
+
typeBadge.textContent = dep.type === 'scheduled' ? 'Scheduled' : 'Triggered';
|
|
6712
|
+
top.appendChild(icon);
|
|
6713
|
+
top.appendChild(label);
|
|
6714
|
+
top.appendChild(typeBadge);
|
|
6715
|
+
card.appendChild(top);
|
|
6716
|
+
|
|
6717
|
+
if (dep.cronExpr) {
|
|
6718
|
+
const cron = document.createElement('div');
|
|
6719
|
+
cron.className = 'dep-detail';
|
|
6720
|
+
const human = cronToHuman(dep.cronExpr);
|
|
6721
|
+
cron.textContent = human !== dep.cronExpr ? human : dep.cronExpr;
|
|
6722
|
+
if (human !== dep.cronExpr) cron.title = dep.cronExpr;
|
|
6723
|
+
card.appendChild(cron);
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6726
|
+
const employee = (state.bootstrap?.employees || []).find((e) => e.id === dep.hostId);
|
|
6727
|
+
if (employee) {
|
|
6728
|
+
const empRow = document.createElement('div');
|
|
6729
|
+
empRow.className = 'dep-detail dep-detail--emp';
|
|
6730
|
+
empRow.textContent = employee.label;
|
|
6731
|
+
card.appendChild(empRow);
|
|
6732
|
+
}
|
|
6733
|
+
|
|
6734
|
+
if (dep.inboundUrl) {
|
|
6735
|
+
const urlRow = document.createElement('div');
|
|
6736
|
+
urlRow.className = 'dep-detail dep-detail--url';
|
|
6737
|
+
const urlCode = document.createElement('code');
|
|
6738
|
+
urlCode.textContent = dep.inboundUrl;
|
|
6739
|
+
urlCode.className = 'dep-inbound-url dep-inbound-url--inline';
|
|
6740
|
+
const copyBtn = document.createElement('button');
|
|
6741
|
+
copyBtn.type = 'button';
|
|
6742
|
+
copyBtn.className = 'hm-copy-btn';
|
|
6743
|
+
copyBtn.textContent = 'Copy';
|
|
6744
|
+
copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(dep.inboundUrl).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); }).catch(() => {}); });
|
|
6745
|
+
urlRow.appendChild(urlCode);
|
|
6746
|
+
urlRow.appendChild(copyBtn);
|
|
6747
|
+
card.appendChild(urlRow);
|
|
6748
|
+
}
|
|
6749
|
+
|
|
6750
|
+
const cardActions = document.createElement('div');
|
|
6751
|
+
cardActions.className = 'dep-card-actions';
|
|
6752
|
+
|
|
6753
|
+
const editBtn = document.createElement('button');
|
|
6754
|
+
editBtn.type = 'button';
|
|
6755
|
+
editBtn.className = 'dep-edit-btn';
|
|
6756
|
+
editBtn.title = 'Edit this assignment';
|
|
6757
|
+
editBtn.textContent = 'Edit';
|
|
6758
|
+
editBtn.addEventListener('click', () => { openDeploymentModal(dep.type === 'scheduled' ? 'schedule' : 'webhook', dep); });
|
|
6759
|
+
cardActions.appendChild(editBtn);
|
|
6760
|
+
|
|
6761
|
+
const delBtn = document.createElement('button');
|
|
6762
|
+
delBtn.type = 'button';
|
|
6763
|
+
delBtn.className = 'dep-del-btn';
|
|
6764
|
+
delBtn.title = 'Remove this assignment';
|
|
6765
|
+
delBtn.textContent = 'Remove';
|
|
6766
|
+
delBtn.addEventListener('click', async () => {
|
|
6767
|
+
const endpoint = dep.type === 'scheduled' ? 'schedules' : 'webhooks';
|
|
6768
|
+
await fetch('/api/ai-hub/' + endpoint + '/' + dep.id, { method: 'DELETE' });
|
|
6769
|
+
await loadDeployments();
|
|
6770
|
+
});
|
|
6771
|
+
cardActions.appendChild(delBtn);
|
|
6772
|
+
card.appendChild(cardActions);
|
|
6773
|
+
container.appendChild(card);
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
6776
|
+
|
|
6777
|
+
function initDeploymentButtons() {
|
|
6778
|
+
const addSchBtn = document.getElementById('dep-add-schedule-btn');
|
|
6779
|
+
const addWhBtn = document.getElementById('dep-add-webhook-btn');
|
|
6780
|
+
if (addSchBtn) addSchBtn.addEventListener('click', () => openDeploymentModal('schedule'));
|
|
6781
|
+
if (addWhBtn) addWhBtn.addEventListener('click', () => openDeploymentModal('webhook'));
|
|
6782
|
+
|
|
6783
|
+
// Close buttons
|
|
6784
|
+
const schClose = document.getElementById('dep-schedule-close');
|
|
6785
|
+
const whClose = document.getElementById('dep-webhook-close');
|
|
6786
|
+
if (schClose) schClose.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
|
|
6787
|
+
if (whClose) whClose.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
|
|
6788
|
+
|
|
6789
|
+
// Cancel buttons
|
|
6790
|
+
const schCancel = document.getElementById('dep-sch-cancel-btn');
|
|
6791
|
+
const whCancel = document.getElementById('dep-wh-cancel-btn');
|
|
6792
|
+
if (schCancel) schCancel.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
|
|
6793
|
+
if (whCancel) whCancel.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
|
|
6794
|
+
|
|
6795
|
+
// Save buttons
|
|
6796
|
+
const schSave = document.getElementById('dep-sch-save-btn');
|
|
6797
|
+
if (schSave) schSave.addEventListener('click', saveScheduleDeployment);
|
|
6798
|
+
const whSave = document.getElementById('dep-wh-save-btn');
|
|
6799
|
+
if (whSave) whSave.addEventListener('click', saveWebhookDeployment);
|
|
6800
|
+
|
|
6801
|
+
// Preset chips
|
|
6802
|
+
document.querySelectorAll('#dep-sch-presets .dep-preset-chip').forEach(chip => {
|
|
6803
|
+
chip.addEventListener('click', () => applySchPreset(chip.dataset.preset));
|
|
6804
|
+
});
|
|
6805
|
+
const schTimeInput = document.getElementById('dep-sch-time');
|
|
6806
|
+
const schDayInput = document.getElementById('dep-sch-day');
|
|
6807
|
+
const cronPresetInput = document.getElementById('dep-sch-cron-preset');
|
|
6808
|
+
if (schTimeInput) {
|
|
6809
|
+
schTimeInput.addEventListener('change', () => {
|
|
6810
|
+
if (_activeSchPreset && _activeSchPreset !== 'custom') {
|
|
6811
|
+
const cron = updateSchCronFromPreset(_activeSchPreset, schTimeInput.value, schDayInput ? schDayInput.value : '1');
|
|
6812
|
+
if (cronPresetInput) cronPresetInput.value = cron;
|
|
6813
|
+
const prev = document.getElementById('dep-sch-cron-preview');
|
|
6814
|
+
if (prev) prev.textContent = cronToHuman(cron);
|
|
6815
|
+
}
|
|
6816
|
+
});
|
|
6817
|
+
}
|
|
6818
|
+
if (schDayInput) {
|
|
6819
|
+
schDayInput.addEventListener('change', () => {
|
|
6820
|
+
if (_activeSchPreset === 'weekly') {
|
|
6821
|
+
const cron = updateSchCronFromPreset('weekly', schTimeInput ? schTimeInput.value : '09:00', schDayInput.value);
|
|
6822
|
+
if (cronPresetInput) cronPresetInput.value = cron;
|
|
6823
|
+
const prev = document.getElementById('dep-sch-cron-preview');
|
|
6824
|
+
if (prev) prev.textContent = cronToHuman(cron);
|
|
6825
|
+
}
|
|
6826
|
+
});
|
|
6827
|
+
}
|
|
6828
|
+
|
|
6829
|
+
// Live cron preview (custom mode)
|
|
6830
|
+
const cronInput = document.getElementById('dep-sch-cron');
|
|
6831
|
+
const cronPreview = document.getElementById('dep-sch-cron-preview');
|
|
6832
|
+
if (cronInput && cronPreview) {
|
|
6833
|
+
cronInput.addEventListener('input', () => {
|
|
6834
|
+
const human = cronToHuman(cronInput.value.trim());
|
|
6835
|
+
cronPreview.textContent = human && human !== cronInput.value.trim() ? human : '';
|
|
6836
|
+
});
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6839
|
+
// Copy inbound URL button
|
|
6840
|
+
const copyBtn = document.getElementById('dep-wh-copy-btn');
|
|
6841
|
+
if (copyBtn) copyBtn.addEventListener('click', () => {
|
|
6842
|
+
const url = document.getElementById('dep-wh-inbound-url').textContent;
|
|
6843
|
+
navigator.clipboard.writeText(url).catch(() => {});
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6846
|
+
|
|
6847
|
+
function openDeploymentModal(type, dep) {
|
|
6848
|
+
const jobs = state.bootstrap?.jobs ?? [];
|
|
6849
|
+
const employees = state.bootstrap?.employees ?? [];
|
|
6850
|
+
const editing = dep != null;
|
|
6851
|
+
_editingDepId = editing ? dep.id : null;
|
|
6852
|
+
|
|
6853
|
+
function populateJobs(sel, currentJobId) {
|
|
6854
|
+
sel.innerHTML = '';
|
|
6855
|
+
for (const j of jobs) {
|
|
6856
|
+
const opt = document.createElement('option');
|
|
6857
|
+
opt.value = j.id;
|
|
6858
|
+
opt.textContent = j.title;
|
|
6859
|
+
sel.appendChild(opt);
|
|
6860
|
+
}
|
|
6861
|
+
if (currentJobId) sel.value = currentJobId;
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6864
|
+
function populateEmployees(sel, currentHostId) {
|
|
6865
|
+
sel.innerHTML = '';
|
|
6866
|
+
const fallback = [{ id: 'claude', label: 'Claude' }, { id: 'codex', label: 'Codex' }, { id: 'gemini', label: 'Gemini' }, { id: 'copilot', label: 'Copilot' }];
|
|
6867
|
+
const list = employees.length ? employees : fallback;
|
|
6868
|
+
for (const e of list) {
|
|
6869
|
+
const opt = document.createElement('option');
|
|
6870
|
+
opt.value = e.id;
|
|
6871
|
+
opt.textContent = e.label;
|
|
6872
|
+
sel.appendChild(opt);
|
|
6873
|
+
}
|
|
6874
|
+
if (currentHostId) sel.value = currentHostId;
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6877
|
+
if (type === 'schedule') {
|
|
6878
|
+
const modal = document.getElementById('dep-schedule-modal');
|
|
6879
|
+
document.getElementById('dep-sch-modal-title').textContent = editing ? 'Edit Scheduled Assignment' : 'New Scheduled Assignment';
|
|
6880
|
+
document.getElementById('dep-sch-save-btn').textContent = editing ? 'Save changes' : 'Add assignment';
|
|
6881
|
+
populateJobs(document.getElementById('dep-sch-job'), dep?.jobId);
|
|
6882
|
+
populateEmployees(document.getElementById('dep-sch-host'), dep?.hostId || 'claude');
|
|
6883
|
+
document.getElementById('dep-sch-error').hidden = true;
|
|
6884
|
+
document.getElementById('dep-sch-label').value = dep?.label ?? '';
|
|
6885
|
+
document.getElementById('dep-sch-instructions').value = dep?.instructions ?? '';
|
|
6886
|
+
// Detect preset and pre-fill time/day
|
|
6887
|
+
const preset = dep?.cronExpr ? detectPreset(dep.cronExpr) : null;
|
|
6888
|
+
if (preset && preset !== 'custom' && dep?.cronExpr) {
|
|
6889
|
+
const parts = dep.cronExpr.trim().split(/\s+/);
|
|
6890
|
+
const m = parseInt(parts[0], 10) || 0;
|
|
6891
|
+
const h = parseInt(parts[1], 10) || 9;
|
|
6892
|
+
document.getElementById('dep-sch-time').value = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
|
6893
|
+
if (preset === 'weekly') document.getElementById('dep-sch-day').value = parts[4];
|
|
6894
|
+
document.getElementById('dep-sch-cron-preset').value = dep.cronExpr;
|
|
6895
|
+
} else if (preset === 'custom' && dep?.cronExpr) {
|
|
6896
|
+
document.getElementById('dep-sch-cron').value = dep.cronExpr;
|
|
6897
|
+
const cronPreview = document.getElementById('dep-sch-cron-preview');
|
|
6898
|
+
if (cronPreview) cronPreview.textContent = cronToHuman(dep.cronExpr);
|
|
6899
|
+
} else {
|
|
6900
|
+
document.getElementById('dep-sch-cron').value = '';
|
|
6901
|
+
document.getElementById('dep-sch-time').value = '09:00';
|
|
6902
|
+
}
|
|
6903
|
+
applySchPreset(preset || 'daily');
|
|
6904
|
+
modal.hidden = false;
|
|
6905
|
+
} else {
|
|
6906
|
+
const modal = document.getElementById('dep-webhook-modal');
|
|
6907
|
+
document.getElementById('dep-wh-modal-title').textContent = editing ? 'Edit Triggered Assignment' : 'New Triggered Assignment';
|
|
6908
|
+
document.getElementById('dep-wh-save-btn').textContent = editing ? 'Save changes' : 'Add assignment';
|
|
6909
|
+
populateJobs(document.getElementById('dep-wh-job'), dep?.jobId);
|
|
6910
|
+
populateEmployees(document.getElementById('dep-wh-host'), dep?.hostId || 'claude');
|
|
6911
|
+
document.getElementById('dep-wh-error').hidden = true;
|
|
6912
|
+
document.getElementById('dep-wh-inbound-row').hidden = true;
|
|
6913
|
+
document.getElementById('dep-wh-label').value = dep?.label ?? '';
|
|
6914
|
+
document.getElementById('dep-wh-instructions').value = dep?.instructions ?? '';
|
|
6915
|
+
modal.hidden = false;
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
async function saveScheduleDeployment() {
|
|
6920
|
+
const label = document.getElementById('dep-sch-label').value.trim();
|
|
6921
|
+
const jobId = document.getElementById('dep-sch-job').value;
|
|
6922
|
+
const hostId = document.getElementById('dep-sch-host').value;
|
|
6923
|
+
const isCustom = _activeSchPreset === 'custom';
|
|
6924
|
+
const cronExpr = isCustom
|
|
6925
|
+
? document.getElementById('dep-sch-cron').value.trim()
|
|
6926
|
+
: (document.getElementById('dep-sch-cron-preset').value.trim() || '');
|
|
6927
|
+
const instructions = document.getElementById('dep-sch-instructions').value.trim();
|
|
6928
|
+
const errEl = document.getElementById('dep-sch-error');
|
|
6929
|
+
if (!label || !cronExpr) { errEl.textContent = 'Label and schedule are required.'; errEl.hidden = false; return; }
|
|
6930
|
+
const isEdit = _editingDepId !== null;
|
|
6931
|
+
const url = isEdit ? '/api/ai-hub/schedules/' + _editingDepId : '/api/ai-hub/schedules';
|
|
6932
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
6933
|
+
try {
|
|
6934
|
+
const resp = await fetch(url, {
|
|
6935
|
+
method,
|
|
6936
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6937
|
+
body: JSON.stringify({ label, jobId, hostId, cronExpr, instructions: instructions || undefined }),
|
|
6938
|
+
});
|
|
6939
|
+
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || (isEdit ? 'Failed to update assignment.' : 'Failed to create assignment.'); errEl.hidden = false; return; }
|
|
6940
|
+
_editingDepId = null;
|
|
6941
|
+
document.getElementById('dep-schedule-modal').hidden = true;
|
|
6942
|
+
await loadDeployments();
|
|
6943
|
+
} catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6946
|
+
async function saveWebhookDeployment() {
|
|
6947
|
+
const label = document.getElementById('dep-wh-label').value.trim();
|
|
6948
|
+
const jobId = document.getElementById('dep-wh-job').value;
|
|
6949
|
+
const hostId = document.getElementById('dep-wh-host').value;
|
|
6950
|
+
const instructions = document.getElementById('dep-wh-instructions').value.trim();
|
|
6951
|
+
const errEl = document.getElementById('dep-wh-error');
|
|
6952
|
+
if (!label) { errEl.textContent = 'Label is required.'; errEl.hidden = false; return; }
|
|
6953
|
+
const isEdit = _editingDepId !== null;
|
|
6954
|
+
const url = isEdit ? '/api/ai-hub/webhooks/' + _editingDepId : '/api/ai-hub/webhooks';
|
|
6955
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
6956
|
+
try {
|
|
6957
|
+
const resp = await fetch(url, {
|
|
6958
|
+
method,
|
|
6959
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6960
|
+
body: JSON.stringify({ label, jobId, hostId, instructions: instructions || undefined }),
|
|
6961
|
+
});
|
|
6962
|
+
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || (isEdit ? 'Failed to update assignment.' : 'Failed to create assignment.'); errEl.hidden = false; return; }
|
|
6963
|
+
if (!isEdit) {
|
|
6964
|
+
const dep = await resp.json();
|
|
6965
|
+
document.getElementById('dep-wh-inbound-url').textContent = dep.inboundUrl;
|
|
6966
|
+
document.getElementById('dep-wh-inbound-row').hidden = false;
|
|
6967
|
+
}
|
|
6968
|
+
_editingDepId = null;
|
|
6969
|
+
if (isEdit) document.getElementById('dep-webhook-modal').hidden = true;
|
|
6970
|
+
await loadDeployments();
|
|
6971
|
+
} catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
|
|
6972
|
+
}
|
|
6973
|
+
|
|
5837
6974
|
// ---------------------------------------------------------------------------
|
|
5838
6975
|
// Company / Manager / Brain content
|
|
5839
6976
|
// ---------------------------------------------------------------------------
|
|
@@ -5902,6 +7039,54 @@ async function tfSaveContext(key, content) {
|
|
|
5902
7039
|
return data;
|
|
5903
7040
|
}
|
|
5904
7041
|
|
|
7042
|
+
// #594 R3/R4: GitHub push helpers.
|
|
7043
|
+
function tfIsGithubConfigured() {
|
|
7044
|
+
return !!(state.bootstrap && state.bootstrap.repository && state.bootstrap.repository.provider === 'github');
|
|
7045
|
+
}
|
|
7046
|
+
|
|
7047
|
+
// #594 R4: Render a dismissable inline push bar into `container` when GitHub is
|
|
7048
|
+
// configured. Called after a successful inline context save so the manager can
|
|
7049
|
+
// push the change to the team repo with one click.
|
|
7050
|
+
function tfMaybeShowPushPrompt(key, container) {
|
|
7051
|
+
if (!tfIsGithubConfigured()) return;
|
|
7052
|
+
const existing = container && container.querySelector('.ctx-push-bar');
|
|
7053
|
+
if (existing) existing.remove();
|
|
7054
|
+
const bar = document.createElement('div');
|
|
7055
|
+
bar.className = 'ctx-push-bar';
|
|
7056
|
+
const msg = document.createElement('span');
|
|
7057
|
+
msg.className = 'ctx-push-msg';
|
|
7058
|
+
msg.textContent = 'Change saved locally. Push to share with your team?';
|
|
7059
|
+
const btn = document.createElement('button');
|
|
7060
|
+
btn.className = 'ctx-push-btn';
|
|
7061
|
+
btn.type = 'button';
|
|
7062
|
+
btn.textContent = 'Push to team';
|
|
7063
|
+
btn.addEventListener('click', async () => {
|
|
7064
|
+
btn.disabled = true;
|
|
7065
|
+
btn.textContent = 'Pushing...';
|
|
7066
|
+
try {
|
|
7067
|
+
await requestJson('/api/ai-hub/context/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) });
|
|
7068
|
+
bar.remove();
|
|
7069
|
+
} catch {
|
|
7070
|
+
btn.disabled = false;
|
|
7071
|
+
btn.textContent = 'Push to team';
|
|
7072
|
+
}
|
|
7073
|
+
});
|
|
7074
|
+
const dismiss = document.createElement('button');
|
|
7075
|
+
dismiss.className = 'ctx-push-dismiss';
|
|
7076
|
+
dismiss.type = 'button';
|
|
7077
|
+
dismiss.textContent = 'Skip';
|
|
7078
|
+
dismiss.addEventListener('click', () => bar.remove());
|
|
7079
|
+
const autoDismiss = document.createElement('span');
|
|
7080
|
+
autoDismiss.className = 'ctx-push-auto';
|
|
7081
|
+
autoDismiss.textContent = 'Auto-dismisses in 30s';
|
|
7082
|
+
bar.appendChild(msg);
|
|
7083
|
+
bar.appendChild(btn);
|
|
7084
|
+
bar.appendChild(dismiss);
|
|
7085
|
+
bar.appendChild(autoDismiss);
|
|
7086
|
+
if (container) container.appendChild(bar);
|
|
7087
|
+
setTimeout(() => { if (bar.isConnected) bar.remove(); }, 30000);
|
|
7088
|
+
}
|
|
7089
|
+
|
|
5905
7090
|
function tfContextKind(key) {
|
|
5906
7091
|
return (key === 'orgRules' || key === 'managerRules' || key === 'projectRules') ? 'rules' : 'context';
|
|
5907
7092
|
}
|
|
@@ -6277,6 +7462,7 @@ function tfContextRow(key, label, placeholder, rerender) {
|
|
|
6277
7462
|
await tfSaveContext(key, tfContextContentForSave(key, label, readValue()));
|
|
6278
7463
|
renderView();
|
|
6279
7464
|
if (typeof rerender === 'function') rerender();
|
|
7465
|
+
tfMaybeShowPushPrompt(key, row);
|
|
6280
7466
|
} catch (e) {
|
|
6281
7467
|
save.disabled = false;
|
|
6282
7468
|
save.textContent = 'Save';
|
|
@@ -6424,6 +7610,125 @@ function tfProjectUpdatesSection() {
|
|
|
6424
7610
|
return details;
|
|
6425
7611
|
}
|
|
6426
7612
|
|
|
7613
|
+
// #594 R2: render a conversation panel (topline + messages + coach input) into an
|
|
7614
|
+
// area-level container so org/manager jobs are viewable inside Company/Manager tabs.
|
|
7615
|
+
function tfRenderAreaConvPanel(el, conv) {
|
|
7616
|
+
const prevTaVal = (el.querySelector('.area-conv-coach-ta') || {}).value || '';
|
|
7617
|
+
el.innerHTML = '';
|
|
7618
|
+
const topline = document.createElement('div');
|
|
7619
|
+
topline.className = 'conv-topline area-conv-topline';
|
|
7620
|
+
const titleEl = document.createElement('span');
|
|
7621
|
+
titleEl.className = 'conv-job-title';
|
|
7622
|
+
titleEl.textContent = conv.title || conv.jobTitle || '';
|
|
7623
|
+
const dotCls = conversationStateDotClass(conv);
|
|
7624
|
+
const pill = document.createElement('span');
|
|
7625
|
+
pill.className = 'run-state-pill ' + dotCls;
|
|
7626
|
+
pill.textContent = conversationStateLabel(conv);
|
|
7627
|
+
topline.appendChild(titleEl);
|
|
7628
|
+
topline.appendChild(pill);
|
|
7629
|
+
el.appendChild(topline);
|
|
7630
|
+
const thread = document.createElement('div');
|
|
7631
|
+
thread.className = 'area-conv-thread';
|
|
7632
|
+
const msgs = (conv.messages || []).filter((m) => m.role !== 'system');
|
|
7633
|
+
if (msgs.length === 0) {
|
|
7634
|
+
const empty = document.createElement('p');
|
|
7635
|
+
empty.className = 'area-conv-empty';
|
|
7636
|
+
empty.textContent = conv.status === 'running' ? 'Working…' : 'No messages yet.';
|
|
7637
|
+
thread.appendChild(empty);
|
|
7638
|
+
} else {
|
|
7639
|
+
for (const msg of msgs) {
|
|
7640
|
+
const row = document.createElement('div');
|
|
7641
|
+
row.className = 'area-conv-msg area-conv-msg--' + (msg.role || 'employee');
|
|
7642
|
+
const txt = document.createElement('p');
|
|
7643
|
+
txt.className = 'area-conv-msg-text';
|
|
7644
|
+
txt.textContent = msg.text || '';
|
|
7645
|
+
row.appendChild(txt);
|
|
7646
|
+
thread.appendChild(row);
|
|
7647
|
+
}
|
|
7648
|
+
}
|
|
7649
|
+
el.appendChild(thread);
|
|
7650
|
+
// Show the coach input while the job is active — covers 'running' (working)
|
|
7651
|
+
// and 'waiting' uiState (review gate: employee posted and needs manager reply).
|
|
7652
|
+
const uiSt = conversationUiState(conv);
|
|
7653
|
+
const canCoach = uiSt === 'working' || uiSt === 'waiting';
|
|
7654
|
+
if (canCoach) {
|
|
7655
|
+
const coach = document.createElement('div');
|
|
7656
|
+
coach.className = 'area-conv-coach';
|
|
7657
|
+
const ta = document.createElement('textarea');
|
|
7658
|
+
ta.className = 'area-conv-coach-ta';
|
|
7659
|
+
ta.placeholder = 'Coach the team…';
|
|
7660
|
+
ta.rows = 2;
|
|
7661
|
+
if (prevTaVal) ta.value = prevTaVal;
|
|
7662
|
+
const sendBtn = document.createElement('button');
|
|
7663
|
+
sendBtn.type = 'button';
|
|
7664
|
+
sendBtn.className = 'area-conv-coach-send';
|
|
7665
|
+
sendBtn.textContent = 'Send';
|
|
7666
|
+
sendBtn.addEventListener('click', () => {
|
|
7667
|
+
const val = ta.value.trim();
|
|
7668
|
+
if (!val) return;
|
|
7669
|
+
state.activeId = conv.id;
|
|
7670
|
+
continueRun(val);
|
|
7671
|
+
ta.value = '';
|
|
7672
|
+
});
|
|
7673
|
+
coach.appendChild(ta);
|
|
7674
|
+
coach.appendChild(sendBtn);
|
|
7675
|
+
el.appendChild(coach);
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
|
|
7679
|
+
// #594 R2: return the most-relevant org-scoped conversation to show in the Company area.
|
|
7680
|
+
// state.conversations is { [projectPath]: ConversationSummary[] } — flatten all.
|
|
7681
|
+
function tfActiveOrgConv() {
|
|
7682
|
+
const orgJobs = new Set(['organization-onboarding', 'organizational-learning-synthesis']);
|
|
7683
|
+
const convs = Object.values(state.conversations || {}).flat().filter((c) => orgJobs.has(c.jobId));
|
|
7684
|
+
// Prefer running > waiting (needs coaching) > most recently updated completed.
|
|
7685
|
+
return (
|
|
7686
|
+
convs.find((c) => c.status === 'running') ||
|
|
7687
|
+
convs.find((c) => c.status === 'waiting') ||
|
|
7688
|
+
convs.slice().sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0))[0] ||
|
|
7689
|
+
null
|
|
7690
|
+
);
|
|
7691
|
+
}
|
|
7692
|
+
|
|
7693
|
+
// #594 R2: return the org-scoped manager-agreements conversation to show in Manager area.
|
|
7694
|
+
function tfActiveMgrConv() {
|
|
7695
|
+
const mgrJobs = new Set(['manager-agreements']);
|
|
7696
|
+
const convs = Object.values(state.conversations || {}).flat().filter((c) => mgrJobs.has(c.jobId));
|
|
7697
|
+
return (
|
|
7698
|
+
convs.find((c) => c.status === 'running') ||
|
|
7699
|
+
convs.find((c) => c.status === 'waiting') ||
|
|
7700
|
+
convs.slice().sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0))[0] ||
|
|
7701
|
+
null
|
|
7702
|
+
);
|
|
7703
|
+
}
|
|
7704
|
+
|
|
7705
|
+
// Move the shared .page conversation panel into the specified area's workspace-conv host.
|
|
7706
|
+
// All .workspace-conv CSS rules (header/rail hidden, conv fills space) apply automatically
|
|
7707
|
+
// because the area-conv-host elements carry the workspace-conv class.
|
|
7708
|
+
function tfEnsurePageInArea(area) {
|
|
7709
|
+
const page = document.getElementById('hub-conv-page');
|
|
7710
|
+
if (!page) return;
|
|
7711
|
+
const target = area === 'projects'
|
|
7712
|
+
? document.querySelector('#proj-workspace .workspace-conv')
|
|
7713
|
+
: document.getElementById(area + '-conv-host');
|
|
7714
|
+
if (target && page.parentElement !== target) {
|
|
7715
|
+
target.appendChild(page);
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
|
|
7719
|
+
// Toggle a Company/Manager area between its info view (accordions) and conversation panel.
|
|
7720
|
+
function tfToggleAreaView(area, view) {
|
|
7721
|
+
const host = document.getElementById(area + '-conv-host');
|
|
7722
|
+
const info = document.getElementById(area + '-info-view');
|
|
7723
|
+
if (view === 'info') {
|
|
7724
|
+
if (host) host.hidden = true;
|
|
7725
|
+
if (info) info.hidden = false;
|
|
7726
|
+
tfEnsurePageInArea('projects');
|
|
7727
|
+
state.activeId = null;
|
|
7728
|
+
renderActive();
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
|
|
6427
7732
|
function tfRenderCompany() {
|
|
6428
7733
|
// #521 R4/R6: company-level jobs live ONLY in this left rail (organization-onboarding
|
|
6429
7734
|
// + organizational-learning-synthesis) — not in any project's job list, not as
|
|
@@ -6431,6 +7736,13 @@ function tfRenderCompany() {
|
|
|
6431
7736
|
const rail = document.getElementById('company-rail');
|
|
6432
7737
|
if (rail) {
|
|
6433
7738
|
rail.innerHTML = '';
|
|
7739
|
+
const orgConvActive = !!tfActiveOrgConv();
|
|
7740
|
+
const infoBtn = document.createElement('button');
|
|
7741
|
+
infoBtn.type = 'button';
|
|
7742
|
+
infoBtn.className = 'area-rail-info-btn' + (orgConvActive ? '' : ' info-active');
|
|
7743
|
+
infoBtn.textContent = '📋 Company Info';
|
|
7744
|
+
infoBtn.addEventListener('click', () => tfToggleAreaView('company', 'info'));
|
|
7745
|
+
rail.appendChild(infoBtn);
|
|
6434
7746
|
const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Company jobs';
|
|
6435
7747
|
rail.appendChild(head);
|
|
6436
7748
|
rail.appendChild(tfAreaRailJob('🏢', 'organization-onboarding', 'Organization onboarding', 'Set up / update org context & rules', () => tfStartOrgOnboarding()));
|
|
@@ -6443,32 +7755,145 @@ function tfRenderCompany() {
|
|
|
6443
7755
|
const learn = document.getElementById('company-learnings');
|
|
6444
7756
|
if (profile) {
|
|
6445
7757
|
profile.innerHTML = '';
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
'
|
|
6455
|
-
'
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
7758
|
+
const orgCache = tfCtxCache()['org'];
|
|
7759
|
+
if (orgCache && !orgCache.present) {
|
|
7760
|
+
// Context is confirmed absent: show CTA and open the accordion so it is
|
|
7761
|
+
// immediately visible without requiring a manual expand.
|
|
7762
|
+
const ctxAcc = document.getElementById('company-ctx-acc');
|
|
7763
|
+
if (ctxAcc) ctxAcc.open = true;
|
|
7764
|
+
const cta = document.createElement('div');
|
|
7765
|
+
cta.className = 'ctx-cta';
|
|
7766
|
+
const ico = document.createElement('div');
|
|
7767
|
+
ico.className = 'ctx-cta-ico';
|
|
7768
|
+
ico.textContent = '🏢';
|
|
7769
|
+
const title = document.createElement('p');
|
|
7770
|
+
title.className = 'ctx-cta-title';
|
|
7771
|
+
title.textContent = "Your team hasn't been onboarded yet";
|
|
7772
|
+
const msg = document.createElement('p');
|
|
7773
|
+
msg.className = 'ctx-cta-msg';
|
|
7774
|
+
msg.textContent = "Run organization onboarding to teach your employees what you do, how you operate, and the rules they must follow on every job.";
|
|
7775
|
+
const btn = document.createElement('button');
|
|
7776
|
+
btn.className = 'ctx-cta-btn';
|
|
7777
|
+
btn.type = 'button';
|
|
7778
|
+
btn.textContent = 'Run organization onboarding →';
|
|
7779
|
+
btn.addEventListener('click', () => tfStartOrgOnboarding());
|
|
7780
|
+
cta.appendChild(ico);
|
|
7781
|
+
cta.appendChild(title);
|
|
7782
|
+
cta.appendChild(msg);
|
|
7783
|
+
cta.appendChild(btn);
|
|
7784
|
+
profile.appendChild(cta);
|
|
7785
|
+
} else {
|
|
7786
|
+
// Context is present or not yet fetched: render rows (they load themselves).
|
|
7787
|
+
// If not yet fetched, start a background fetch and re-render to possibly switch to CTA.
|
|
7788
|
+
profile.appendChild(tfContextRow(
|
|
7789
|
+
'org',
|
|
7790
|
+
'What you do',
|
|
7791
|
+
'Not set up yet — run onboarding to teach your team about your organization, or click Edit to write it directly.',
|
|
7792
|
+
tfRenderCompany
|
|
7793
|
+
));
|
|
7794
|
+
profile.appendChild(tfContextRow(
|
|
7795
|
+
'orgRules',
|
|
7796
|
+
'Guardrails',
|
|
7797
|
+
'Org-wide rules every employee respects before starting any job. Click Edit to add them.',
|
|
7798
|
+
tfRenderCompany
|
|
7799
|
+
));
|
|
7800
|
+
if (!orgCache) {
|
|
7801
|
+
tfFetchContext('org').then(() => tfRenderCompany()).catch(() => {});
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
6460
7804
|
}
|
|
6461
7805
|
if (learn) {
|
|
6462
7806
|
tfRenderLearningsList(learn, 'org', 'org', 'Nothing here yet — org-wide learnings that apply on every project. Run organizational-learning-synthesis (in Company jobs, left), or add one with + Add learning.');
|
|
6463
7807
|
}
|
|
6464
|
-
|
|
6465
|
-
|
|
7808
|
+
tfMaybeRenderOrgPushBanner();
|
|
7809
|
+
// Show the full coaching conversation panel when there is an active org conv;
|
|
7810
|
+
// otherwise show the info view (accordions). This uses the shared .page element
|
|
7811
|
+
// moved via tfEnsurePageInArea() so renderActive() renders the full panel.
|
|
7812
|
+
const orgConv = tfActiveOrgConv();
|
|
7813
|
+
const host = document.getElementById('company-conv-host');
|
|
7814
|
+
const info = document.getElementById('company-info-view');
|
|
7815
|
+
if (orgConv && tf.area === 'company') {
|
|
7816
|
+
state.activeId = orgConv.id;
|
|
7817
|
+
tfEnsurePageInArea('company');
|
|
7818
|
+
if (host) host.hidden = false;
|
|
7819
|
+
if (info) info.hidden = true;
|
|
7820
|
+
renderActive();
|
|
7821
|
+
} else {
|
|
7822
|
+
if (host) host.hidden = true;
|
|
7823
|
+
if (info) info.hidden = false;
|
|
7824
|
+
if (tf.area === 'company') tfEnsurePageInArea('projects');
|
|
7825
|
+
}
|
|
6466
7826
|
}
|
|
7827
|
+
|
|
7828
|
+
// #594 R3: show push banner in Company area after an org onboarding run completes
|
|
7829
|
+
// and GitHub is configured, so the manager can push the new context to the team repo.
|
|
7830
|
+
function tfMaybeRenderOrgPushBanner() {
|
|
7831
|
+
const host = document.getElementById('company-push-banner');
|
|
7832
|
+
if (!host) return;
|
|
7833
|
+
if (!tfIsGithubConfigured()) { host.innerHTML = ''; return; }
|
|
7834
|
+
const orgJobs = ['organization-onboarding', 'organizational-learning-synthesis'];
|
|
7835
|
+
const hasCompleted = Object.values(state.conversations || {}).flat().some(
|
|
7836
|
+
(c) => orgJobs.includes(c.jobId) && c.status === 'completed'
|
|
7837
|
+
);
|
|
7838
|
+
if (!hasCompleted) { host.innerHTML = ''; return; }
|
|
7839
|
+
const dismissed = window.localStorage.getItem('fraim.aiHub.orgPushDismissed');
|
|
7840
|
+
if (dismissed === '1') { host.innerHTML = ''; return; }
|
|
7841
|
+
if (host.querySelector('.push-to-team-banner')) return;
|
|
7842
|
+
host.innerHTML = '';
|
|
7843
|
+
const banner = document.createElement('div');
|
|
7844
|
+
banner.className = 'push-to-team-banner';
|
|
7845
|
+
banner.id = 'company-push-banner-inner';
|
|
7846
|
+
const ico = document.createElement('span');
|
|
7847
|
+
ico.className = 'ptb-ico';
|
|
7848
|
+
ico.textContent = '📤';
|
|
7849
|
+
const copy = document.createElement('div');
|
|
7850
|
+
copy.className = 'ptb-copy';
|
|
7851
|
+
const repo = state.bootstrap && state.bootstrap.repository;
|
|
7852
|
+
const repoLabel = (repo && repo.owner && repo.name) ? (repo.owner + '/' + repo.name) : 'your team repo';
|
|
7853
|
+
copy.innerHTML = '<strong>Push to team?</strong> Your org context was updated locally. Push to <code>' + repoLabel + '</code> so teammates pick it up on their next pull.';
|
|
7854
|
+
const pushBtn = document.createElement('button');
|
|
7855
|
+
pushBtn.className = 'ptb-push-btn';
|
|
7856
|
+
pushBtn.type = 'button';
|
|
7857
|
+
pushBtn.textContent = 'Push to team';
|
|
7858
|
+
pushBtn.addEventListener('click', async () => {
|
|
7859
|
+
pushBtn.disabled = true;
|
|
7860
|
+
pushBtn.textContent = 'Pushing...';
|
|
7861
|
+
try {
|
|
7862
|
+
await requestJson('/api/ai-hub/context/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scope: 'org' }) });
|
|
7863
|
+
window.localStorage.setItem('fraim.aiHub.orgPushDismissed', '1');
|
|
7864
|
+
host.innerHTML = '';
|
|
7865
|
+
} catch {
|
|
7866
|
+
pushBtn.disabled = false;
|
|
7867
|
+
pushBtn.textContent = 'Push to team';
|
|
7868
|
+
}
|
|
7869
|
+
});
|
|
7870
|
+
const dismissBtn = document.createElement('button');
|
|
7871
|
+
dismissBtn.className = 'ptb-dismiss';
|
|
7872
|
+
dismissBtn.type = 'button';
|
|
7873
|
+
dismissBtn.textContent = 'Keep local';
|
|
7874
|
+
dismissBtn.addEventListener('click', () => {
|
|
7875
|
+
window.localStorage.setItem('fraim.aiHub.orgPushDismissed', '1');
|
|
7876
|
+
host.innerHTML = '';
|
|
7877
|
+
});
|
|
7878
|
+
banner.appendChild(ico);
|
|
7879
|
+
banner.appendChild(copy);
|
|
7880
|
+
banner.appendChild(pushBtn);
|
|
7881
|
+
banner.appendChild(dismissBtn);
|
|
7882
|
+
host.appendChild(banner);
|
|
7883
|
+
}
|
|
7884
|
+
|
|
6467
7885
|
function tfRenderManager() {
|
|
6468
7886
|
// #521 R6: manager-level job (manager-agreements) lives ONLY in this left rail.
|
|
6469
7887
|
const rail = document.getElementById('manager-rail');
|
|
6470
7888
|
if (rail) {
|
|
6471
7889
|
rail.innerHTML = '';
|
|
7890
|
+
const mgrConvActive = !!tfActiveMgrConv();
|
|
7891
|
+
const infoBtn = document.createElement('button');
|
|
7892
|
+
infoBtn.type = 'button';
|
|
7893
|
+
infoBtn.className = 'area-rail-info-btn' + (mgrConvActive ? '' : ' info-active');
|
|
7894
|
+
infoBtn.textContent = '📋 Manager Info';
|
|
7895
|
+
infoBtn.addEventListener('click', () => tfToggleAreaView('manager', 'info'));
|
|
7896
|
+
rail.appendChild(infoBtn);
|
|
6472
7897
|
const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Manager jobs';
|
|
6473
7898
|
rail.appendChild(head);
|
|
6474
7899
|
rail.appendChild(tfAreaRailJob('🤝', 'manager-agreements', 'manager-agreements', 'Set up / update how you work', () => tfStartManagerOnboarding()));
|
|
@@ -6481,21 +7906,51 @@ function tfRenderManager() {
|
|
|
6481
7906
|
const reverse = document.getElementById('reverse-mentoring');
|
|
6482
7907
|
if (profile) {
|
|
6483
7908
|
profile.innerHTML = '';
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
'manager'
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
'
|
|
6494
|
-
'
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
7909
|
+
const mgrCache = tfCtxCache()['manager'];
|
|
7910
|
+
if (mgrCache && !mgrCache.present) {
|
|
7911
|
+
// Context is confirmed absent: show CTA and open the accordion.
|
|
7912
|
+
const mgrCtxAcc = document.getElementById('manager-ctx-acc');
|
|
7913
|
+
if (mgrCtxAcc) mgrCtxAcc.open = true;
|
|
7914
|
+
const cta = document.createElement('div');
|
|
7915
|
+
cta.className = 'ctx-cta';
|
|
7916
|
+
const ico = document.createElement('div');
|
|
7917
|
+
ico.className = 'ctx-cta-ico';
|
|
7918
|
+
ico.textContent = '🤝';
|
|
7919
|
+
const title = document.createElement('p');
|
|
7920
|
+
title.className = 'ctx-cta-title';
|
|
7921
|
+
title.textContent = 'Set up your manager profile';
|
|
7922
|
+
const msg = document.createElement('p');
|
|
7923
|
+
msg.className = 'ctx-cta-msg';
|
|
7924
|
+
msg.textContent = "Run manager onboarding so your team knows how you like to work, give feedback, and delegate. Your preferences become standing rules every employee respects.";
|
|
7925
|
+
const btn = document.createElement('button');
|
|
7926
|
+
btn.className = 'ctx-cta-btn';
|
|
7927
|
+
btn.type = 'button';
|
|
7928
|
+
btn.textContent = 'Run manager onboarding →';
|
|
7929
|
+
btn.addEventListener('click', () => tfStartManagerOnboarding());
|
|
7930
|
+
cta.appendChild(ico);
|
|
7931
|
+
cta.appendChild(title);
|
|
7932
|
+
cta.appendChild(msg);
|
|
7933
|
+
cta.appendChild(btn);
|
|
7934
|
+
profile.appendChild(cta);
|
|
7935
|
+
} else {
|
|
7936
|
+
// Context is present or not yet fetched: render rows (they load themselves).
|
|
7937
|
+
// If not yet fetched, start a background fetch and re-render to possibly switch to CTA.
|
|
7938
|
+
profile.appendChild(tfContextRow(
|
|
7939
|
+
'manager',
|
|
7940
|
+
'How you work',
|
|
7941
|
+
'Not set up yet — run onboarding to teach your team how you like direction, feedback, priorities, and hand-off. Click Edit to write it directly.',
|
|
7942
|
+
tfRenderManager
|
|
7943
|
+
));
|
|
7944
|
+
profile.appendChild(tfContextRow(
|
|
7945
|
+
'managerRules',
|
|
7946
|
+
'Rules',
|
|
7947
|
+
'Standing rules for how employees should work with you. Click Edit to add them.',
|
|
7948
|
+
tfRenderManager
|
|
7949
|
+
));
|
|
7950
|
+
if (!mgrCache) {
|
|
7951
|
+
tfFetchContext('manager').then(() => tfRenderManager()).catch(() => {});
|
|
7952
|
+
}
|
|
7953
|
+
}
|
|
6499
7954
|
}
|
|
6500
7955
|
if (learn) {
|
|
6501
7956
|
// #533: Manager learnings = "how your team adapts to you" = your work patterns
|
|
@@ -6507,42 +7962,25 @@ function tfRenderManager() {
|
|
|
6507
7962
|
// the manager-coaching learnings the team has captured for you.
|
|
6508
7963
|
tfRenderLearningsList(reverse, 'reverse', 'machine', 'Nothing here yet — as your team works with you, coaching on what you can improve on as a manager appears here. Add one with + Add learning.');
|
|
6509
7964
|
}
|
|
6510
|
-
tfRenderManagerTeam();
|
|
6511
7965
|
// Issue #540 R4-R7: render manager team pool on every manager-tab activation.
|
|
6512
7966
|
renderManagerTeamPool();
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
const
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
btn.addEventListener('click', () => tfShowArea('company'));
|
|
6529
|
-
section.appendChild(btn);
|
|
6530
|
-
return;
|
|
7967
|
+
// Show the full coaching conversation panel when there is an active manager conv;
|
|
7968
|
+
// otherwise show the info view (accordions). Uses the shared .page element.
|
|
7969
|
+
const mgrConv = tfActiveMgrConv();
|
|
7970
|
+
const mgrHost = document.getElementById('manager-conv-host');
|
|
7971
|
+
const mgrInfo = document.getElementById('manager-info-view');
|
|
7972
|
+
if (mgrConv && tf.area === 'manager') {
|
|
7973
|
+
state.activeId = mgrConv.id;
|
|
7974
|
+
tfEnsurePageInArea('manager');
|
|
7975
|
+
if (mgrHost) mgrHost.hidden = false;
|
|
7976
|
+
if (mgrInfo) mgrInfo.hidden = true;
|
|
7977
|
+
renderActive();
|
|
7978
|
+
} else {
|
|
7979
|
+
if (mgrHost) mgrHost.hidden = true;
|
|
7980
|
+
if (mgrInfo) mgrInfo.hidden = false;
|
|
7981
|
+
if (tf.area === 'manager') tfEnsurePageInArea('projects');
|
|
6531
7982
|
}
|
|
6532
|
-
const grid = document.createElement('div');
|
|
6533
|
-
grid.className = 'emp-grid';
|
|
6534
|
-
hired.forEach((p, i) => {
|
|
6535
|
-
const av = tfAvatarFor(p.displayName, i);
|
|
6536
|
-
const tile = document.createElement('div');
|
|
6537
|
-
tile.className = 'emp-tile';
|
|
6538
|
-
tile.innerHTML = '<div class="et-av" style="background:' + av.color + ';">' + av.badge + '</div>' +
|
|
6539
|
-
'<div class="et-name">' + tfEscape(p.displayName) + '</div>' +
|
|
6540
|
-
'<div class="et-role">' + tfEscape(p.role || '') + '</div>';
|
|
6541
|
-
grid.appendChild(tile);
|
|
6542
|
-
});
|
|
6543
|
-
section.appendChild(grid);
|
|
6544
7983
|
}
|
|
6545
|
-
|
|
6546
7984
|
// #533: Reverse mentoring now renders the real manager-coaching learnings via
|
|
6547
7985
|
// tfRenderLearningsList(scope='reverse') — consistent with the other learning
|
|
6548
7986
|
// sections — replacing the earlier representative-seed feed (tfReverseItems /
|
|
@@ -6674,7 +8112,7 @@ async function tfFetchPreservedLearnings(scope, level) {
|
|
|
6674
8112
|
// cross-surface render churn that left the project tree unstable. #533)
|
|
6675
8113
|
function tfReRenderScope(scope, level) {
|
|
6676
8114
|
if (level === 'project') {
|
|
6677
|
-
if (document.querySelector('.workspace-conv') && typeof tfRenderProjectContextTop === 'function') tfRenderProjectContextTop();
|
|
8115
|
+
if (document.querySelector('#proj-workspace .workspace-conv') && typeof tfRenderProjectContextTop === 'function') tfRenderProjectContextTop();
|
|
6678
8116
|
return;
|
|
6679
8117
|
}
|
|
6680
8118
|
if (scope === 'org') { if (typeof tfRenderCompany === 'function') tfRenderCompany(); return; }
|
|
@@ -7032,76 +8470,84 @@ function tfCloseAccountMenu() {
|
|
|
7032
8470
|
if (menu) menu.classList.remove('open');
|
|
7033
8471
|
}
|
|
7034
8472
|
|
|
7035
|
-
//
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
rail.hidden = collapsed;
|
|
7070
|
-
pill.hidden = !collapsed;
|
|
8473
|
+
// Per-job config for the area onboarding pre-flight modal.
|
|
8474
|
+
const AREA_ONBOARD_CONFIG = {
|
|
8475
|
+
'organization-onboarding': {
|
|
8476
|
+
title: 'Organization onboarding',
|
|
8477
|
+
desc: 'FRAIM will learn what your company does and write org_context.md and org_rules.md. Add any specific focus areas or context below.',
|
|
8478
|
+
},
|
|
8479
|
+
'organizational-learning-synthesis': {
|
|
8480
|
+
title: 'Learning synthesis',
|
|
8481
|
+
desc: 'FRAIM will consolidate recurring patterns across projects into org-wide learnings. Add any specific focus areas below.',
|
|
8482
|
+
},
|
|
8483
|
+
'manager-agreements': {
|
|
8484
|
+
title: 'Manager agreements',
|
|
8485
|
+
desc: 'FRAIM will learn how you work and write manager_context.md and manager_rules.md. Add any specific direction below.',
|
|
8486
|
+
},
|
|
8487
|
+
};
|
|
8488
|
+
|
|
8489
|
+
function tfOpenAreaOnboardModal(jobId, baseMessage, area) {
|
|
8490
|
+
tf.aomJob = { jobId, baseMessage, area };
|
|
8491
|
+
const cfg = AREA_ONBOARD_CONFIG[jobId] || { title: jobId, desc: 'Add any specific context for this run.' };
|
|
8492
|
+
const titleEl = document.getElementById('aom-title');
|
|
8493
|
+
const descEl = document.getElementById('aom-desc');
|
|
8494
|
+
const ctx = document.getElementById('aom-context');
|
|
8495
|
+
if (titleEl) titleEl.textContent = cfg.title;
|
|
8496
|
+
if (descEl) descEl.textContent = cfg.desc;
|
|
8497
|
+
if (ctx) ctx.value = '';
|
|
8498
|
+
const modal = document.getElementById('area-onboard-modal');
|
|
8499
|
+
if (modal) modal.hidden = false;
|
|
8500
|
+
if (ctx) ctx.focus();
|
|
8501
|
+
}
|
|
8502
|
+
|
|
8503
|
+
function tfCloseAreaOnboardModal() {
|
|
8504
|
+
const modal = document.getElementById('area-onboard-modal');
|
|
8505
|
+
if (modal) modal.hidden = true;
|
|
8506
|
+
tf.aomJob = null;
|
|
7071
8507
|
}
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
if (
|
|
8508
|
+
|
|
8509
|
+
async function tfSubmitAreaOnboardModal() {
|
|
8510
|
+
const { jobId, baseMessage, area } = tf.aomJob || {};
|
|
8511
|
+
if (!jobId) return;
|
|
8512
|
+
const ctx = document.getElementById('aom-context');
|
|
8513
|
+
const userContext = (ctx && ctx.value.trim()) || '';
|
|
8514
|
+
tfCloseAreaOnboardModal();
|
|
8515
|
+
await tfStartOnboardingJob(jobId, baseMessage, area, userContext);
|
|
7076
8516
|
}
|
|
7077
8517
|
|
|
7078
|
-
async function tfStartOnboardingJob(jobId, message, targetArea) {
|
|
8518
|
+
async function tfStartOnboardingJob(jobId, message, targetArea, userContext) {
|
|
7079
8519
|
const employeeId = state.selectedEmployeeId || 'claude';
|
|
7080
8520
|
const allJobs = [
|
|
7081
8521
|
...((state.bootstrap && state.bootstrap.jobs) || []),
|
|
7082
8522
|
...((state.bootstrap && state.bootstrap.managerTemplates) || []),
|
|
7083
8523
|
];
|
|
7084
8524
|
const job = allJobs.find((j) => j.id === jobId) || { id: jobId, title: jobId };
|
|
8525
|
+
// If the user typed a direction in the pre-flight modal, send only that — the
|
|
8526
|
+
// job already knows what to do from its FRAIM instructions. Fall back to the
|
|
8527
|
+
// job's registry intent (or the base message string) when no context is given.
|
|
8528
|
+
const jobMessage = (job && job.intent && job.intent.trim()) ? job.intent : message;
|
|
8529
|
+
const finalMessage = (userContext && userContext.trim()) ? userContext.trim() : jobMessage;
|
|
7085
8530
|
if (job && typeof startRun === 'function') {
|
|
7086
8531
|
if (targetArea) tfShowArea(targetArea);
|
|
7087
|
-
await startRun(job,
|
|
7088
|
-
|
|
7089
|
-
|
|
8532
|
+
await startRun(job, finalMessage, employeeId);
|
|
8533
|
+
// After run creation refresh the area panel so the conversation becomes visible.
|
|
8534
|
+
if (targetArea === 'company') tfRenderCompany();
|
|
8535
|
+
else if (targetArea === 'manager') tfRenderManager();
|
|
7090
8536
|
} else if (typeof showStatus === 'function') {
|
|
7091
8537
|
showStatus('Onboarding job not found in this project yet. Pick a project folder first.', true);
|
|
7092
8538
|
}
|
|
7093
8539
|
}
|
|
7094
8540
|
|
|
7095
|
-
|
|
7096
|
-
|
|
8541
|
+
function tfStartOrgOnboarding() {
|
|
8542
|
+
tfOpenAreaOnboardModal(
|
|
7097
8543
|
'organization-onboarding',
|
|
7098
8544
|
'Onboard my organization: learn what we do and the guardrails every employee should follow, then write org_context.md and org_rules.md.',
|
|
7099
8545
|
'company'
|
|
7100
8546
|
);
|
|
7101
8547
|
}
|
|
7102
8548
|
|
|
7103
|
-
|
|
7104
|
-
|
|
8549
|
+
function tfStartManagerOnboarding() {
|
|
8550
|
+
tfOpenAreaOnboardModal(
|
|
7105
8551
|
'manager-agreements',
|
|
7106
8552
|
'Set up my manager agreements: learn how I like to work, how I give feedback, and the standing rules employees should follow with me, then write manager_context.md and manager_rules.md.',
|
|
7107
8553
|
'manager'
|
|
@@ -7110,8 +8556,8 @@ async function tfStartManagerOnboarding() {
|
|
|
7110
8556
|
|
|
7111
8557
|
// #521 R4: company-level learning synthesis — wired to the real
|
|
7112
8558
|
// organizational-learning-synthesis job, launched only from the Company rail.
|
|
7113
|
-
|
|
7114
|
-
|
|
8559
|
+
function tfStartOrgLearningSynthesis() {
|
|
8560
|
+
tfOpenAreaOnboardModal(
|
|
7115
8561
|
'organizational-learning-synthesis',
|
|
7116
8562
|
'Synthesize our company learnings: consolidate the recurring patterns across projects into preserved org-wide learnings, then update org memory.',
|
|
7117
8563
|
'company'
|
|
@@ -7137,36 +8583,59 @@ async function tfStartProjectOnboarding(userInput) {
|
|
|
7137
8583
|
await tfStartOnboardingJob('project-onboarding', parts.join('\n\n'), 'projects');
|
|
7138
8584
|
}
|
|
7139
8585
|
|
|
7140
|
-
// #
|
|
7141
|
-
//
|
|
7142
|
-
//
|
|
7143
|
-
function tfOpenOnboardInput(
|
|
7144
|
-
|
|
7145
|
-
const title = document.getElementById('obi-title');
|
|
7146
|
-
const sub = document.getElementById('obi-sub');
|
|
7147
|
-
const input = document.getElementById('obi-input');
|
|
7148
|
-
if (!modal) { tfStartProjectOnboarding(); return; }
|
|
7149
|
-
const reprocess = mode === 'reprocess';
|
|
7150
|
-
if (title) title.textContent = reprocess ? 'Reprocess project onboarding' : 'Run project onboarding';
|
|
7151
|
-
if (sub) {
|
|
7152
|
-
sub.textContent = reprocess
|
|
7153
|
-
? "Your team re-reads the current context & rules (including your edits) and refines them. Add anything you want them to focus on or incorporate — optional."
|
|
7154
|
-
: "Your team explores the project and captures its context and rules. Tell them what this project is or what to focus on — optional.";
|
|
7155
|
-
}
|
|
7156
|
-
if (input) input.value = '';
|
|
7157
|
-
modal.hidden = false;
|
|
7158
|
-
if (input) input.focus();
|
|
8586
|
+
// #594 R7: #onboard-input-modal retired. tfOpenOnboardInput now opens #np-modal
|
|
8587
|
+
// with the folder pre-filled and locked so the manager can only supply a direction
|
|
8588
|
+
// message (the intent field) before starting the re-run.
|
|
8589
|
+
function tfOpenOnboardInput() {
|
|
8590
|
+
tfOpenNewProjectRerun();
|
|
7159
8591
|
}
|
|
7160
8592
|
function tfCloseOnboardInput() {
|
|
7161
|
-
|
|
7162
|
-
if (m) m.hidden = true;
|
|
8593
|
+
tfCloseNewProject();
|
|
7163
8594
|
}
|
|
7164
8595
|
function tfSubmitOnboardInput() {
|
|
7165
|
-
const
|
|
7166
|
-
const val =
|
|
7167
|
-
|
|
8596
|
+
const intent = document.getElementById('np-intent');
|
|
8597
|
+
const val = intent ? intent.value : '';
|
|
8598
|
+
tfCloseNewProject();
|
|
7168
8599
|
tfStartProjectOnboarding(val);
|
|
7169
8600
|
}
|
|
8601
|
+
function tfOpenNewProjectRerun() {
|
|
8602
|
+
const modal = document.getElementById('np-modal');
|
|
8603
|
+
if (!modal) { tfStartProjectOnboarding(); return; }
|
|
8604
|
+
const folder = document.getElementById('np-folder');
|
|
8605
|
+
const np1Next = document.getElementById('np1-next');
|
|
8606
|
+
const intent = document.getElementById('np-intent');
|
|
8607
|
+
const browseBtn = document.getElementById('np-browse');
|
|
8608
|
+
if (folder) {
|
|
8609
|
+
folder.value = state.projectPath || '';
|
|
8610
|
+
folder.readOnly = true;
|
|
8611
|
+
}
|
|
8612
|
+
if (browseBtn) browseBtn.hidden = true;
|
|
8613
|
+
if (intent) {
|
|
8614
|
+
intent.value = '';
|
|
8615
|
+
const label = intent.closest('.np-field') && intent.closest('.np-field').querySelector('label');
|
|
8616
|
+
if (label) label.dataset.originalText = label.innerHTML;
|
|
8617
|
+
if (label) label.innerHTML = 'Anything new to focus on this time? <span class="np-optional">(optional)</span>';
|
|
8618
|
+
}
|
|
8619
|
+
if (np1Next) { np1Next.disabled = false; np1Next.removeAttribute('disabled'); }
|
|
8620
|
+
// Update Step 1 heading and description for re-run context.
|
|
8621
|
+
const np1Hdr = document.querySelector('#np1 .np-hdr');
|
|
8622
|
+
if (np1Hdr) {
|
|
8623
|
+
const h2 = np1Hdr.querySelector('h2');
|
|
8624
|
+
const desc = np1Hdr.querySelector('p');
|
|
8625
|
+
const stepLabel = np1Hdr.querySelector('.step-label');
|
|
8626
|
+
if (h2) { h2.dataset.originalText = h2.textContent; h2.textContent = 'Update project onboarding'; }
|
|
8627
|
+
if (desc) { desc.dataset.originalText = desc.textContent; desc.textContent = 'Your project folder is already set. Tell the team anything new to focus on this time.'; }
|
|
8628
|
+
if (stepLabel) { stepLabel.dataset.originalText = stepLabel.textContent; stepLabel.textContent = 'Step 1 of 4 — Re-run'; }
|
|
8629
|
+
}
|
|
8630
|
+
const np1Hint = document.querySelector('#np1 .np-field-hint');
|
|
8631
|
+
if (np1Hint) { np1Hint.dataset.originalText = np1Hint.textContent; np1Hint.textContent = 'This project folder is already set — only the direction below will change.'; }
|
|
8632
|
+
modal.dataset.rerun = '1';
|
|
8633
|
+
// Pre-select all hired personas so step 2 starts with a sensible default.
|
|
8634
|
+
tf.npSelectedEmployees = tfHiredPersonas().map((p) => p.key);
|
|
8635
|
+
tfRenderNpEmployees();
|
|
8636
|
+
tfNpGo(1);
|
|
8637
|
+
modal.hidden = false;
|
|
8638
|
+
}
|
|
7170
8639
|
|
|
7171
8640
|
// ---------------------------------------------------------------------------
|
|
7172
8641
|
// New-project modal (4-step)
|
|
@@ -7187,7 +8656,26 @@ function tfOpenNewProject() {
|
|
|
7187
8656
|
}
|
|
7188
8657
|
function tfCloseNewProject() {
|
|
7189
8658
|
const m = document.getElementById('np-modal');
|
|
7190
|
-
if (m) m.hidden = true;
|
|
8659
|
+
if (m) { m.hidden = true; delete m.dataset.rerun; }
|
|
8660
|
+
const folder = document.getElementById('np-folder');
|
|
8661
|
+
if (folder) folder.readOnly = false;
|
|
8662
|
+
const browseBtn = document.getElementById('np-browse');
|
|
8663
|
+
if (browseBtn) browseBtn.hidden = false;
|
|
8664
|
+
const intent = document.getElementById('np-intent');
|
|
8665
|
+
if (intent) {
|
|
8666
|
+
const label = intent.closest('.np-field') && intent.closest('.np-field').querySelector('label');
|
|
8667
|
+
if (label && label.dataset.originalText) { label.innerHTML = label.dataset.originalText; delete label.dataset.originalText; }
|
|
8668
|
+
}
|
|
8669
|
+
// Restore Step 1 heading, description, step-label, and hint to new-project defaults.
|
|
8670
|
+
const np1Hdr = document.querySelector('#np1 .np-hdr');
|
|
8671
|
+
if (np1Hdr) {
|
|
8672
|
+
for (const sel of ['h2', 'p', '.step-label']) {
|
|
8673
|
+
const el = np1Hdr.querySelector(sel);
|
|
8674
|
+
if (el && el.dataset.originalText) { el.textContent = el.dataset.originalText; delete el.dataset.originalText; }
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
const np1Hint = document.querySelector('#np1 .np-field-hint');
|
|
8678
|
+
if (np1Hint && np1Hint.dataset.originalText) { np1Hint.textContent = np1Hint.dataset.originalText; delete np1Hint.dataset.originalText; }
|
|
7191
8679
|
}
|
|
7192
8680
|
function tfNpGo(n) {
|
|
7193
8681
|
tf.npStep = n;
|
|
@@ -7642,16 +9130,6 @@ function tfWireShell() {
|
|
|
7642
9130
|
if (overviewTab) overviewTab.addEventListener('click', () => tfSelectProjectView('overview'));
|
|
7643
9131
|
const addTab = document.getElementById('ptab-add');
|
|
7644
9132
|
if (addTab) addTab.addEventListener('click', tfOpenNewProject);
|
|
7645
|
-
const cta = document.getElementById('gs-cta');
|
|
7646
|
-
if (cta) cta.addEventListener('click', () => {
|
|
7647
|
-
const fr = state.bootstrap && state.bootstrap.firstRun;
|
|
7648
|
-
const active = fr && tfFirstRunActiveStep(fr);
|
|
7649
|
-
if (active) tfRailGo(active);
|
|
7650
|
-
});
|
|
7651
|
-
const hide = document.getElementById('gs-hide');
|
|
7652
|
-
if (hide) hide.addEventListener('click', () => { window.localStorage.setItem(STORAGE_KEY_RAIL_512, '1'); tfRenderRail(); });
|
|
7653
|
-
const pill = document.getElementById('gs-pill');
|
|
7654
|
-
if (pill) pill.addEventListener('click', () => { window.localStorage.removeItem(STORAGE_KEY_RAIL_512); tfRenderRail(); });
|
|
7655
9133
|
const npClose = document.getElementById('np-close');
|
|
7656
9134
|
if (npClose) npClose.addEventListener('click', tfCloseNewProject);
|
|
7657
9135
|
|
|
@@ -7689,7 +9167,16 @@ function tfWireShell() {
|
|
|
7689
9167
|
}
|
|
7690
9168
|
const npModal = document.getElementById('np-modal');
|
|
7691
9169
|
if (npModal) npModal.addEventListener('click', (e) => { if (e.target === npModal) tfCloseNewProject(); });
|
|
7692
|
-
|
|
9170
|
+
// #594: Area onboarding pre-flight modal wiring.
|
|
9171
|
+
const aomClose = document.getElementById('aom-close');
|
|
9172
|
+
if (aomClose) aomClose.addEventListener('click', tfCloseAreaOnboardModal);
|
|
9173
|
+
const aomCancel = document.getElementById('aom-cancel');
|
|
9174
|
+
if (aomCancel) aomCancel.addEventListener('click', tfCloseAreaOnboardModal);
|
|
9175
|
+
const aomStart = document.getElementById('aom-start');
|
|
9176
|
+
if (aomStart) aomStart.addEventListener('click', tfSubmitAreaOnboardModal);
|
|
9177
|
+
const aomModal = document.getElementById('area-onboard-modal');
|
|
9178
|
+
if (aomModal) aomModal.addEventListener('click', (e) => { if (e.target === aomModal) tfCloseAreaOnboardModal(); });
|
|
9179
|
+
const closers = [['aj-close', tfCloseAssignJob], ['ae-close', tfCloseAddEmp], ['pr-close', tfClosePricing], ['hm-close', tfCloseHireManager]];
|
|
7693
9180
|
for (const [id, fn] of closers) {
|
|
7694
9181
|
const el = document.getElementById(id);
|
|
7695
9182
|
if (el) el.addEventListener('click', fn);
|
|
@@ -7715,16 +9202,7 @@ function tfWireShell() {
|
|
|
7715
9202
|
const q = document.getElementById('hm-query');
|
|
7716
9203
|
if (q && navigator.clipboard) navigator.clipboard.writeText(q.textContent || '');
|
|
7717
9204
|
});
|
|
7718
|
-
const
|
|
7719
|
-
if (hmBtn) hmBtn.addEventListener('click', () => {
|
|
7720
|
-
const mh = state.bootstrap && state.bootstrap.managerHiring;
|
|
7721
|
-
const hired = ((state.bootstrap && state.bootstrap.personas) || []).filter((p) => p.status === 'hired' && mh && mh.roles[p.key]);
|
|
7722
|
-
const first = hired[0] ? hired[0].key : (mh ? Object.keys(mh.roles)[0] : null);
|
|
7723
|
-
openHireManagerModal(first);
|
|
7724
|
-
});
|
|
7725
|
-
const obiStart = document.getElementById('obi-start');
|
|
7726
|
-
if (obiStart) obiStart.addEventListener('click', tfSubmitOnboardInput);
|
|
7727
|
-
const backdrops = [['assign-job-modal', tfCloseAssignJob], ['add-emp-modal', tfCloseAddEmp], ['pricing-modal', tfClosePricing], ['onboard-input-modal', tfCloseOnboardInput], ['hm-modal', tfCloseHireManager]];
|
|
9205
|
+
const backdrops = [['assign-job-modal', tfCloseAssignJob], ['add-emp-modal', tfCloseAddEmp], ['pricing-modal', tfClosePricing], ['hm-modal', tfCloseHireManager]];
|
|
7728
9206
|
for (const [id, fn] of backdrops) {
|
|
7729
9207
|
const el = document.getElementById(id);
|
|
7730
9208
|
if (el) el.addEventListener('click', (e) => { if (e.target === el) fn(); });
|
|
@@ -7735,7 +9213,6 @@ function tfWireShell() {
|
|
|
7735
9213
|
// dismisses whichever team-flow modal is open (top-most first).
|
|
7736
9214
|
const escClosers = [
|
|
7737
9215
|
['hm-modal', tfCloseHireManager],
|
|
7738
|
-
['onboard-input-modal', tfCloseOnboardInput],
|
|
7739
9216
|
['pricing-modal', tfClosePricing],
|
|
7740
9217
|
['add-emp-modal', tfCloseAddEmp],
|
|
7741
9218
|
['assign-job-modal', tfCloseAssignJob],
|
|
@@ -7804,9 +9281,10 @@ function tfInitShell() {
|
|
|
7804
9281
|
} else if (tf.projects.length && !tf.activeProjectId) {
|
|
7805
9282
|
tf.activeProjectId = tf.projects[0].id;
|
|
7806
9283
|
}
|
|
7807
|
-
tfRenderRail();
|
|
7808
9284
|
tfRenderProjectTabs();
|
|
7809
9285
|
tfShowArea('projects');
|
|
9286
|
+
// Issue #578: wire deployment modal buttons.
|
|
9287
|
+
initDeploymentButtons();
|
|
7810
9288
|
// Open the workspace by default so the conversation surface (#conversation,
|
|
7811
9289
|
// #new-conv-btn, #empty, the welcome line, the project button) is visible on
|
|
7812
9290
|
// load — preserving the contract the shipped #339/#347/#429/#442 Hub UI
|
|
@@ -7816,12 +9294,15 @@ function tfInitShell() {
|
|
|
7816
9294
|
} else {
|
|
7817
9295
|
tfSelectProjectView('overview');
|
|
7818
9296
|
}
|
|
9297
|
+
// Server-hydrated completed runs can arrive before the shell finishes wiring
|
|
9298
|
+
// the project workspace. Re-render once the workspace is selected so manager
|
|
9299
|
+
// delegation boards are visible on first load, not only after a refresh tick.
|
|
9300
|
+
renderActive();
|
|
7819
9301
|
}
|
|
7820
9302
|
|
|
7821
9303
|
// Refresh shell-derived UI after a bootstrap reload (e.g. project picked).
|
|
7822
9304
|
function tfRefreshAfterBootstrap() {
|
|
7823
9305
|
if (!tf.enabled) return;
|
|
7824
|
-
tfRenderRail();
|
|
7825
9306
|
tfRenderProjectTabs();
|
|
7826
9307
|
if (tf.area === 'company') tfRenderCompany();
|
|
7827
9308
|
else if (tf.area === 'manager') tfRenderManager();
|