fraim-framework 2.0.184 → 2.0.185
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/desktop-main.js +2 -10
- package/dist/src/ai-hub/remote-hub-gateway.js +88 -0
- package/dist/src/ai-hub/server.js +54 -163
- package/dist/src/api/ai-hub/manager-team.js +93 -0
- package/package.json +1 -1
- package/public/ai-hub/index.html +77 -90
- package/public/ai-hub/review.css +4 -2
- package/public/ai-hub/script.js +487 -536
- package/public/ai-hub/styles.css +151 -35
package/public/ai-hub/script.js
CHANGED
|
@@ -839,11 +839,9 @@ function renderRail() {
|
|
|
839
839
|
renderTeamRoster();
|
|
840
840
|
els['conv-list'].innerHTML = '';
|
|
841
841
|
// R4: filter by selected persona when one is active.
|
|
842
|
-
// #
|
|
843
|
-
// sleep-on-learnings)
|
|
844
|
-
//
|
|
845
|
-
const inWorkspace = !!document.querySelector('#proj-workspace .workspace-conv');
|
|
846
|
-
const projectUpdateJobs = new Set(['project-onboarding', 'sleep-on-learnings']);
|
|
842
|
+
// #693 R1: the "Project Updates" section was retired; project-lifecycle runs
|
|
843
|
+
// (project-onboarding + sleep-on-learnings) now surface in the Runs list under
|
|
844
|
+
// their employee like any other run, so their history stays reachable.
|
|
847
845
|
const allProjectConversations = projectConversations();
|
|
848
846
|
const managedChildren = allProjectConversations.filter(isManagedDelegationChild);
|
|
849
847
|
const childrenByManager = new Map();
|
|
@@ -856,7 +854,6 @@ function renderRail() {
|
|
|
856
854
|
const list = allProjectConversations.filter((conv) =>
|
|
857
855
|
(!state.selectedPersonaKey || conv.personaKey === state.selectedPersonaKey) &&
|
|
858
856
|
!isManagedDelegationChild(conv) &&
|
|
859
|
-
!(inWorkspace && projectUpdateJobs.has(conv.jobId)) &&
|
|
860
857
|
!AREA_SCOPED_JOBS.has(conv.jobId)
|
|
861
858
|
);
|
|
862
859
|
|
|
@@ -886,6 +883,31 @@ function renderRail() {
|
|
|
886
883
|
groups.get(key).conversations.push(conv);
|
|
887
884
|
}
|
|
888
885
|
|
|
886
|
+
// #693 R1 (PR round 2): this Runs list is the single employees section in the
|
|
887
|
+
// workspace tree, so include every project employee — team, manager-team, and
|
|
888
|
+
// assigned — even before they have a run, so you can assign/delegate to them.
|
|
889
|
+
const tfProjId = (typeof tf !== 'undefined' && tf) ? tf.activeProjectId : null;
|
|
890
|
+
const managerTeamKeys = (state.bootstrap?.managerTeam || []).map((e) => e.personaKey).filter(Boolean);
|
|
891
|
+
// merge: master resolves the tree's employees via tfProjectTeamKeys() (hired personas),
|
|
892
|
+
// not project.team; align on it so every hired employee appears in the single section.
|
|
893
|
+
const projectEmployeeKeys = new Set([
|
|
894
|
+
...(typeof tfProjectTeamKeys === 'function' ? tfProjectTeamKeys() : []),
|
|
895
|
+
...(tfProjId && typeof tfProjectAssignments === 'function' ? tfProjectAssignments(tfProjId).map((a) => a.employeeKey).filter(Boolean) : []),
|
|
896
|
+
...managerTeamKeys,
|
|
897
|
+
]);
|
|
898
|
+
for (const key of projectEmployeeKeys) {
|
|
899
|
+
if (groups.has(key)) continue;
|
|
900
|
+
const p = typeof tfPersonaByKey === 'function' ? tfPersonaByKey(key) : null;
|
|
901
|
+
if (p && p.status !== 'hired') continue; // reconcile against real entitlements (#538 follow-up)
|
|
902
|
+
groups.set(key, {
|
|
903
|
+
key,
|
|
904
|
+
label: p ? p.displayName : key,
|
|
905
|
+
detail: p ? (p.role || 'AI Employee') : '',
|
|
906
|
+
sample: p ? { personaKey: key } : { agentName: key },
|
|
907
|
+
conversations: [],
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
889
911
|
function buildRunButton(conv, options = {}) {
|
|
890
912
|
const btn = document.createElement('button');
|
|
891
913
|
btn.type = 'button';
|
|
@@ -999,9 +1021,33 @@ function renderRail() {
|
|
|
999
1021
|
summary.appendChild(avatar);
|
|
1000
1022
|
summary.appendChild(copy);
|
|
1001
1023
|
summary.appendChild(addBtn);
|
|
1024
|
+
// #693 R1 (PR round 2): hire-a-human-manager (#538) affordance, ported from the
|
|
1025
|
+
// retired emp-hero list. The old per-employee "Delegate" button is intentionally
|
|
1026
|
+
// omitted — it did the same openPalette('/persona') as the "+" above, so it was
|
|
1027
|
+
// redundant and only squeezed the employee-name column.
|
|
1028
|
+
if (state.bootstrap && state.bootstrap.managerHiring && state.bootstrap.managerHiring.roles && state.bootstrap.managerHiring.roles[group.key]) {
|
|
1029
|
+
const hireBtn = document.createElement('button');
|
|
1030
|
+
hireBtn.type = 'button';
|
|
1031
|
+
hireBtn.className = 'eh-hire-manager';
|
|
1032
|
+
hireBtn.textContent = '🧑💼';
|
|
1033
|
+
hireBtn.title = 'Find a human manager for ' + group.label;
|
|
1034
|
+
hireBtn.setAttribute('aria-label', 'Find a human manager for ' + group.label);
|
|
1035
|
+
hireBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openHireManagerModal(group.key); });
|
|
1036
|
+
summary.appendChild(hireBtn);
|
|
1037
|
+
}
|
|
1002
1038
|
summary.appendChild(count);
|
|
1003
1039
|
details.appendChild(summary);
|
|
1004
|
-
|
|
1040
|
+
if (group.conversations.length) {
|
|
1041
|
+
details.appendChild(buildGroupList(group.conversations));
|
|
1042
|
+
} else {
|
|
1043
|
+
const emptyWrap = document.createElement('div');
|
|
1044
|
+
emptyWrap.className = 'conv-employee-list';
|
|
1045
|
+
const em = document.createElement('div');
|
|
1046
|
+
em.className = 'eh-empty';
|
|
1047
|
+
em.textContent = 'No runs yet — + to assign a job.';
|
|
1048
|
+
emptyWrap.appendChild(em);
|
|
1049
|
+
details.appendChild(emptyWrap);
|
|
1050
|
+
}
|
|
1005
1051
|
els['conv-list'].appendChild(details);
|
|
1006
1052
|
}
|
|
1007
1053
|
|
|
@@ -1016,9 +1062,11 @@ function renderRail() {
|
|
|
1016
1062
|
wcDetails.open = true;
|
|
1017
1063
|
const wcSummary = document.createElement('summary');
|
|
1018
1064
|
wcSummary.className = 'conv-employee-tab';
|
|
1019
|
-
//
|
|
1065
|
+
// #693.8: watercooler icon (replaces the blank dashed placeholder).
|
|
1020
1066
|
const wcAvatar = document.createElement('span');
|
|
1021
|
-
wcAvatar.className = 'conv-employee-avatar conv-employee-avatar--
|
|
1067
|
+
wcAvatar.className = 'conv-employee-avatar conv-employee-avatar--watercooler';
|
|
1068
|
+
wcAvatar.setAttribute('aria-hidden', 'true');
|
|
1069
|
+
wcAvatar.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="2.5" width="10" height="6" rx="1.2"></rect><path d="M8.5 8.5h7v9a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2z"></path><line x1="9.5" y1="21.5" x2="14.5" y2="21.5"></line></svg>';
|
|
1022
1070
|
const wcCopy = document.createElement('span');
|
|
1023
1071
|
wcCopy.className = 'conv-employee-tab-copy';
|
|
1024
1072
|
const wcLabel = document.createElement('strong');
|
|
@@ -2989,7 +3037,6 @@ function convAwaitingReview(conv) {
|
|
|
2989
3037
|
// output, not deliverables submitted to the human manager. Other manager jobs (e.g.,
|
|
2990
3038
|
// stakeholder-status-reporting) may legitimately surface artifacts for human review.
|
|
2991
3039
|
if (delegationLedgerForConversation(conv)) return false;
|
|
2992
|
-
if (conv.status === 'completed' && Array.isArray(conv.artifacts) && conv.artifacts.length > 0) return true;
|
|
2993
3040
|
// #521: a plain completed turn is NOT awaiting review — the approve/reject card
|
|
2994
3041
|
// only belongs when the employee actually submits for review (a review_handoff,
|
|
2995
3042
|
// emitted by the submit phase). A turn that just finished (a question, a pause
|
|
@@ -3054,20 +3101,22 @@ function normalizeReviewHandoff(raw) {
|
|
|
3054
3101
|
if (targetType === 'pull_request') {
|
|
3055
3102
|
const url = safeHttpUrl(rawTarget && rawTarget.url);
|
|
3056
3103
|
if (!url) return null;
|
|
3104
|
+
if (artifacts.length > 0) return null;
|
|
3057
3105
|
return {
|
|
3058
3106
|
reviewRequired: true,
|
|
3059
3107
|
reviewTarget: { type: 'pull_request', label: targetLabel || 'Pull request', url },
|
|
3060
|
-
artifacts,
|
|
3108
|
+
artifacts: [],
|
|
3061
3109
|
summary: typeof raw.summary === 'string' ? raw.summary.trim() : '',
|
|
3062
3110
|
feedbackMode: typeof raw.feedbackMode === 'string' ? raw.feedbackMode.trim() : 'pull_request_comments',
|
|
3063
3111
|
};
|
|
3064
3112
|
}
|
|
3065
3113
|
|
|
3066
|
-
|
|
3114
|
+
const fileArtifacts = artifacts.filter((artifact) => artifact.path && !artifact.url);
|
|
3115
|
+
if (targetType === 'artifact_set' && fileArtifacts.length > 0 && fileArtifacts.length === artifacts.length) {
|
|
3067
3116
|
return {
|
|
3068
3117
|
reviewRequired: true,
|
|
3069
3118
|
reviewTarget: { type: 'artifact_set', label: targetLabel || `${artifacts.length || 1} artifact${artifacts.length === 1 ? '' : 's'}` },
|
|
3070
|
-
artifacts,
|
|
3119
|
+
artifacts: fileArtifacts,
|
|
3071
3120
|
summary: typeof raw.summary === 'string' ? raw.summary.trim() : '',
|
|
3072
3121
|
feedbackMode: typeof raw.feedbackMode === 'string' ? raw.feedbackMode.trim() : 'inline',
|
|
3073
3122
|
};
|
|
@@ -3759,7 +3808,7 @@ async function handleArtifactAction(conv, action) {
|
|
|
3759
3808
|
'Please resend the review handoff contract as valid JSON inside <review_handoff>...</review_handoff>.',
|
|
3760
3809
|
'Do not rely on prose.',
|
|
3761
3810
|
'Include reviewRequired, reviewTarget, artifacts, summary, and feedbackMode.',
|
|
3762
|
-
'Use reviewTarget.type "pull_request" only
|
|
3811
|
+
'Use reviewTarget.type "pull_request" with only the PR URL and no artifacts, or "artifact_set" with exact local file paths and no PR URL.',
|
|
3763
3812
|
].join(' ');
|
|
3764
3813
|
if (conv && conv.sessionId) {
|
|
3765
3814
|
els['coach-text'].value = message;
|
|
@@ -3954,17 +4003,14 @@ function wirePopovers() {
|
|
|
3954
4003
|
|
|
3955
4004
|
if (e.key === 'Escape') {
|
|
3956
4005
|
const cpModal = document.getElementById('cp-modal');
|
|
3957
|
-
const
|
|
3958
|
-
const webhookModal = document.getElementById('dep-webhook-modal');
|
|
4006
|
+
const assignModal = document.getElementById('dep-assignment-modal');
|
|
3959
4007
|
const aomModal = document.getElementById('area-onboard-modal');
|
|
3960
4008
|
if (aomModal && !aomModal.hidden) {
|
|
3961
4009
|
tfCloseAreaOnboardModal();
|
|
3962
4010
|
} else if (cpModal && !cpModal.hidden) {
|
|
3963
4011
|
closePalette();
|
|
3964
|
-
} else if (
|
|
3965
|
-
|
|
3966
|
-
} else if (webhookModal && !webhookModal.hidden) {
|
|
3967
|
-
webhookModal.hidden = true;
|
|
4012
|
+
} else if (assignModal && !assignModal.hidden) {
|
|
4013
|
+
assignModal.hidden = true;
|
|
3968
4014
|
} else if (els['modal'].classList.contains('open')) {
|
|
3969
4015
|
closeModal();
|
|
3970
4016
|
} else {
|
|
@@ -4145,13 +4191,37 @@ function renderCpRows(searchText) {
|
|
|
4145
4191
|
});
|
|
4146
4192
|
}
|
|
4147
4193
|
|
|
4148
|
-
//
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4194
|
+
// #693 R5: agent picker — choose the AI agent for this launch.
|
|
4195
|
+
renderCpAgentPicker();
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
// #693 R5: render the command-palette AI-agent picker. Lists only agents
|
|
4199
|
+
// available on the machine as selectable; unavailable agents are shown disabled
|
|
4200
|
+
// with an install hint. Defaults to the saved agent (state.cpEmployee); if that
|
|
4201
|
+
// agent is not installed, falls back to the first available one.
|
|
4202
|
+
function renderCpAgentPicker() {
|
|
4203
|
+
const picker = document.getElementById('cp-agent-picker');
|
|
4204
|
+
if (!picker) return;
|
|
4205
|
+
picker.innerHTML = '';
|
|
4206
|
+
const employees = (state.bootstrap && state.bootstrap.employees) || [];
|
|
4207
|
+
const list = employees.length ? employees : [{ id: 'claude', label: 'Claude', available: true }];
|
|
4208
|
+
const curOk = list.some((e) => e.id === state.cpEmployee && e.available !== false);
|
|
4209
|
+
if (!curOk) {
|
|
4210
|
+
const firstAvail = list.find((e) => e.available !== false);
|
|
4211
|
+
if (firstAvail) state.cpEmployee = firstAvail.id;
|
|
4212
|
+
}
|
|
4213
|
+
for (const e of list) {
|
|
4214
|
+
const pill = document.createElement('button');
|
|
4215
|
+
pill.type = 'button';
|
|
4216
|
+
pill.className = 'cp-agent-pill' + (e.id === state.cpEmployee ? ' on' : '');
|
|
4217
|
+
pill.textContent = e.label + (e.available === false ? ' · install' : '');
|
|
4218
|
+
if (e.available === false) {
|
|
4219
|
+
pill.disabled = true;
|
|
4220
|
+
pill.title = e.label + ' is not installed on this machine';
|
|
4221
|
+
} else {
|
|
4222
|
+
pill.addEventListener('click', () => { state.cpEmployee = e.id; renderCpAgentPicker(); });
|
|
4223
|
+
}
|
|
4224
|
+
picker.appendChild(pill);
|
|
4155
4225
|
}
|
|
4156
4226
|
}
|
|
4157
4227
|
|
|
@@ -4919,29 +4989,25 @@ function foldRunIntoConversation(conv, run) {
|
|
|
4919
4989
|
conv.personaKey = run.personaKey;
|
|
4920
4990
|
}
|
|
4921
4991
|
const runHandoff = normalizeReviewHandoff(run.reviewHandoff);
|
|
4922
|
-
if (runHandoff)
|
|
4923
|
-
|
|
4992
|
+
if (runHandoff) {
|
|
4993
|
+
conv.reviewHandoff = runHandoff;
|
|
4994
|
+
} else if (run.reviewHandoff) {
|
|
4995
|
+
conv.reviewHandoff = run.reviewHandoff;
|
|
4996
|
+
} else {
|
|
4997
|
+
conv.reviewHandoff = null;
|
|
4998
|
+
}
|
|
4924
4999
|
const runDelegation = normalizeDelegationLedger(run.delegation);
|
|
4925
5000
|
if (runDelegation) conv.delegation = runDelegation;
|
|
4926
5001
|
else delegationLedgerForConversation(conv);
|
|
4927
5002
|
if (Array.isArray(run.artifacts)) {
|
|
4928
5003
|
conv.artifacts = run.artifacts;
|
|
5004
|
+
} else {
|
|
5005
|
+
conv.artifacts = [];
|
|
4929
5006
|
}
|
|
4930
5007
|
// Update status.
|
|
4931
5008
|
if (run.status === 'completed') conv.status = 'completed';
|
|
4932
5009
|
else if (run.status === 'failed') conv.status = 'failed';
|
|
4933
5010
|
else conv.status = 'running';
|
|
4934
|
-
// Fallback for server responses that do not include structured artifacts.
|
|
4935
|
-
// The browser only parses output text when that structured field is absent.
|
|
4936
|
-
if (!Array.isArray(run.artifacts)) {
|
|
4937
|
-
for (const e of conv.events) {
|
|
4938
|
-
for (const found of extractArtifacts(e.text)) {
|
|
4939
|
-
if (!conv.artifacts.some((a) => a.name === found.name && a.where === found.where)) {
|
|
4940
|
-
conv.artifacts.push(found);
|
|
4941
|
-
}
|
|
4942
|
-
}
|
|
4943
|
-
}
|
|
4944
|
-
}
|
|
4945
5011
|
conv.lastUpdatedAt = Date.now();
|
|
4946
5012
|
}
|
|
4947
5013
|
|
|
@@ -4960,42 +5026,6 @@ function foldCompareRunIntoConversation(conv, compareRun) {
|
|
|
4960
5026
|
};
|
|
4961
5027
|
}
|
|
4962
5028
|
|
|
4963
|
-
// Paths under these directories are FRAIM lifecycle bookkeeping (RCAs,
|
|
4964
|
-
// raw learnings, evidence dumps, mock files), not deliverables the
|
|
4965
|
-
// manager should be drawn to. Excluding them keeps the artifact callout
|
|
4966
|
-
// meaningful — it should mean "the employee produced this file for you".
|
|
4967
|
-
const ARTIFACT_EXCLUDE_RE = /(^|\/)(retrospectives|evidence|learnings|mocks|raw|archive)\//i;
|
|
4968
|
-
function extractArtifact(text) {
|
|
4969
|
-
return extractArtifacts(text)[0] || null;
|
|
4970
|
-
}
|
|
4971
|
-
|
|
4972
|
-
function extractArtifacts(text) {
|
|
4973
|
-
if (!text) return [];
|
|
4974
|
-
const artifacts = [];
|
|
4975
|
-
const seen = new Set();
|
|
4976
|
-
const matches = String(text)
|
|
4977
|
-
.split(/[\s`"'()<>{}\[\],;:]+/)
|
|
4978
|
-
.filter((token) => /^(docs|public|src|tests)\//.test(token));
|
|
4979
|
-
for (const candidate of matches) {
|
|
4980
|
-
const fullPath = candidate.replace(/[.)]+$/g, '');
|
|
4981
|
-
if (!/\.[A-Za-z0-9]+$/.test(fullPath)) continue;
|
|
4982
|
-
if (ARTIFACT_EXCLUDE_RE.test(fullPath)) continue;
|
|
4983
|
-
const segments = fullPath.split('/');
|
|
4984
|
-
const name = segments[segments.length - 1];
|
|
4985
|
-
const where = segments.slice(0, -1).join('/') + '/';
|
|
4986
|
-
// Store the absolute path so the agent can re-read the artifact (and any
|
|
4987
|
-
// .docx export written alongside it) without guessing the project root.
|
|
4988
|
-
const absPath = state.projectPath
|
|
4989
|
-
? state.projectPath.replace(/\\/g, '/').replace(/\/$/, '') + '/' + fullPath
|
|
4990
|
-
: null;
|
|
4991
|
-
const key = absPath || fullPath;
|
|
4992
|
-
if (seen.has(key)) continue;
|
|
4993
|
-
seen.add(key);
|
|
4994
|
-
artifacts.push({ name, where, path: absPath });
|
|
4995
|
-
}
|
|
4996
|
-
return artifacts;
|
|
4997
|
-
}
|
|
4998
|
-
|
|
4999
5029
|
function startPolling() {
|
|
5000
5030
|
if (state.pollHandle) window.clearInterval(state.pollHandle);
|
|
5001
5031
|
state.pollHandle = window.setInterval(async () => {
|
|
@@ -6695,10 +6725,10 @@ function tfRenderTree() {
|
|
|
6695
6725
|
});
|
|
6696
6726
|
host.appendChild(briefNav);
|
|
6697
6727
|
host.appendChild(tfTreeSep());
|
|
6698
|
-
// #
|
|
6699
|
-
//
|
|
6700
|
-
|
|
6701
|
-
|
|
6728
|
+
// #693 R1: the standalone "Project Updates" accordion is retired. Its two
|
|
6729
|
+
// lifecycle actions now live where their output lands — Run Project Onboarding
|
|
6730
|
+
// in the Brief section and Sleep on learnings in the Learnings section
|
|
6731
|
+
// (see tfRenderProjectContextTop). No separate tree section.
|
|
6702
6732
|
// The legacy standalone "+ New job" entry (#new-conv-btn → openModal → the
|
|
6703
6733
|
// #job-catalog/#next1/#instructions/#start chain) lives in the old `.rail`,
|
|
6704
6734
|
// which the shell suppresses (display:none). Relocate the live node to the
|
|
@@ -6711,126 +6741,20 @@ function tfRenderTree() {
|
|
|
6711
6741
|
host.appendChild(newConvBtn);
|
|
6712
6742
|
host.appendChild(tfTreeSep());
|
|
6713
6743
|
}
|
|
6714
|
-
//
|
|
6715
|
-
//
|
|
6716
|
-
//
|
|
6717
|
-
//
|
|
6718
|
-
// keeps targeting the same cached #conv-list node wherever it now lives.
|
|
6744
|
+
// #693 R1 (PR round 2): the per-employee "Runs" accordions (renderRail's
|
|
6745
|
+
// conv-employee-group) ARE the single employees section — they list every
|
|
6746
|
+
// project employee with that employee's runs nested under their accordion.
|
|
6747
|
+
// Relocate that section into the tree; the redundant emp-hero list is retired.
|
|
6719
6748
|
const convList = (typeof els !== 'undefined' && els['conv-list']) || document.getElementById('conv-list');
|
|
6720
6749
|
const runsSection = convList && convList.closest('.rail-section--runs');
|
|
6721
6750
|
if (runsSection) {
|
|
6722
6751
|
runsSection.classList.add('tree-runs');
|
|
6723
6752
|
host.appendChild(runsSection);
|
|
6724
6753
|
}
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
// Issue #540 R8: include personas on the manager's team so delegate buttons
|
|
6730
|
-
// appear in the workspace tree even when the persona hasn't been assigned yet.
|
|
6731
|
-
const managerTeamPersonaKeys = (state.bootstrap?.managerTeam || []).map((e) => e.personaKey);
|
|
6732
|
-
const empKeys = Array.from(new Set([...onProject, ...assigned.map((a) => a.employeeKey).filter(Boolean), ...managerTeamPersonaKeys]))
|
|
6733
|
-
// #538 follow-up: reconcile against real entitlements. `assigned` job records
|
|
6734
|
-
// and manager-team keys can reference personas that are no longer hired; drop
|
|
6735
|
-
// any key that resolves to a known persona that is NOT hired. Keep hired
|
|
6736
|
-
// personas and genuinely custom employees (no persona record). onProject is
|
|
6737
|
-
// already hired-only (tfProjectTeamKeys), so this is a no-op for it.
|
|
6738
|
-
.filter((key) => { const p = tfPersonaByKey(key); return !p || p.status === 'hired'; });
|
|
6739
|
-
const managerTeamKeySet = new Set(managerTeamPersonaKeys);
|
|
6740
|
-
|
|
6741
|
-
empKeys.forEach((key, i) => {
|
|
6742
|
-
const persona = tfPersonaByKey(key);
|
|
6743
|
-
const name = persona ? persona.displayName : key;
|
|
6744
|
-
const role = (persona && persona.role) || '';
|
|
6745
|
-
const av = tfAvatarFor(name, i);
|
|
6746
|
-
// R1 (#521): each employee is a large hero accordion — the team are the
|
|
6747
|
-
// heroes of the workspace, not a thin nav list. Their jobs nest inside.
|
|
6748
|
-
const details = document.createElement('details');
|
|
6749
|
-
details.className = 'emp-hero';
|
|
6750
|
-
details.dataset.accKey = 'emp:' + key; // #533 R5: stable key for open-state preservation
|
|
6751
|
-
details.open = true;
|
|
6752
|
-
const summary = document.createElement('summary');
|
|
6753
|
-
summary.title = name;
|
|
6754
|
-
const avEl = document.createElement('div');
|
|
6755
|
-
avEl.className = 'eh-av';
|
|
6756
|
-
avEl.style.background = av.color;
|
|
6757
|
-
avEl.textContent = av.badge;
|
|
6758
|
-
const idWrap = document.createElement('div');
|
|
6759
|
-
idWrap.className = 'eh-id';
|
|
6760
|
-
const nameEl = document.createElement('div');
|
|
6761
|
-
nameEl.className = 'eh-name';
|
|
6762
|
-
nameEl.textContent = name;
|
|
6763
|
-
idWrap.appendChild(nameEl);
|
|
6764
|
-
if (role) {
|
|
6765
|
-
const roleEl = document.createElement('div');
|
|
6766
|
-
roleEl.className = 'eh-role';
|
|
6767
|
-
roleEl.textContent = role;
|
|
6768
|
-
idWrap.appendChild(roleEl);
|
|
6769
|
-
}
|
|
6770
|
-
const addBtn = document.createElement('button');
|
|
6771
|
-
addBtn.type = 'button';
|
|
6772
|
-
addBtn.className = 'eh-add';
|
|
6773
|
-
addBtn.textContent = '+';
|
|
6774
|
-
addBtn.title = 'Assign a job to ' + name;
|
|
6775
|
-
addBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); tfOpenAssignJob(key); });
|
|
6776
|
-
// Issue #538 — per-employee entry point: hire a human manager for this role.
|
|
6777
|
-
let hireBtn = null;
|
|
6778
|
-
if (state.bootstrap && state.bootstrap.managerHiring && state.bootstrap.managerHiring.roles && state.bootstrap.managerHiring.roles[key]) {
|
|
6779
|
-
hireBtn = document.createElement('button');
|
|
6780
|
-
hireBtn.type = 'button';
|
|
6781
|
-
hireBtn.className = 'eh-hire-manager';
|
|
6782
|
-
hireBtn.textContent = '🧑💼';
|
|
6783
|
-
hireBtn.title = 'Find a human manager for ' + name;
|
|
6784
|
-
hireBtn.setAttribute('aria-label', 'Find a human manager for ' + name);
|
|
6785
|
-
hireBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openHireManagerModal(key); });
|
|
6786
|
-
}
|
|
6787
|
-
const empDot = tfEmployeeDot(projectId, key);
|
|
6788
|
-
const dot = document.createElement('span');
|
|
6789
|
-
dot.className = 'eh-dot t-dot dot-' + empDot;
|
|
6790
|
-
dot.style.background = 'var(--' + empDot + ')';
|
|
6791
|
-
dot.title = tfDotTitle(empDot);
|
|
6792
|
-
const chev = document.createElement('span');
|
|
6793
|
-
chev.className = 'eh-chev';
|
|
6794
|
-
chev.textContent = '▸';
|
|
6795
|
-
summary.appendChild(avEl);
|
|
6796
|
-
summary.appendChild(idWrap);
|
|
6797
|
-
if (hireBtn) summary.appendChild(hireBtn);
|
|
6798
|
-
summary.appendChild(addBtn);
|
|
6799
|
-
// Issue #540 R8: delegate button for personas on the manager's team.
|
|
6800
|
-
if (managerTeamKeySet.has(key)) {
|
|
6801
|
-
summary.appendChild(renderDelegateButton(key));
|
|
6802
|
-
}
|
|
6803
|
-
summary.appendChild(dot);
|
|
6804
|
-
summary.appendChild(chev);
|
|
6805
|
-
details.appendChild(summary);
|
|
6806
|
-
|
|
6807
|
-
const body = document.createElement('div');
|
|
6808
|
-
body.className = 'eh-jobs';
|
|
6809
|
-
const empJobs = assigned.filter((x) => x.employeeKey === key);
|
|
6810
|
-
if (empJobs.length === 0) {
|
|
6811
|
-
const none = document.createElement('div');
|
|
6812
|
-
none.className = 'eh-empty';
|
|
6813
|
-
none.textContent = 'No jobs yet — + to assign one.';
|
|
6814
|
-
body.appendChild(none);
|
|
6815
|
-
}
|
|
6816
|
-
for (const a of empJobs) {
|
|
6817
|
-
const jobRow = document.createElement('div');
|
|
6818
|
-
jobRow.className = 'tree-job';
|
|
6819
|
-
const jobDot = tfAssignmentDot(a);
|
|
6820
|
-
const label = document.createElement('span');
|
|
6821
|
-
label.className = 'tj-label';
|
|
6822
|
-
label.textContent = a.jobName;
|
|
6823
|
-
const jd = document.createElement('span');
|
|
6824
|
-
jd.className = 'tj-dot dot-' + jobDot;
|
|
6825
|
-
jd.style.background = 'var(--' + jobDot + ')';
|
|
6826
|
-
jd.title = tfDotTitle(jobDot);
|
|
6827
|
-
jobRow.appendChild(label); jobRow.appendChild(jd);
|
|
6828
|
-
jobRow.addEventListener('click', () => tfSelectAssignmentConversation(a));
|
|
6829
|
-
body.appendChild(jobRow);
|
|
6830
|
-
}
|
|
6831
|
-
details.appendChild(body);
|
|
6832
|
-
host.appendChild(details);
|
|
6833
|
-
});
|
|
6754
|
+
// #693 R1 (PR round 2): the redundant emp-hero employee list was removed. The
|
|
6755
|
+
// relocated Runs section above (renderRail's conv-employee-group) is now the
|
|
6756
|
+
// single employees section — every project employee, with their runs nested
|
|
6757
|
+
// under their own accordion, plus per-employee assign/hire-manager.
|
|
6834
6758
|
|
|
6835
6759
|
// #521 R5/R6.4: the project brief and learnings moved OUT of the tree into the
|
|
6836
6760
|
// collapsible top sections (#proj-brief-acc / #proj-learnings-acc), rendered by
|
|
@@ -6931,6 +6855,37 @@ function tfRenderProjectContextTop() {
|
|
|
6931
6855
|
// #533 #4: project-LEVEL manager learnings (repo-local) live in the project tab.
|
|
6932
6856
|
tfRenderLearningsList(learnHost, 'manager', 'project', 'Nothing here yet — what your team learns about working with you on THIS project (preferences, patterns to avoid, validated approaches) appears here. Add one with + Add learning.');
|
|
6933
6857
|
}
|
|
6858
|
+
// #693 R1: the retired "Project Updates" section's actions now live where their
|
|
6859
|
+
// output lands — Run Project Onboarding in the Brief section, Sleep on learnings
|
|
6860
|
+
// in the Learnings section.
|
|
6861
|
+
tfEnsureAccAction('proj-brief-acc', '🚀', 'Run Project Onboarding', () => tfOpenOnboardInput('run'));
|
|
6862
|
+
tfEnsureAccAction('proj-learnings-acc', '🧠', 'Sleep on learnings', () => tfRunShareLearnings('project'));
|
|
6863
|
+
}
|
|
6864
|
+
|
|
6865
|
+
// #693 R1: insert (idempotently) a single lifecycle-action button at the top of a
|
|
6866
|
+
// ctx accordion's body. Used to place onboarding/sleep actions inside the Brief,
|
|
6867
|
+
// Learnings, and Company/Manager context sections they populate.
|
|
6868
|
+
function tfEnsureAccAction(accId, ico, label, onClick) {
|
|
6869
|
+
const acc = document.getElementById(accId);
|
|
6870
|
+
if (!acc) return;
|
|
6871
|
+
const body = acc.querySelector('.ctx-acc-body');
|
|
6872
|
+
if (!body) return;
|
|
6873
|
+
let row = body.querySelector(':scope > .ctx-acc-action-row');
|
|
6874
|
+
if (!row) {
|
|
6875
|
+
row = document.createElement('div');
|
|
6876
|
+
row.className = 'ctx-acc-action-row';
|
|
6877
|
+
body.insertBefore(row, body.firstChild);
|
|
6878
|
+
}
|
|
6879
|
+
// Rebuild the button so repeated renders don't stack duplicates or stale handlers.
|
|
6880
|
+
row.innerHTML = '';
|
|
6881
|
+
const btn = document.createElement('button');
|
|
6882
|
+
btn.type = 'button';
|
|
6883
|
+
btn.className = 'ctx-acc-action';
|
|
6884
|
+
const ic = document.createElement('span'); ic.className = 'caa-ico'; ic.textContent = ico;
|
|
6885
|
+
const tx = document.createElement('span'); tx.textContent = label;
|
|
6886
|
+
btn.appendChild(ic); btn.appendChild(tx);
|
|
6887
|
+
btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onClick(); });
|
|
6888
|
+
row.appendChild(btn);
|
|
6934
6889
|
}
|
|
6935
6890
|
|
|
6936
6891
|
// #521: the Brief section is the project's real captured context + rules — the two
|
|
@@ -7099,119 +7054,181 @@ function cronToHuman(expr) {
|
|
|
7099
7054
|
return expr;
|
|
7100
7055
|
}
|
|
7101
7056
|
|
|
7057
|
+
// #693 R3 (PR round 2): render assignments grouped by the EMPLOYEE the job maps to
|
|
7058
|
+
// (job.requiredPersonaKey), not the AI agent, reusing the Runs section's
|
|
7059
|
+
// conv-employee-group accordion so the two sections look and behave the same.
|
|
7102
7060
|
function renderDeploymentList(container, deployments) {
|
|
7103
7061
|
container.innerHTML = '';
|
|
7104
7062
|
if (!deployments.length) {
|
|
7105
7063
|
const empty = document.createElement('p');
|
|
7106
7064
|
empty.className = 'dep-empty';
|
|
7107
|
-
empty.textContent = 'No assignments yet. Use +
|
|
7065
|
+
empty.textContent = 'No assignments yet. Use + New Assignment above to schedule a job or trigger it from an external system.';
|
|
7108
7066
|
container.appendChild(empty);
|
|
7109
7067
|
return;
|
|
7110
7068
|
}
|
|
7069
|
+
const jobs = state.bootstrap?.jobs ?? [];
|
|
7070
|
+
const UNASSIGNED = '__unassigned__';
|
|
7071
|
+
// The employee is determined by the job: its requiredPersonaKey. Fall back to an
|
|
7072
|
+
// "Unassigned" group for generic jobs with no required specialist.
|
|
7073
|
+
const personaKeyForDep = (dep) => {
|
|
7074
|
+
const j = jobs.find((x) => x.id === dep.jobId);
|
|
7075
|
+
return (j && j.requiredPersonaKey) || UNASSIGNED;
|
|
7076
|
+
};
|
|
7077
|
+
const groups = new Map();
|
|
7111
7078
|
for (const dep of deployments) {
|
|
7112
|
-
const
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
const
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
if (human !== dep.cronExpr) cron.title = dep.cronExpr;
|
|
7136
|
-
card.appendChild(cron);
|
|
7137
|
-
}
|
|
7138
|
-
|
|
7139
|
-
const employee = (state.bootstrap?.employees || []).find((e) => e.id === dep.hostId);
|
|
7140
|
-
if (employee) {
|
|
7141
|
-
const empRow = document.createElement('div');
|
|
7142
|
-
empRow.className = 'dep-detail dep-detail--emp';
|
|
7143
|
-
empRow.textContent = employee.label;
|
|
7144
|
-
card.appendChild(empRow);
|
|
7079
|
+
const key = personaKeyForDep(dep);
|
|
7080
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
7081
|
+
groups.get(key).push(dep);
|
|
7082
|
+
}
|
|
7083
|
+
for (const [personaKey, deps] of groups) {
|
|
7084
|
+
const persona = personaKey === UNASSIGNED ? null : tfPersonaByKey(personaKey);
|
|
7085
|
+
const empLabel = persona ? persona.displayName : 'Unassigned';
|
|
7086
|
+
const empDetail = persona ? (persona.role || 'AI Employee') : 'No specialist required';
|
|
7087
|
+
const details = document.createElement('details');
|
|
7088
|
+
details.className = 'conv-employee-group';
|
|
7089
|
+
details.open = true;
|
|
7090
|
+
const summary = document.createElement('summary');
|
|
7091
|
+
summary.className = 'conv-employee-tab';
|
|
7092
|
+
// DiceBear when the employee has an avatar; else an initials chip.
|
|
7093
|
+
let avatar;
|
|
7094
|
+
if (persona && persona.avatarUrl) {
|
|
7095
|
+
avatar = document.createElement('img');
|
|
7096
|
+
avatar.className = 'conv-employee-avatar';
|
|
7097
|
+
avatar.src = persona.avatarUrl; avatar.alt = empLabel;
|
|
7098
|
+
} else {
|
|
7099
|
+
avatar = document.createElement('span');
|
|
7100
|
+
avatar.className = 'conv-employee-avatar';
|
|
7101
|
+
avatar.textContent = initialBadge(empLabel);
|
|
7145
7102
|
}
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7103
|
+
const copy = document.createElement('span');
|
|
7104
|
+
copy.className = 'conv-employee-tab-copy';
|
|
7105
|
+
const label = document.createElement('strong');
|
|
7106
|
+
label.className = 'conv-employee-tab-label';
|
|
7107
|
+
label.textContent = empLabel;
|
|
7108
|
+
const detail = document.createElement('small');
|
|
7109
|
+
detail.className = 'conv-employee-tab-detail';
|
|
7110
|
+
detail.textContent = empDetail;
|
|
7111
|
+
copy.appendChild(label); copy.appendChild(detail);
|
|
7112
|
+
const count = document.createElement('span');
|
|
7113
|
+
count.className = 'conv-employee-tab-count';
|
|
7114
|
+
count.textContent = String(deps.length);
|
|
7115
|
+
const addBtn = document.createElement('button');
|
|
7116
|
+
addBtn.type = 'button';
|
|
7117
|
+
addBtn.className = 'conv-employee-add';
|
|
7118
|
+
addBtn.textContent = '+';
|
|
7119
|
+
addBtn.title = 'New assignment for ' + empLabel;
|
|
7120
|
+
addBtn.setAttribute('aria-label', 'New assignment for ' + empLabel);
|
|
7121
|
+
addBtn.addEventListener('click', (e) => {
|
|
7122
|
+
e.preventDefault(); e.stopPropagation();
|
|
7123
|
+
openDeploymentModal();
|
|
7124
|
+
// Pre-select a job that maps to this employee so the grouping stays consistent.
|
|
7125
|
+
if (persona) {
|
|
7126
|
+
const sel = document.getElementById('dep-job');
|
|
7127
|
+
const match = jobs.find((x) => x.requiredPersonaKey === personaKey);
|
|
7128
|
+
if (sel && match && Array.from(sel.options).some((o) => o.value === match.id)) sel.value = match.id;
|
|
7129
|
+
}
|
|
7130
|
+
});
|
|
7131
|
+
summary.appendChild(avatar);
|
|
7132
|
+
summary.appendChild(copy);
|
|
7133
|
+
summary.appendChild(addBtn);
|
|
7134
|
+
summary.appendChild(count);
|
|
7135
|
+
details.appendChild(summary);
|
|
7136
|
+
const list = document.createElement('div');
|
|
7137
|
+
list.className = 'conv-employee-list';
|
|
7138
|
+
for (const dep of deps) {
|
|
7139
|
+
list.appendChild(buildDeploymentRow(dep));
|
|
7140
|
+
if (dep.inboundUrl) list.appendChild(buildInboundUrlRow(dep));
|
|
7161
7141
|
}
|
|
7142
|
+
details.appendChild(list);
|
|
7143
|
+
container.appendChild(details);
|
|
7144
|
+
}
|
|
7145
|
+
}
|
|
7162
7146
|
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7147
|
+
// #693 R3: a single assignment rendered as a Runs-style row.
|
|
7148
|
+
function buildDeploymentRow(dep) {
|
|
7149
|
+
const row = document.createElement('div');
|
|
7150
|
+
// #693 R3: a dedicated class (not .conv-item) so run-list logic never treats an
|
|
7151
|
+
// assignment as a run; styled to match the Runs rows via CSS.
|
|
7152
|
+
row.className = 'dep-row';
|
|
7153
|
+
const body = document.createElement('span');
|
|
7154
|
+
body.className = 'conv-body';
|
|
7155
|
+
const title = document.createElement('span');
|
|
7156
|
+
title.className = 'dep-row-title';
|
|
7157
|
+
title.textContent = dep.label;
|
|
7158
|
+
body.appendChild(title);
|
|
7159
|
+
const meta = document.createElement('span');
|
|
7160
|
+
meta.className = 'assign-row-meta';
|
|
7161
|
+
const badge = document.createElement('span');
|
|
7162
|
+
badge.className = 'assign-badge assign-badge--' + (dep.type === 'scheduled' ? 'scheduled' : 'triggered');
|
|
7163
|
+
badge.textContent = dep.type === 'scheduled' ? 'Scheduled' : 'Triggered';
|
|
7164
|
+
meta.appendChild(badge);
|
|
7165
|
+
const detailText = document.createElement('span');
|
|
7166
|
+
if (dep.type === 'scheduled' && dep.cronExpr) {
|
|
7167
|
+
const human = cronToHuman(dep.cronExpr);
|
|
7168
|
+
detailText.textContent = human !== dep.cronExpr ? human : dep.cronExpr;
|
|
7169
|
+
if (human !== dep.cronExpr) detailText.title = dep.cronExpr;
|
|
7170
|
+
} else {
|
|
7171
|
+
detailText.textContent = 'Inbound webhook';
|
|
7187
7172
|
}
|
|
7173
|
+
meta.appendChild(detailText);
|
|
7174
|
+
body.appendChild(meta);
|
|
7175
|
+
row.appendChild(body);
|
|
7176
|
+
const actions = document.createElement('span');
|
|
7177
|
+
actions.className = 'dep-row-actions';
|
|
7178
|
+
const editBtn = document.createElement('button');
|
|
7179
|
+
editBtn.type = 'button'; editBtn.className = 'dep-edit-btn'; editBtn.textContent = 'Edit'; editBtn.title = 'Edit this assignment';
|
|
7180
|
+
editBtn.addEventListener('click', () => openDeploymentModal(dep));
|
|
7181
|
+
const delBtn = document.createElement('button');
|
|
7182
|
+
delBtn.type = 'button'; delBtn.className = 'dep-del-btn'; delBtn.textContent = 'Remove'; delBtn.title = 'Remove this assignment';
|
|
7183
|
+
delBtn.addEventListener('click', async () => {
|
|
7184
|
+
const endpoint = dep.type === 'scheduled' ? 'schedules' : 'webhooks';
|
|
7185
|
+
await fetch('/api/ai-hub/' + endpoint + '/' + dep.id, { method: 'DELETE' });
|
|
7186
|
+
await loadDeployments();
|
|
7187
|
+
});
|
|
7188
|
+
actions.appendChild(editBtn); actions.appendChild(delBtn);
|
|
7189
|
+
row.appendChild(actions);
|
|
7190
|
+
return row;
|
|
7191
|
+
}
|
|
7192
|
+
|
|
7193
|
+
// #693 R3: inbound-URL sub-row for triggered assignments (kept from the old card).
|
|
7194
|
+
function buildInboundUrlRow(dep) {
|
|
7195
|
+
const urlRow = document.createElement('div');
|
|
7196
|
+
// #693 R3: self-contained class — do NOT add .dep-detail (its width:100% plus this
|
|
7197
|
+
// row's left margin overflows the container and forces a horizontal scrollbar).
|
|
7198
|
+
urlRow.className = 'dep-inbound-sub';
|
|
7199
|
+
const urlCode = document.createElement('code');
|
|
7200
|
+
urlCode.textContent = dep.inboundUrl;
|
|
7201
|
+
urlCode.className = 'dep-inbound-url dep-inbound-url--inline';
|
|
7202
|
+
const copyBtn = document.createElement('button');
|
|
7203
|
+
copyBtn.type = 'button';
|
|
7204
|
+
copyBtn.className = 'hm-copy-btn';
|
|
7205
|
+
copyBtn.textContent = 'Copy';
|
|
7206
|
+
copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(dep.inboundUrl).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); }).catch(() => {}); });
|
|
7207
|
+
urlRow.appendChild(urlCode);
|
|
7208
|
+
urlRow.appendChild(copyBtn);
|
|
7209
|
+
return urlRow;
|
|
7188
7210
|
}
|
|
7189
7211
|
|
|
7190
7212
|
function initDeploymentButtons() {
|
|
7191
|
-
|
|
7192
|
-
const
|
|
7193
|
-
if (
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
const
|
|
7198
|
-
|
|
7199
|
-
if (
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
if (schSave) schSave.addEventListener('click', saveScheduleDeployment);
|
|
7211
|
-
const whSave = document.getElementById('dep-wh-save-btn');
|
|
7212
|
-
if (whSave) whSave.addEventListener('click', saveWebhookDeployment);
|
|
7213
|
-
|
|
7214
|
-
// Preset chips
|
|
7213
|
+
// #693 R2: one entry point opens the consolidated assignment modal.
|
|
7214
|
+
const addBtn = document.getElementById('dep-add-btn');
|
|
7215
|
+
if (addBtn) addBtn.addEventListener('click', () => openDeploymentModal());
|
|
7216
|
+
|
|
7217
|
+
const closeModal = () => { const m = document.getElementById('dep-assignment-modal'); if (m) m.hidden = true; };
|
|
7218
|
+
const close = document.getElementById('dep-assign-close');
|
|
7219
|
+
const cancel = document.getElementById('dep-cancel-btn');
|
|
7220
|
+
if (close) close.addEventListener('click', closeModal);
|
|
7221
|
+
if (cancel) cancel.addEventListener('click', closeModal);
|
|
7222
|
+
|
|
7223
|
+
const save = document.getElementById('dep-save-btn');
|
|
7224
|
+
if (save) save.addEventListener('click', saveDeployment);
|
|
7225
|
+
|
|
7226
|
+
// Scheduled / Triggered segmented toggle.
|
|
7227
|
+
document.querySelectorAll('#dep-type-seg .dep-type-btn').forEach((btn) => {
|
|
7228
|
+
btn.addEventListener('click', () => setDepType(btn.dataset.depType));
|
|
7229
|
+
});
|
|
7230
|
+
|
|
7231
|
+
// Preset chips (scheduled block).
|
|
7215
7232
|
document.querySelectorAll('#dep-sch-presets .dep-preset-chip').forEach(chip => {
|
|
7216
7233
|
chip.addEventListener('click', () => applySchPreset(chip.dataset.preset));
|
|
7217
7234
|
});
|
|
@@ -7257,131 +7274,131 @@ function initDeploymentButtons() {
|
|
|
7257
7274
|
});
|
|
7258
7275
|
}
|
|
7259
7276
|
|
|
7260
|
-
|
|
7261
|
-
|
|
7277
|
+
// #693 R2: current assignment type in the consolidated modal.
|
|
7278
|
+
let _depType = 'scheduled';
|
|
7279
|
+
function setDepType(type) {
|
|
7280
|
+
_depType = (type === 'triggered') ? 'triggered' : 'scheduled';
|
|
7281
|
+
document.querySelectorAll('#dep-type-seg .dep-type-btn').forEach((b) => {
|
|
7282
|
+
b.classList.toggle('on', b.dataset.depType === _depType);
|
|
7283
|
+
});
|
|
7284
|
+
const sched = document.getElementById('dep-sched-block');
|
|
7285
|
+
const trig = document.getElementById('dep-trig-block');
|
|
7286
|
+
if (sched) sched.hidden = _depType !== 'scheduled';
|
|
7287
|
+
if (trig) trig.hidden = _depType !== 'triggered';
|
|
7288
|
+
}
|
|
7289
|
+
|
|
7290
|
+
// #693 R2/R5: populate the AI Agent select with only agents available on the
|
|
7291
|
+
// machine. Mirrors the existing employee-select behavior — unavailable agents are
|
|
7292
|
+
// shown disabled rather than as selectable options.
|
|
7293
|
+
function populateAgentSelect(sel, currentHostId) {
|
|
7294
|
+
sel.innerHTML = '';
|
|
7262
7295
|
const employees = state.bootstrap?.employees ?? [];
|
|
7296
|
+
const list = employees.length ? employees : [{ id: 'claude', label: 'Claude', available: true }];
|
|
7297
|
+
for (const e of list) {
|
|
7298
|
+
const opt = document.createElement('option');
|
|
7299
|
+
opt.value = e.id;
|
|
7300
|
+
opt.textContent = e.label + (e.available === false ? ' (not installed)' : '');
|
|
7301
|
+
if (e.available === false) opt.disabled = true;
|
|
7302
|
+
sel.appendChild(opt);
|
|
7303
|
+
}
|
|
7304
|
+
const avail = list.filter((e) => e.available !== false);
|
|
7305
|
+
const pick = (currentHostId && list.some((e) => e.id === currentHostId && e.available !== false))
|
|
7306
|
+
? currentHostId
|
|
7307
|
+
: (avail[0]?.id || list[0]?.id);
|
|
7308
|
+
if (pick) sel.value = pick;
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7311
|
+
// #693 R2: open the single consolidated assignment modal. `dep` present = edit;
|
|
7312
|
+
// its .type locks the segmented control. New assignments default to Scheduled.
|
|
7313
|
+
function openDeploymentModal(dep) {
|
|
7314
|
+
const jobs = state.bootstrap?.jobs ?? [];
|
|
7263
7315
|
const editing = dep != null;
|
|
7264
7316
|
_editingDepId = editing ? dep.id : null;
|
|
7265
7317
|
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7318
|
+
const jobSel = document.getElementById('dep-job');
|
|
7319
|
+
jobSel.innerHTML = '';
|
|
7320
|
+
for (const j of jobs) {
|
|
7321
|
+
const opt = document.createElement('option');
|
|
7322
|
+
opt.value = j.id; opt.textContent = j.title;
|
|
7323
|
+
jobSel.appendChild(opt);
|
|
7324
|
+
}
|
|
7325
|
+
if (dep?.jobId) jobSel.value = dep.jobId;
|
|
7326
|
+
|
|
7327
|
+
populateAgentSelect(document.getElementById('dep-agent'), dep?.hostId || 'claude');
|
|
7328
|
+
document.getElementById('dep-label').value = dep?.label ?? '';
|
|
7329
|
+
document.getElementById('dep-instructions').value = dep?.instructions ?? '';
|
|
7330
|
+
const errEl = document.getElementById('dep-error');
|
|
7331
|
+
if (errEl) errEl.hidden = true;
|
|
7332
|
+
const inbound = document.getElementById('dep-wh-inbound-row');
|
|
7333
|
+
if (inbound) inbound.hidden = true;
|
|
7334
|
+
|
|
7335
|
+
setDepType(editing ? (dep.type === 'scheduled' ? 'scheduled' : 'triggered') : 'scheduled');
|
|
7336
|
+
|
|
7337
|
+
// Scheduled fields: detect preset and pre-fill time/day.
|
|
7338
|
+
const preset = dep?.cronExpr ? detectPreset(dep.cronExpr) : null;
|
|
7339
|
+
if (preset && preset !== 'custom' && dep?.cronExpr) {
|
|
7340
|
+
const parts = dep.cronExpr.trim().split(/\s+/);
|
|
7341
|
+
const m = parseInt(parts[0], 10) || 0;
|
|
7342
|
+
const h = parseInt(parts[1], 10) || 9;
|
|
7343
|
+
document.getElementById('dep-sch-time').value = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
|
7344
|
+
if (preset === 'weekly') document.getElementById('dep-sch-day').value = parts[4];
|
|
7345
|
+
document.getElementById('dep-sch-cron-preset').value = dep.cronExpr;
|
|
7346
|
+
} else if (preset === 'custom' && dep?.cronExpr) {
|
|
7347
|
+
document.getElementById('dep-sch-cron').value = dep.cronExpr;
|
|
7348
|
+
const cronPreview = document.getElementById('dep-sch-cron-preview');
|
|
7349
|
+
if (cronPreview) cronPreview.textContent = cronToHuman(dep.cronExpr);
|
|
7350
|
+
} else {
|
|
7351
|
+
document.getElementById('dep-sch-cron').value = '';
|
|
7352
|
+
document.getElementById('dep-sch-time').value = '09:00';
|
|
7275
7353
|
}
|
|
7354
|
+
applySchPreset(preset || 'daily');
|
|
7276
7355
|
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
const list = employees.length ? employees : fallback;
|
|
7281
|
-
for (const e of list) {
|
|
7282
|
-
const opt = document.createElement('option');
|
|
7283
|
-
opt.value = e.id;
|
|
7284
|
-
opt.textContent = e.label;
|
|
7285
|
-
sel.appendChild(opt);
|
|
7286
|
-
}
|
|
7287
|
-
if (currentHostId) sel.value = currentHostId;
|
|
7288
|
-
}
|
|
7289
|
-
|
|
7290
|
-
if (type === 'schedule') {
|
|
7291
|
-
const modal = document.getElementById('dep-schedule-modal');
|
|
7292
|
-
document.getElementById('dep-sch-modal-title').textContent = editing ? 'Edit Scheduled Assignment' : 'New Scheduled Assignment';
|
|
7293
|
-
document.getElementById('dep-sch-save-btn').textContent = editing ? 'Save changes' : 'Add assignment';
|
|
7294
|
-
populateJobs(document.getElementById('dep-sch-job'), dep?.jobId);
|
|
7295
|
-
populateEmployees(document.getElementById('dep-sch-host'), dep?.hostId || 'claude');
|
|
7296
|
-
document.getElementById('dep-sch-error').hidden = true;
|
|
7297
|
-
document.getElementById('dep-sch-label').value = dep?.label ?? '';
|
|
7298
|
-
document.getElementById('dep-sch-instructions').value = dep?.instructions ?? '';
|
|
7299
|
-
// Detect preset and pre-fill time/day
|
|
7300
|
-
const preset = dep?.cronExpr ? detectPreset(dep.cronExpr) : null;
|
|
7301
|
-
if (preset && preset !== 'custom' && dep?.cronExpr) {
|
|
7302
|
-
const parts = dep.cronExpr.trim().split(/\s+/);
|
|
7303
|
-
const m = parseInt(parts[0], 10) || 0;
|
|
7304
|
-
const h = parseInt(parts[1], 10) || 9;
|
|
7305
|
-
document.getElementById('dep-sch-time').value = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
|
7306
|
-
if (preset === 'weekly') document.getElementById('dep-sch-day').value = parts[4];
|
|
7307
|
-
document.getElementById('dep-sch-cron-preset').value = dep.cronExpr;
|
|
7308
|
-
} else if (preset === 'custom' && dep?.cronExpr) {
|
|
7309
|
-
document.getElementById('dep-sch-cron').value = dep.cronExpr;
|
|
7310
|
-
const cronPreview = document.getElementById('dep-sch-cron-preview');
|
|
7311
|
-
if (cronPreview) cronPreview.textContent = cronToHuman(dep.cronExpr);
|
|
7312
|
-
} else {
|
|
7313
|
-
document.getElementById('dep-sch-cron').value = '';
|
|
7314
|
-
document.getElementById('dep-sch-time').value = '09:00';
|
|
7315
|
-
}
|
|
7316
|
-
applySchPreset(preset || 'daily');
|
|
7317
|
-
modal.hidden = false;
|
|
7318
|
-
} else {
|
|
7319
|
-
const modal = document.getElementById('dep-webhook-modal');
|
|
7320
|
-
document.getElementById('dep-wh-modal-title').textContent = editing ? 'Edit Triggered Assignment' : 'New Triggered Assignment';
|
|
7321
|
-
document.getElementById('dep-wh-save-btn').textContent = editing ? 'Save changes' : 'Add assignment';
|
|
7322
|
-
populateJobs(document.getElementById('dep-wh-job'), dep?.jobId);
|
|
7323
|
-
populateEmployees(document.getElementById('dep-wh-host'), dep?.hostId || 'claude');
|
|
7324
|
-
document.getElementById('dep-wh-error').hidden = true;
|
|
7325
|
-
document.getElementById('dep-wh-inbound-row').hidden = true;
|
|
7326
|
-
document.getElementById('dep-wh-label').value = dep?.label ?? '';
|
|
7327
|
-
document.getElementById('dep-wh-instructions').value = dep?.instructions ?? '';
|
|
7328
|
-
modal.hidden = false;
|
|
7329
|
-
}
|
|
7330
|
-
}
|
|
7331
|
-
|
|
7332
|
-
async function saveScheduleDeployment() {
|
|
7333
|
-
const label = document.getElementById('dep-sch-label').value.trim();
|
|
7334
|
-
const jobId = document.getElementById('dep-sch-job').value;
|
|
7335
|
-
const hostId = document.getElementById('dep-sch-host').value;
|
|
7336
|
-
const isCustom = _activeSchPreset === 'custom';
|
|
7337
|
-
const cronExpr = isCustom
|
|
7338
|
-
? document.getElementById('dep-sch-cron').value.trim()
|
|
7339
|
-
: (document.getElementById('dep-sch-cron-preset').value.trim() || '');
|
|
7340
|
-
const instructions = document.getElementById('dep-sch-instructions').value.trim();
|
|
7341
|
-
const errEl = document.getElementById('dep-sch-error');
|
|
7342
|
-
if (!label || !cronExpr) { errEl.textContent = 'Label and schedule are required.'; errEl.hidden = false; return; }
|
|
7343
|
-
const isEdit = _editingDepId !== null;
|
|
7344
|
-
const url = isEdit ? '/api/ai-hub/schedules/' + _editingDepId : '/api/ai-hub/schedules';
|
|
7345
|
-
const method = isEdit ? 'PUT' : 'POST';
|
|
7346
|
-
try {
|
|
7347
|
-
const resp = await fetch(url, {
|
|
7348
|
-
method,
|
|
7349
|
-
headers: { 'Content-Type': 'application/json' },
|
|
7350
|
-
body: JSON.stringify({ label, jobId, projectPath: state.projectPath || undefined, hostId, cronExpr, instructions: instructions || undefined }),
|
|
7351
|
-
});
|
|
7352
|
-
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; }
|
|
7353
|
-
_editingDepId = null;
|
|
7354
|
-
document.getElementById('dep-schedule-modal').hidden = true;
|
|
7355
|
-
await loadDeployments();
|
|
7356
|
-
} catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
|
|
7356
|
+
document.getElementById('dep-assign-title').textContent = editing ? 'Edit assignment' : 'New assignment';
|
|
7357
|
+
document.getElementById('dep-save-btn').textContent = editing ? 'Save changes' : 'Add assignment';
|
|
7358
|
+
document.getElementById('dep-assignment-modal').hidden = false;
|
|
7357
7359
|
}
|
|
7358
7360
|
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
const
|
|
7362
|
-
const
|
|
7363
|
-
const
|
|
7364
|
-
const
|
|
7365
|
-
|
|
7361
|
+
// #693 R2: one save path routes to the schedule or webhook endpoint by type.
|
|
7362
|
+
async function saveDeployment() {
|
|
7363
|
+
const errEl = document.getElementById('dep-error');
|
|
7364
|
+
const label = document.getElementById('dep-label').value.trim();
|
|
7365
|
+
const jobId = document.getElementById('dep-job').value;
|
|
7366
|
+
const hostId = document.getElementById('dep-agent').value;
|
|
7367
|
+
const instructions = document.getElementById('dep-instructions').value.trim();
|
|
7366
7368
|
const isEdit = _editingDepId !== null;
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
const
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
}
|
|
7369
|
+
if (!label) { errEl.textContent = 'Name is required.'; errEl.hidden = false; return; }
|
|
7370
|
+
|
|
7371
|
+
if (_depType === 'scheduled') {
|
|
7372
|
+
const isCustom = _activeSchPreset === 'custom';
|
|
7373
|
+
const cronExpr = isCustom
|
|
7374
|
+
? document.getElementById('dep-sch-cron').value.trim()
|
|
7375
|
+
: (document.getElementById('dep-sch-cron-preset').value.trim() || '');
|
|
7376
|
+
if (!cronExpr) { errEl.textContent = 'A schedule is required.'; errEl.hidden = false; return; }
|
|
7377
|
+
const url = isEdit ? '/api/ai-hub/schedules/' + _editingDepId : '/api/ai-hub/schedules';
|
|
7378
|
+
try {
|
|
7379
|
+
// merge: keep master's projectPath association on the request body.
|
|
7380
|
+
const resp = await fetch(url, { method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label, jobId, projectPath: state.projectPath || undefined, hostId, cronExpr, instructions: instructions || undefined }) });
|
|
7381
|
+
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to save assignment.'; errEl.hidden = false; return; }
|
|
7382
|
+
_editingDepId = null;
|
|
7383
|
+
document.getElementById('dep-assignment-modal').hidden = true;
|
|
7384
|
+
await loadDeployments();
|
|
7385
|
+
} catch { errEl.textContent = 'Network error - is the Hub running?'; errEl.hidden = false; }
|
|
7386
|
+
} else {
|
|
7387
|
+
const url = isEdit ? '/api/ai-hub/webhooks/' + _editingDepId : '/api/ai-hub/webhooks';
|
|
7388
|
+
try {
|
|
7389
|
+
const resp = await fetch(url, { method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label, jobId, projectPath: state.projectPath || undefined, hostId, instructions: instructions || undefined }) });
|
|
7390
|
+
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to save assignment.'; errEl.hidden = false; return; }
|
|
7391
|
+
if (!isEdit) {
|
|
7392
|
+
const dep = await resp.json();
|
|
7393
|
+
document.getElementById('dep-wh-inbound-url').textContent = dep.inboundUrl;
|
|
7394
|
+
document.getElementById('dep-wh-inbound-row').hidden = false;
|
|
7395
|
+
} else {
|
|
7396
|
+
document.getElementById('dep-assignment-modal').hidden = true;
|
|
7397
|
+
}
|
|
7398
|
+
_editingDepId = null;
|
|
7399
|
+
await loadDeployments();
|
|
7400
|
+
} catch { errEl.textContent = 'Network error - is the Hub running?'; errEl.hidden = false; }
|
|
7401
|
+
}
|
|
7385
7402
|
}
|
|
7386
7403
|
|
|
7387
7404
|
// ---------------------------------------------------------------------------
|
|
@@ -7905,123 +7922,12 @@ function tfContextRow(key, label, placeholder, rerender) {
|
|
|
7905
7922
|
|
|
7906
7923
|
return row;
|
|
7907
7924
|
}
|
|
7908
|
-
// #
|
|
7909
|
-
// runs — the SAME paradigm as the project tree (employee → job runs). The summary
|
|
7910
|
-
// is the job (icon, name, run count, + to run again); the body lists prior runs,
|
|
7911
|
-
// each clicking through to that run's conversation.
|
|
7912
|
-
function tfAreaRailJob(ico, jobId, name, sub, onRun) {
|
|
7913
|
-
const details = document.createElement('details');
|
|
7914
|
-
details.className = 'emp-hero area-rail-acc';
|
|
7915
|
-
details.open = true;
|
|
7916
|
-
const summary = document.createElement('summary');
|
|
7917
|
-
const av = document.createElement('div');
|
|
7918
|
-
av.className = 'eh-av job-ico';
|
|
7919
|
-
av.style.borderRadius = '9px';
|
|
7920
|
-
av.style.background = 'var(--accent-soft)';
|
|
7921
|
-
av.style.color = 'var(--accent)';
|
|
7922
|
-
av.textContent = ico;
|
|
7923
|
-
const id = document.createElement('div'); id.className = 'eh-id';
|
|
7924
|
-
const nm = document.createElement('div'); nm.className = 'eh-name'; nm.style.fontSize = '14px'; nm.textContent = name;
|
|
7925
|
-
const runs = tfConversationsForJob(jobId);
|
|
7926
|
-
const role = document.createElement('div'); role.className = 'eh-role';
|
|
7927
|
-
role.textContent = runs.length ? (runs.length + (runs.length === 1 ? ' past run' : ' past runs')) : sub;
|
|
7928
|
-
id.appendChild(nm); id.appendChild(role);
|
|
7929
|
-
const add = document.createElement('button');
|
|
7930
|
-
add.type = 'button'; add.className = 'eh-add'; add.textContent = '+'; add.title = 'Run ' + name + ' again';
|
|
7931
|
-
add.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onRun(); });
|
|
7932
|
-
const chev = document.createElement('span'); chev.className = 'eh-chev'; chev.textContent = '▸';
|
|
7933
|
-
summary.appendChild(av); summary.appendChild(id); summary.appendChild(add); summary.appendChild(chev);
|
|
7934
|
-
details.appendChild(summary);
|
|
7935
|
-
const body = document.createElement('div'); body.className = 'eh-jobs';
|
|
7936
|
-
if (!runs.length) {
|
|
7937
|
-
const none = document.createElement('div'); none.className = 'eh-empty'; none.textContent = 'No runs yet — + to run it.';
|
|
7938
|
-
body.appendChild(none);
|
|
7939
|
-
} else {
|
|
7940
|
-
for (const c of runs) {
|
|
7941
|
-
const row = document.createElement('div'); row.className = 'tree-job';
|
|
7942
|
-
const label = document.createElement('span'); label.className = 'tj-label'; label.textContent = c.title || c.jobTitle || name;
|
|
7943
|
-
const dotCls = (typeof conversationStateDotClass === 'function') ? conversationStateDotClass(c) : 'grey';
|
|
7944
|
-
const dot = document.createElement('span'); dot.className = 'tj-dot dot-' + dotCls; dot.style.background = 'var(--' + dotCls + ')';
|
|
7945
|
-
row.appendChild(label); row.appendChild(dot);
|
|
7946
|
-
tfAttachRunDelete(row, c, { tree: true }); // #533 R6: delete an area-rail job run
|
|
7947
|
-
row.addEventListener('click', () => { if (typeof switchToConversation === 'function') switchToConversation(c.id); });
|
|
7948
|
-
body.appendChild(row);
|
|
7949
|
-
}
|
|
7950
|
-
}
|
|
7951
|
-
details.appendChild(body);
|
|
7952
|
-
return details;
|
|
7953
|
-
}
|
|
7925
|
+
// #693 R1 (PR round 2): tfAreaRailJob removed with the Company/Manager job rails.
|
|
7954
7926
|
|
|
7955
|
-
// #
|
|
7956
|
-
//
|
|
7957
|
-
//
|
|
7958
|
-
//
|
|
7959
|
-
function tfProjectUpdatesSection() {
|
|
7960
|
-
const JOBS = [
|
|
7961
|
-
{ id: 'project-onboarding', ico: '🚀', name: 'Project Onboarding', run: () => tfStartProjectOnboarding() },
|
|
7962
|
-
{ id: 'sleep-on-learnings', ico: '🧠', name: 'Sleep on learnings', run: () => tfRunShareLearnings('project') },
|
|
7963
|
-
];
|
|
7964
|
-
const details = document.createElement('details');
|
|
7965
|
-
// NB: deliberately NOT `.emp-hero` — that class is the employee-hero selector
|
|
7966
|
-
// the tree's per-employee assign-job affordance keys off; Project Updates reuses
|
|
7967
|
-
// the same visual chrome via `.proj-updates-acc` grouped CSS instead.
|
|
7968
|
-
details.className = 'proj-updates-acc';
|
|
7969
|
-
details.id = 'proj-updates-acc';
|
|
7970
|
-
details.dataset.accKey = 'proj-updates'; // #533 R5: preserve open-state across status re-renders
|
|
7971
|
-
details.open = true;
|
|
7972
|
-
|
|
7973
|
-
const summary = document.createElement('summary');
|
|
7974
|
-
const av = document.createElement('div');
|
|
7975
|
-
av.className = 'eh-av job-ico';
|
|
7976
|
-
av.style.borderRadius = '9px';
|
|
7977
|
-
av.style.background = 'var(--accent-soft)';
|
|
7978
|
-
av.style.color = 'var(--accent)';
|
|
7979
|
-
av.textContent = '🔄';
|
|
7980
|
-
const id = document.createElement('div'); id.className = 'eh-id';
|
|
7981
|
-
const nm = document.createElement('div'); nm.className = 'eh-name'; nm.style.fontSize = '14px'; nm.textContent = 'Project Updates';
|
|
7982
|
-
const totalRuns = JOBS.reduce((n, j) => n + tfConversationsForJob(j.id, { projectPath: state.projectPath }).length, 0);
|
|
7983
|
-
const role = document.createElement('div'); role.className = 'eh-role';
|
|
7984
|
-
role.textContent = totalRuns ? (totalRuns + (totalRuns === 1 ? ' run' : ' runs')) : 'Onboarding & learnings';
|
|
7985
|
-
id.appendChild(nm); id.appendChild(role);
|
|
7986
|
-
|
|
7987
|
-
const add = document.createElement('button');
|
|
7988
|
-
add.type = 'button'; add.className = 'eh-add'; add.textContent = '+'; add.title = 'Run a project update';
|
|
7989
|
-
add.addEventListener('click', (e) => {
|
|
7990
|
-
e.preventDefault(); e.stopPropagation();
|
|
7991
|
-
openPalette();
|
|
7992
|
-
});
|
|
7993
|
-
|
|
7994
|
-
const chev = document.createElement('span'); chev.className = 'eh-chev'; chev.textContent = '▸';
|
|
7995
|
-
summary.appendChild(av); summary.appendChild(id); summary.appendChild(add); summary.appendChild(chev);
|
|
7996
|
-
details.appendChild(summary);
|
|
7997
|
-
|
|
7998
|
-
const body = document.createElement('div'); body.className = 'eh-jobs';
|
|
7999
|
-
JOBS.forEach((j) => {
|
|
8000
|
-
const group = document.createElement('div'); group.className = 'pu-job';
|
|
8001
|
-
const head = document.createElement('div'); head.className = 'pu-job-name';
|
|
8002
|
-
head.textContent = j.ico + ' ' + j.name;
|
|
8003
|
-
group.appendChild(head);
|
|
8004
|
-
const runs = tfConversationsForJob(j.id, { projectPath: state.projectPath });
|
|
8005
|
-
if (!runs.length) {
|
|
8006
|
-
const none = document.createElement('div'); none.className = 'eh-empty'; none.textContent = 'No runs yet.';
|
|
8007
|
-
group.appendChild(none);
|
|
8008
|
-
} else {
|
|
8009
|
-
runs.forEach((c) => {
|
|
8010
|
-
const row = document.createElement('div'); row.className = 'tree-job';
|
|
8011
|
-
const rl = document.createElement('span'); rl.className = 'tj-label'; rl.textContent = c.title || c.jobTitle || j.name;
|
|
8012
|
-
const dotCls = (typeof conversationStateDotClass === 'function') ? conversationStateDotClass(c) : 'grey';
|
|
8013
|
-
const dot = document.createElement('span'); dot.className = 'tj-dot dot-' + dotCls; dot.style.background = 'var(--' + dotCls + ')';
|
|
8014
|
-
row.appendChild(rl); row.appendChild(dot);
|
|
8015
|
-
tfAttachRunDelete(row, c, { tree: true }); // #533 R6: delete a project-update run
|
|
8016
|
-
row.addEventListener('click', () => { if (typeof switchToConversation === 'function') switchToConversation(c.id); });
|
|
8017
|
-
group.appendChild(row);
|
|
8018
|
-
});
|
|
8019
|
-
}
|
|
8020
|
-
body.appendChild(group);
|
|
8021
|
-
});
|
|
8022
|
-
details.appendChild(body);
|
|
8023
|
-
return details;
|
|
8024
|
-
}
|
|
7927
|
+
// #693 R1: tfProjectUpdatesSection was removed. The "Project Updates" tree
|
|
7928
|
+
// accordion is retired; its Run Project Onboarding and Sleep on learnings actions
|
|
7929
|
+
// now live inside the Brief and Learnings sections (see tfEnsureAccAction, called
|
|
7930
|
+
// from tfRenderProjectContextTop).
|
|
8025
7931
|
|
|
8026
7932
|
// #594 R2: render a conversation panel (topline + messages + coach input) into an
|
|
8027
7933
|
// area-level container so org/manager jobs are viewable inside Company/Manager tabs.
|
|
@@ -8156,13 +8062,9 @@ function tfRenderCompany() {
|
|
|
8156
8062
|
infoBtn.textContent = '📋 Company Info';
|
|
8157
8063
|
infoBtn.addEventListener('click', () => tfToggleAreaView('company', 'info'));
|
|
8158
8064
|
rail.appendChild(infoBtn);
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
rail.appendChild(tfAreaRailJob('🧠', 'organizational-learning-synthesis', 'organizational-learning-synthesis', 'Synthesize company learnings', () => tfStartOrgLearningSynthesis()));
|
|
8163
|
-
const note = document.createElement('div'); note.className = 'area-rail-note';
|
|
8164
|
-
note.textContent = "Company-wide work lives here — it applies across every project, so it's not in any project's job list. (sleep-on-learnings runs at the project level.)";
|
|
8165
|
-
rail.appendChild(note);
|
|
8065
|
+
// #693 R1 (PR round 2): the "Company jobs" launcher list is retired. Its jobs
|
|
8066
|
+
// now run from the section they populate — Run Organization Onboarding in
|
|
8067
|
+
// "Context & rules", Synthesize company learnings in "Company learnings".
|
|
8166
8068
|
}
|
|
8167
8069
|
const profile = document.getElementById('company-profile');
|
|
8168
8070
|
const learn = document.getElementById('company-learnings');
|
|
@@ -8218,6 +8120,11 @@ function tfRenderCompany() {
|
|
|
8218
8120
|
if (learn) {
|
|
8219
8121
|
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.');
|
|
8220
8122
|
}
|
|
8123
|
+
// #693 R1: lifecycle actions live in the sections they populate (mirrors the
|
|
8124
|
+
// Projects tab): onboarding in Context & rules, learnings synthesis in Company learnings.
|
|
8125
|
+
tfEnsureAccAction('company-ctx-acc', '🏢', 'Run Organization Onboarding', () => tfStartOrgOnboarding());
|
|
8126
|
+
// #693 R1 (PR round 2): Company's learnings job is organizational-learning-synthesis, not sleep-on-learnings.
|
|
8127
|
+
tfEnsureAccAction('company-learn-acc', '🧠', 'Synthesize company learnings', () => tfStartOrgLearningSynthesis());
|
|
8221
8128
|
tfMaybeRenderOrgPushBanner();
|
|
8222
8129
|
// Show the full coaching conversation panel when there is an active org conv;
|
|
8223
8130
|
// otherwise show the info view (accordions). This uses the shared .page element
|
|
@@ -8307,12 +8214,8 @@ function tfRenderManager() {
|
|
|
8307
8214
|
infoBtn.textContent = '📋 Manager Info';
|
|
8308
8215
|
infoBtn.addEventListener('click', () => tfToggleAreaView('manager', 'info'));
|
|
8309
8216
|
rail.appendChild(infoBtn);
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
rail.appendChild(tfAreaRailJob('🤝', 'manager-agreements', 'manager-agreements', 'Set up / update how you work', () => tfStartManagerOnboarding()));
|
|
8313
|
-
const note = document.createElement('div'); note.className = 'area-rail-note';
|
|
8314
|
-
note.textContent = "manager-agreements is about you, not a single project — so it isn't in any project's job list. You coach in project conversations; what the team learns about you surfaces here.";
|
|
8315
|
-
rail.appendChild(note);
|
|
8217
|
+
// #693 R1 (PR round 2): the "Manager jobs" launcher list is retired.
|
|
8218
|
+
// Manager agreements now runs from the "Context & rules" section it populates.
|
|
8316
8219
|
}
|
|
8317
8220
|
const profile = document.getElementById('manager-profile');
|
|
8318
8221
|
const learn = document.getElementById('manager-learnings');
|
|
@@ -8375,6 +8278,12 @@ function tfRenderManager() {
|
|
|
8375
8278
|
// the manager-coaching learnings the team has captured for you.
|
|
8376
8279
|
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.');
|
|
8377
8280
|
}
|
|
8281
|
+
// #693 R1: lifecycle actions live in the sections they populate (mirrors the
|
|
8282
|
+
// Projects tab): manager agreements in Context & rules, learnings in Manager learnings.
|
|
8283
|
+
tfEnsureAccAction('manager-ctx-acc', '🤝', 'Set up manager agreements', () => tfStartManagerOnboarding());
|
|
8284
|
+
// #693 R1 (PR round 2): Manager has no learnings-synthesis job — its learnings
|
|
8285
|
+
// surface here from project sleep-on-learnings promoted to manager scope — so no
|
|
8286
|
+
// run-action button in the Manager learnings section.
|
|
8378
8287
|
// Issue #540 R4-R7: render manager team pool on every manager-tab activation.
|
|
8379
8288
|
renderManagerTeamPool();
|
|
8380
8289
|
// Show the full coaching conversation panel when there is an active manager conv;
|
|
@@ -8883,6 +8792,47 @@ function tfCloseAccountMenu() {
|
|
|
8883
8792
|
if (menu) menu.classList.remove('open');
|
|
8884
8793
|
}
|
|
8885
8794
|
|
|
8795
|
+
// ---------------------------------------------------------------------------
|
|
8796
|
+
// Theme (#700): explicit light/dark with an in-app toggle. The <head> bootstrap
|
|
8797
|
+
// sets [data-theme] pre-paint (no flash) from localStorage['fraim-theme'] or the
|
|
8798
|
+
// OS preference; here we reflect it in the toggle control and persist changes.
|
|
8799
|
+
// Same-origin key sharing keeps /account and /analytics in sync on their next load.
|
|
8800
|
+
// ---------------------------------------------------------------------------
|
|
8801
|
+
function tfCurrentTheme() {
|
|
8802
|
+
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
|
8803
|
+
}
|
|
8804
|
+
function tfSyncThemeToggle(theme) {
|
|
8805
|
+
const btn = document.getElementById('am-theme-toggle');
|
|
8806
|
+
if (!btn) return;
|
|
8807
|
+
const on = theme === 'dark';
|
|
8808
|
+
btn.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
8809
|
+
const sub = document.getElementById('am-theme-sub');
|
|
8810
|
+
if (sub) sub.textContent = on ? 'On' : 'Off';
|
|
8811
|
+
const ico = document.getElementById('am-theme-ico');
|
|
8812
|
+
if (ico) ico.textContent = on ? '🌙' : '☀️';
|
|
8813
|
+
}
|
|
8814
|
+
function tfApplyTheme(theme, persist) {
|
|
8815
|
+
const t = theme === 'dark' ? 'dark' : 'light';
|
|
8816
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
8817
|
+
if (persist) { try { localStorage.setItem('fraim-theme', t); } catch (e) {} }
|
|
8818
|
+
tfSyncThemeToggle(t);
|
|
8819
|
+
// Keep any same-origin embedded surface (e.g. an analytics iframe) in sync live.
|
|
8820
|
+
try {
|
|
8821
|
+
document.querySelectorAll('iframe').forEach((f) => {
|
|
8822
|
+
if (f.contentWindow) f.contentWindow.postMessage({ type: 'fraim-theme-change', theme: t }, location.origin);
|
|
8823
|
+
});
|
|
8824
|
+
} catch (e) {}
|
|
8825
|
+
}
|
|
8826
|
+
function tfToggleTheme(e) {
|
|
8827
|
+
if (e) e.stopPropagation();
|
|
8828
|
+
tfApplyTheme(tfCurrentTheme() === 'dark' ? 'light' : 'dark', true);
|
|
8829
|
+
}
|
|
8830
|
+
function tfWireThemeToggle() {
|
|
8831
|
+
const btn = document.getElementById('am-theme-toggle');
|
|
8832
|
+
if (btn) btn.addEventListener('click', tfToggleTheme);
|
|
8833
|
+
tfSyncThemeToggle(tfCurrentTheme());
|
|
8834
|
+
}
|
|
8835
|
+
|
|
8886
8836
|
// Per-job config for the area onboarding pre-flight modal.
|
|
8887
8837
|
const AREA_ONBOARD_CONFIG = {
|
|
8888
8838
|
'organization-onboarding': {
|
|
@@ -9591,6 +9541,7 @@ function tfWireShell() {
|
|
|
9591
9541
|
}
|
|
9592
9542
|
const avatar = document.getElementById('avatar-btn');
|
|
9593
9543
|
if (avatar) avatar.addEventListener('click', tfToggleAccountMenu);
|
|
9544
|
+
tfWireThemeToggle();
|
|
9594
9545
|
const brainItem = document.getElementById('am-brain');
|
|
9595
9546
|
if (brainItem) brainItem.addEventListener('click', () => { tfCloseAccountMenu(); tfShowArea('brain'); });
|
|
9596
9547
|
const signout = document.getElementById('am-signout');
|