fraim-hub 2.0.261 → 2.0.263
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/cli/utils/pack-home.js +73 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +27 -6
- package/package.json +2 -2
- package/public/ai-hub/index.html +33 -0
- package/public/ai-hub/review.css +38 -0
- package/public/ai-hub/script.js +148 -90
- package/public/ai-hub/styles.css +45 -35
|
@@ -7,6 +7,7 @@ exports.packsDir = packsDir;
|
|
|
7
7
|
exports.defaultCloneDir = defaultCloneDir;
|
|
8
8
|
exports.resolvePackHome = resolvePackHome;
|
|
9
9
|
exports.packReadRoots = packReadRoots;
|
|
10
|
+
exports.migrateStrandedContent = migrateStrandedContent;
|
|
10
11
|
exports.findStrandedLegacyContent = findStrandedLegacyContent;
|
|
11
12
|
exports.gitUrlHasUserinfo = gitUrlHasUserinfo;
|
|
12
13
|
exports.ensurePackClone = ensurePackClone;
|
|
@@ -172,6 +173,78 @@ function resolvePackHome(layer) {
|
|
|
172
173
|
function packReadRoots(layer) {
|
|
173
174
|
return [resolvePackHome(layer).contentRoot];
|
|
174
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Auto-migrate content stranded at the legacy standard path to the configured
|
|
178
|
+
* contentRoot. Runs at most once per process per (layer + contentRoot) pair.
|
|
179
|
+
* Never throws: migration failures are silently skipped.
|
|
180
|
+
*
|
|
181
|
+
* Applies when a synced backend (local-folder, git, fraim-cloud) is configured
|
|
182
|
+
* and the legacy standard path differs from contentRoot. Each file is moved
|
|
183
|
+
* with renameSync; on a cross-device rename (e.g. ~/.fraim → OneDrive) it
|
|
184
|
+
* falls back to copyFileSync + unlinkSync. Files already present in contentRoot
|
|
185
|
+
* are never overwritten — contentRoot is always authoritative.
|
|
186
|
+
*/
|
|
187
|
+
const _migratedStrandedLayers = new Set();
|
|
188
|
+
function migrateStrandedContent(layer) {
|
|
189
|
+
const home = resolvePackHome(layer);
|
|
190
|
+
const key = `${layer}:${home.contentRoot}`;
|
|
191
|
+
if (_migratedStrandedLayers.has(key))
|
|
192
|
+
return;
|
|
193
|
+
_migratedStrandedLayers.add(key);
|
|
194
|
+
const legacyContentRoot = home.legacyRoot ? path_1.default.join(home.legacyRoot, 'personalized-employee') : null;
|
|
195
|
+
if (!legacyContentRoot || legacyContentRoot === home.contentRoot)
|
|
196
|
+
return;
|
|
197
|
+
if (!fs_1.default.existsSync(legacyContentRoot))
|
|
198
|
+
return;
|
|
199
|
+
try {
|
|
200
|
+
const walk = (absDir, relDir) => {
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = fs_1.default.readdirSync(absDir, { withFileTypes: true });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
210
|
+
const src = path_1.default.join(absDir, entry.name);
|
|
211
|
+
const dst = path_1.default.join(home.contentRoot, rel);
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
walk(src, rel);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!entry.isFile())
|
|
217
|
+
continue;
|
|
218
|
+
if (fs_1.default.existsSync(dst))
|
|
219
|
+
continue;
|
|
220
|
+
try {
|
|
221
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dst), { recursive: true });
|
|
222
|
+
fs_1.default.renameSync(src, dst);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
try {
|
|
226
|
+
fs_1.default.copyFileSync(src, dst);
|
|
227
|
+
fs_1.default.unlinkSync(src);
|
|
228
|
+
}
|
|
229
|
+
catch { /* leave source intact */ }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
walk(legacyContentRoot, '');
|
|
234
|
+
// Clean up empty dirs left behind (best-effort).
|
|
235
|
+
for (const dir of ORG_CONTENT_DIRS) {
|
|
236
|
+
try {
|
|
237
|
+
fs_1.default.rmdirSync(path_1.default.join(legacyContentRoot, dir));
|
|
238
|
+
}
|
|
239
|
+
catch { /* not empty — fine */ }
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
fs_1.default.rmdirSync(legacyContentRoot);
|
|
243
|
+
}
|
|
244
|
+
catch { /* not empty — fine */ }
|
|
245
|
+
}
|
|
246
|
+
catch { /* migration is best-effort; never fail a sync over it */ }
|
|
247
|
+
}
|
|
175
248
|
/**
|
|
176
249
|
* Content sitting at the standard local path that the configured home does not
|
|
177
250
|
* hold. Since the home is the single read location, this content is stranded:
|
|
@@ -72,11 +72,18 @@ function managerSecondaryLearningsBase() {
|
|
|
72
72
|
function getLearningRoots(workspaceRoot) {
|
|
73
73
|
const managerHome = (0, pack_home_1.resolvePackHome)('manager');
|
|
74
74
|
const managerHomeBase = (0, path_1.join)(managerHome.contentRoot, 'learnings');
|
|
75
|
+
const managerCacheBase = managerSecondaryLearningsBase();
|
|
76
|
+
// Derive the display base from the actual cache path relative to ~/.fraim so
|
|
77
|
+
// the display string always matches the filesystem path (fixed by #1043r2).
|
|
78
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
79
|
+
const managerCacheDisplayBase = managerCacheBase.startsWith(fraimDir)
|
|
80
|
+
? (0, project_fraim_paths_1.getUserFraimDisplayPath)(managerCacheBase.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, ''))
|
|
81
|
+
: managerCacheBase.replace(/\\/g, '/');
|
|
75
82
|
return {
|
|
76
83
|
globalPersonalBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(workspaceRoot),
|
|
77
84
|
globalPersonalDisplayBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDisplayPath)(workspaceRoot),
|
|
78
|
-
managerCacheBase
|
|
79
|
-
managerCacheDisplayBase
|
|
85
|
+
managerCacheBase,
|
|
86
|
+
managerCacheDisplayBase,
|
|
80
87
|
managerHomeBase,
|
|
81
88
|
managerHomeDisplayBase: managerHomeBase.replace(/\\/g, '/'),
|
|
82
89
|
repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
|
|
@@ -637,11 +644,20 @@ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = t
|
|
|
637
644
|
}
|
|
638
645
|
}
|
|
639
646
|
if (!orgCacheEligible) {
|
|
640
|
-
const
|
|
647
|
+
const managerHome = (0, pack_home_1.resolvePackHome)('manager');
|
|
648
|
+
const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
|
|
641
649
|
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
650
|
+
// Derive the display path relative to ~/.fraim. For the standard
|
|
651
|
+
// single-machine backend contentRoot is ~/.fraim/personalized-employee
|
|
652
|
+
// so the display path is ~/.fraim/personalized-employee/... For a
|
|
653
|
+
// synced backend it may be elsewhere (e.g. ~/.fraim/packs/manager-repo).
|
|
654
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
655
|
+
const relToFraim = managerCachePath.startsWith(fraimDir)
|
|
656
|
+
? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
|
|
657
|
+
: `personalized-employee/${relativePath}`;
|
|
642
658
|
return {
|
|
643
659
|
present: true,
|
|
644
|
-
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(
|
|
660
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(relToFraim)
|
|
645
661
|
};
|
|
646
662
|
}
|
|
647
663
|
}
|
|
@@ -812,13 +828,18 @@ function resolveTeamContextFile(workspaceRoot, key) {
|
|
|
812
828
|
}
|
|
813
829
|
}
|
|
814
830
|
if (key === 'manager' || key === 'managerRules') {
|
|
815
|
-
const
|
|
831
|
+
const managerHome = (0, pack_home_1.resolvePackHome)('manager');
|
|
832
|
+
const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
|
|
816
833
|
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
834
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
835
|
+
const relToFraim = managerCachePath.startsWith(fraimDir)
|
|
836
|
+
? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
|
|
837
|
+
: `personalized-employee/${relativePath}`;
|
|
817
838
|
return {
|
|
818
839
|
present: true,
|
|
819
840
|
readPath: managerCachePath,
|
|
820
841
|
writePath: '',
|
|
821
|
-
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(
|
|
842
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(relToFraim),
|
|
822
843
|
scope,
|
|
823
844
|
managedByManagerSync: true
|
|
824
845
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.263",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"electron": "^41.2.2",
|
|
164
164
|
"electron-updater": "^6.8.9",
|
|
165
165
|
"express": "^5.2.1",
|
|
166
|
-
"fraim": "2.0.
|
|
166
|
+
"fraim": "2.0.263",
|
|
167
167
|
"mongodb": "^7.0.0",
|
|
168
168
|
"node-cron": "4.2.1",
|
|
169
169
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -1132,6 +1132,39 @@
|
|
|
1132
1132
|
</div>
|
|
1133
1133
|
|
|
1134
1134
|
<!-- Issue #834: Sharing-backend chooser modal -->
|
|
1135
|
+
<!-- Share destination modal: shown when the user clicks "Share with others" -->
|
|
1136
|
+
<div class="modal-backdrop" id="share-dest-modal" role="dialog" aria-modal="true" aria-labelledby="share-dest-title" hidden>
|
|
1137
|
+
<div class="modal">
|
|
1138
|
+
<div class="modal-header">
|
|
1139
|
+
<h2 id="share-dest-title">Share learnings</h2>
|
|
1140
|
+
<p id="share-dest-sub">Choose where to share these learnings.</p>
|
|
1141
|
+
</div>
|
|
1142
|
+
<div class="modal-body">
|
|
1143
|
+
<div class="lp-pp-dest-row" id="share-dest-row">
|
|
1144
|
+
<button class="lp-pp-dest-btn" type="button" data-dest="org" id="share-dest-org">
|
|
1145
|
+
<span class="lp-pp-dest-name">Company</span>
|
|
1146
|
+
<span class="lp-pp-dest-sub">Everyone in the organization</span>
|
|
1147
|
+
</button>
|
|
1148
|
+
<button class="lp-pp-dest-btn" type="button" data-dest="project" id="share-dest-proj">
|
|
1149
|
+
<span class="lp-pp-dest-name">This project</span>
|
|
1150
|
+
<span class="lp-pp-dest-sub">Visible on a specific project</span>
|
|
1151
|
+
</button>
|
|
1152
|
+
</div>
|
|
1153
|
+
<div class="lp-pp-project-section" id="share-dest-proj-section" hidden>
|
|
1154
|
+
<label class="lp-pp-label" for="share-dest-proj-sel">Which project?</label>
|
|
1155
|
+
<select class="lp-pp-sel" id="share-dest-proj-sel"></select>
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div class="modal-footer">
|
|
1159
|
+
<span class="left" id="share-dest-hint"></span>
|
|
1160
|
+
<div class="right">
|
|
1161
|
+
<button class="ghost" type="button" id="share-dest-cancel">Cancel</button>
|
|
1162
|
+
<button class="send-button" type="button" id="share-dest-go" disabled>Start</button>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1135
1168
|
<div class="modal-backdrop" id="sharing-chooser-modal" role="dialog" aria-modal="true" aria-labelledby="sharing-chooser-title" hidden>
|
|
1136
1169
|
<div class="modal">
|
|
1137
1170
|
<div class="modal-header">
|
package/public/ai-hub/review.css
CHANGED
|
@@ -638,3 +638,41 @@
|
|
|
638
638
|
display: flex;
|
|
639
639
|
gap: 8px;
|
|
640
640
|
}
|
|
641
|
+
/* Destination row: Company / This project toggle buttons. */
|
|
642
|
+
.lp-pp-dest-row {
|
|
643
|
+
display: flex;
|
|
644
|
+
gap: 8px;
|
|
645
|
+
}
|
|
646
|
+
.lp-pp-dest-btn {
|
|
647
|
+
flex: 1;
|
|
648
|
+
display: flex;
|
|
649
|
+
flex-direction: column;
|
|
650
|
+
align-items: flex-start;
|
|
651
|
+
gap: 2px;
|
|
652
|
+
padding: 8px 12px;
|
|
653
|
+
border-radius: 8px;
|
|
654
|
+
border: 1.5px solid var(--line);
|
|
655
|
+
background: var(--bg);
|
|
656
|
+
cursor: pointer;
|
|
657
|
+
text-align: left;
|
|
658
|
+
transition: border-color 0.12s, background 0.12s;
|
|
659
|
+
}
|
|
660
|
+
.lp-pp-dest-btn:hover { border-color: var(--accent, #0055cc); }
|
|
661
|
+
.lp-pp-dest-btn.lp-pp-dest-selected {
|
|
662
|
+
border-color: var(--accent, #0055cc);
|
|
663
|
+
background: color-mix(in srgb, var(--accent, #0055cc) 8%, var(--bg));
|
|
664
|
+
}
|
|
665
|
+
.lp-pp-dest-name {
|
|
666
|
+
font-size: 13px;
|
|
667
|
+
font-weight: 600;
|
|
668
|
+
color: var(--text);
|
|
669
|
+
}
|
|
670
|
+
.lp-pp-dest-sub {
|
|
671
|
+
font-size: 11px;
|
|
672
|
+
color: var(--muted);
|
|
673
|
+
}
|
|
674
|
+
.lp-pp-project-section {
|
|
675
|
+
display: flex;
|
|
676
|
+
flex-direction: column;
|
|
677
|
+
gap: 6px;
|
|
678
|
+
}
|
package/public/ai-hub/script.js
CHANGED
|
@@ -656,12 +656,12 @@ function taskPaneProjectEntries() {
|
|
|
656
656
|
return Array.from(byPath.values());
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
-
function taskPaneJobEntries() {
|
|
660
|
-
const jobs = [
|
|
661
|
-
{ id: '__freeform__', title: 'Ad-hoc instructions', intent: 'Start from the instructions below.' },
|
|
662
|
-
...((state.bootstrap && state.bootstrap.jobs) || []),
|
|
663
|
-
...((state.bootstrap && state.bootstrap.managerTemplates) || []),
|
|
664
|
-
];
|
|
659
|
+
function taskPaneJobEntries() {
|
|
660
|
+
const jobs = [
|
|
661
|
+
{ id: '__freeform__', title: 'Ad-hoc instructions', intent: 'Start from the instructions below.' },
|
|
662
|
+
...((state.bootstrap && state.bootstrap.jobs) || []),
|
|
663
|
+
...((state.bootstrap && state.bootstrap.managerTemplates) || []),
|
|
664
|
+
];
|
|
665
665
|
const seen = new Set();
|
|
666
666
|
return jobs
|
|
667
667
|
.filter((job) => job && job.id && !PAGE_SCOPED_JOBS.has(job.id))
|
|
@@ -715,7 +715,7 @@ function ensureTaskPaneLauncher() {
|
|
|
715
715
|
const fields = [
|
|
716
716
|
['Project', 'task-pane-project-select', 'select'],
|
|
717
717
|
['Agent tool', 'task-pane-employee-select', 'select'],
|
|
718
|
-
['Job', 'task-pane-job-select', 'select'],
|
|
718
|
+
['Job', 'task-pane-job-select', 'select'],
|
|
719
719
|
['Instructions', 'task-pane-instructions', 'textarea'],
|
|
720
720
|
];
|
|
721
721
|
fields.forEach(([labelText, id, tag]) => {
|
|
@@ -726,11 +726,11 @@ function ensureTaskPaneLauncher() {
|
|
|
726
726
|
span.textContent = labelText;
|
|
727
727
|
const input = document.createElement(tag);
|
|
728
728
|
input.id = id;
|
|
729
|
-
if (tag === 'textarea') {
|
|
730
|
-
input.rows = 4;
|
|
731
|
-
input.placeholder = 'Tell the employee what to do with this Office context.';
|
|
732
|
-
input.addEventListener('input', () => { input.dataset.userTouched = '1'; });
|
|
733
|
-
}
|
|
729
|
+
if (tag === 'textarea') {
|
|
730
|
+
input.rows = 4;
|
|
731
|
+
input.placeholder = 'Tell the employee what to do with this Office context.';
|
|
732
|
+
input.addEventListener('input', () => { input.dataset.userTouched = '1'; });
|
|
733
|
+
}
|
|
734
734
|
label.appendChild(span);
|
|
735
735
|
label.appendChild(input);
|
|
736
736
|
root.appendChild(label);
|
|
@@ -780,15 +780,15 @@ function renderTaskPaneLauncher() {
|
|
|
780
780
|
const employeeSelect = document.getElementById('task-pane-employee-select');
|
|
781
781
|
const jobSelect = document.getElementById('task-pane-job-select');
|
|
782
782
|
const instructions = document.getElementById('task-pane-instructions');
|
|
783
|
-
const start = document.getElementById('task-pane-start-job');
|
|
784
|
-
const current = document.getElementById('task-pane-current-job');
|
|
785
|
-
const context = document.getElementById('task-pane-context-summary');
|
|
786
|
-
const conv = typeof activeConversation === 'function' ? activeConversation() : null;
|
|
787
|
-
const priorJobId = jobSelect && jobSelect.value;
|
|
788
|
-
|
|
789
|
-
taskPaneSetOptions(projectSelect, projects, (project) => project.folderPath, (project) => project.name || friendlyProjectShortName(project.folderPath), state.projectPath);
|
|
790
|
-
taskPaneSetOptions(employeeSelect, agents, (agent) => agent.id, (agent) => agent.label || agent.id, state.selectedEmployeeId);
|
|
791
|
-
taskPaneSetOptions(jobSelect, jobs, (job) => job.id, (job) => job.title || job.id, priorJobId || '__freeform__');
|
|
783
|
+
const start = document.getElementById('task-pane-start-job');
|
|
784
|
+
const current = document.getElementById('task-pane-current-job');
|
|
785
|
+
const context = document.getElementById('task-pane-context-summary');
|
|
786
|
+
const conv = typeof activeConversation === 'function' ? activeConversation() : null;
|
|
787
|
+
const priorJobId = jobSelect && jobSelect.value;
|
|
788
|
+
|
|
789
|
+
taskPaneSetOptions(projectSelect, projects, (project) => project.folderPath, (project) => project.name || friendlyProjectShortName(project.folderPath), state.projectPath);
|
|
790
|
+
taskPaneSetOptions(employeeSelect, agents, (agent) => agent.id, (agent) => agent.label || agent.id, state.selectedEmployeeId);
|
|
791
|
+
taskPaneSetOptions(jobSelect, jobs, (job) => job.id, (job) => job.title || job.id, priorJobId || '__freeform__');
|
|
792
792
|
|
|
793
793
|
if (instructions && instructions.dataset.userTouched !== '1') {
|
|
794
794
|
instructions.value = taskPaneInstructionSeed();
|
|
@@ -834,19 +834,19 @@ async function taskPaneStartSelectedJob() {
|
|
|
834
834
|
const jobSelect = document.getElementById('task-pane-job-select');
|
|
835
835
|
const instructions = document.getElementById('task-pane-instructions');
|
|
836
836
|
const start = document.getElementById('task-pane-start-job');
|
|
837
|
-
const statusEl = document.getElementById('task-pane-start-status');
|
|
838
|
-
const selectedProject = projectSelect && projectSelect.value;
|
|
839
|
-
const requestedEmployeeId = employeeSelect && employeeSelect.value;
|
|
840
|
-
const requestedJobId = jobSelect && jobSelect.value;
|
|
841
|
-
if (selectedProject && tfCanonicalProjectPath(selectedProject) !== tfCanonicalProjectPath(state.projectPath)) {
|
|
842
|
-
await taskPaneSwitchProject(selectedProject);
|
|
843
|
-
const updatedEmployeeSelect = document.getElementById('task-pane-employee-select');
|
|
844
|
-
if (updatedEmployeeSelect && requestedEmployeeId) updatedEmployeeSelect.value = requestedEmployeeId;
|
|
845
|
-
const updatedJobSelect = document.getElementById('task-pane-job-select');
|
|
846
|
-
if (updatedJobSelect && requestedJobId) updatedJobSelect.value = requestedJobId;
|
|
847
|
-
}
|
|
848
|
-
const jobs = taskPaneJobEntries();
|
|
849
|
-
const job = jobs.find((entry) => entry.id === requestedJobId);
|
|
837
|
+
const statusEl = document.getElementById('task-pane-start-status');
|
|
838
|
+
const selectedProject = projectSelect && projectSelect.value;
|
|
839
|
+
const requestedEmployeeId = employeeSelect && employeeSelect.value;
|
|
840
|
+
const requestedJobId = jobSelect && jobSelect.value;
|
|
841
|
+
if (selectedProject && tfCanonicalProjectPath(selectedProject) !== tfCanonicalProjectPath(state.projectPath)) {
|
|
842
|
+
await taskPaneSwitchProject(selectedProject);
|
|
843
|
+
const updatedEmployeeSelect = document.getElementById('task-pane-employee-select');
|
|
844
|
+
if (updatedEmployeeSelect && requestedEmployeeId) updatedEmployeeSelect.value = requestedEmployeeId;
|
|
845
|
+
const updatedJobSelect = document.getElementById('task-pane-job-select');
|
|
846
|
+
if (updatedJobSelect && requestedJobId) updatedJobSelect.value = requestedJobId;
|
|
847
|
+
}
|
|
848
|
+
const jobs = taskPaneJobEntries();
|
|
849
|
+
const job = jobs.find((entry) => entry.id === requestedJobId);
|
|
850
850
|
const employeeId = requestedEmployeeId || (employeeSelect && employeeSelect.value);
|
|
851
851
|
if (!job || !employeeId) return;
|
|
852
852
|
if (start) start.disabled = true;
|
|
@@ -10894,7 +10894,13 @@ function tfRenderProjectContextTop() {
|
|
|
10894
10894
|
{ ico: '🧠', label: 'Sleep on learnings', onClick: () => tfRunShareLearnings('project') },
|
|
10895
10895
|
{ ico: '↑', label: 'Share with others', onClick: () => {
|
|
10896
10896
|
const h = document.getElementById('project-learnings');
|
|
10897
|
-
if (h)
|
|
10897
|
+
if (!h) return;
|
|
10898
|
+
const entering = h.dataset.lpShareMode !== 'true';
|
|
10899
|
+
h.dataset.lpShareMode = entering ? 'true' : 'false';
|
|
10900
|
+
const acc = document.getElementById('proj-learnings-acc');
|
|
10901
|
+
const actionRow = acc && acc.querySelector('.ctx-acc-action-row');
|
|
10902
|
+
if (actionRow) actionRow.hidden = entering;
|
|
10903
|
+
tfReRenderScope('manager', 'project');
|
|
10898
10904
|
}},
|
|
10899
10905
|
]);
|
|
10900
10906
|
}
|
|
@@ -12462,7 +12468,15 @@ function tfRenderManager() {
|
|
|
12462
12468
|
// promotion); only the share action.
|
|
12463
12469
|
tfEnsureAccAction('manager-learn-acc', '↑', 'Share with others', () => {
|
|
12464
12470
|
const h = document.getElementById('manager-learnings');
|
|
12465
|
-
if (h)
|
|
12471
|
+
if (!h) return;
|
|
12472
|
+
const entering = h.dataset.lpShareMode !== 'true';
|
|
12473
|
+
h.dataset.lpShareMode = entering ? 'true' : 'false';
|
|
12474
|
+
// Hide the accordion action row while in share mode so the top button
|
|
12475
|
+
// doesn't duplicate the bottom share bar.
|
|
12476
|
+
const acc = document.getElementById('manager-learn-acc');
|
|
12477
|
+
const actionRow = acc && acc.querySelector('.ctx-acc-action-row');
|
|
12478
|
+
if (actionRow) actionRow.hidden = entering;
|
|
12479
|
+
tfReRenderScope('manager', 'machine');
|
|
12466
12480
|
});
|
|
12467
12481
|
// Issue #540 R4-R7: render manager team pool on every manager-tab activation.
|
|
12468
12482
|
renderManagerTeamPool();
|
|
@@ -13119,6 +13133,15 @@ function tfUpdateShareBar(host, scope, level) {
|
|
|
13119
13133
|
cancelBtn.addEventListener('click', () => {
|
|
13120
13134
|
sel.clear();
|
|
13121
13135
|
host.dataset.lpShareMode = 'false';
|
|
13136
|
+
// Restore the accordion action row that was hidden when share mode started.
|
|
13137
|
+
const accId = scope === 'manager' && level === 'machine' ? 'manager-learn-acc'
|
|
13138
|
+
: scope === 'manager' && level === 'project' ? 'proj-learnings-acc'
|
|
13139
|
+
: null;
|
|
13140
|
+
if (accId) {
|
|
13141
|
+
const acc = document.getElementById(accId);
|
|
13142
|
+
const actionRow = acc && acc.querySelector('.ctx-acc-action-row');
|
|
13143
|
+
if (actionRow) actionRow.hidden = false;
|
|
13144
|
+
}
|
|
13122
13145
|
tfReRenderScope(scope, level);
|
|
13123
13146
|
});
|
|
13124
13147
|
bar.appendChild(cancelBtn);
|
|
@@ -13134,68 +13157,103 @@ async function tfRunShareWithOthers(scope, level, selectedTitles) {
|
|
|
13134
13157
|
const job = templates.find((t) => t.id === slug)
|
|
13135
13158
|
|| jobs.find((j) => j.id === slug)
|
|
13136
13159
|
|| { id: slug, title: 'Share with others', intent: 'Move artifacts to the right scope level.' };
|
|
13137
|
-
|
|
13138
|
-
if (typeof showStatus === 'function') showStatus('Pick a project folder first, then share learnings.', true);
|
|
13139
|
-
return;
|
|
13140
|
-
}
|
|
13141
|
-
// When called from the Manager tab, tf.activeProjectId is whatever was last
|
|
13142
|
-
// open in the Projects tab — not necessarily the project the manager has in
|
|
13143
|
-
// mind. With multiple projects loaded, ask first so the run lands in the
|
|
13144
|
-
// right place. With one project there is no ambiguity.
|
|
13145
|
-
const projects = (typeof tf !== 'undefined' && tf.projects) || [];
|
|
13146
|
-
if (projects.length > 1 && tf.area === 'manager') {
|
|
13147
|
-
tfShareWithOthersPicker(scope, level, selectedTitles, job);
|
|
13148
|
-
return;
|
|
13149
|
-
}
|
|
13150
|
-
await tfLaunchShareWithOthers(scope, level, selectedTitles, job, tf.activeProjectId);
|
|
13160
|
+
tfShareDestinationPicker(scope, level, selectedTitles, job);
|
|
13151
13161
|
}
|
|
13152
13162
|
|
|
13153
|
-
//
|
|
13154
|
-
//
|
|
13155
|
-
function
|
|
13156
|
-
const
|
|
13157
|
-
if (!
|
|
13158
|
-
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
const
|
|
13164
|
-
const
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
const
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13163
|
+
// Destination picker: modal overlay. Company vs. This project, then (for project)
|
|
13164
|
+
// optionally which project when more than one is loaded.
|
|
13165
|
+
function tfShareDestinationPicker(scope, level, selectedTitles, job) {
|
|
13166
|
+
const modal = document.getElementById('share-dest-modal');
|
|
13167
|
+
if (!modal) return;
|
|
13168
|
+
|
|
13169
|
+
const orgBtn = document.getElementById('share-dest-org');
|
|
13170
|
+
const projBtn = document.getElementById('share-dest-proj');
|
|
13171
|
+
const projSection = document.getElementById('share-dest-proj-section');
|
|
13172
|
+
const projSel = document.getElementById('share-dest-proj-sel');
|
|
13173
|
+
const goBtn = document.getElementById('share-dest-go');
|
|
13174
|
+
const cancelBtn = document.getElementById('share-dest-cancel');
|
|
13175
|
+
const hintEl = document.getElementById('share-dest-hint');
|
|
13176
|
+
|
|
13177
|
+
if (!orgBtn || !projBtn || !projSection || !projSel || !goBtn || !cancelBtn) return;
|
|
13178
|
+
|
|
13179
|
+
// Reset visual state.
|
|
13180
|
+
orgBtn.classList.remove('lp-pp-dest-selected');
|
|
13181
|
+
projBtn.classList.remove('lp-pp-dest-selected');
|
|
13182
|
+
projSection.hidden = true;
|
|
13183
|
+
goBtn.disabled = true;
|
|
13184
|
+
if (hintEl) hintEl.textContent = selectedTitles && selectedTitles.length > 0
|
|
13185
|
+
? `${selectedTitles.length} learning${selectedTitles.length === 1 ? '' : 's'} selected`
|
|
13186
|
+
: 'All learnings will be shared';
|
|
13187
|
+
|
|
13188
|
+
// Populate project dropdown.
|
|
13189
|
+
projSel.innerHTML = '';
|
|
13190
|
+
const projects = (typeof tf !== 'undefined' && tf.projects) || [];
|
|
13191
|
+
if (projects.length > 0) {
|
|
13192
|
+
for (const p of projects) {
|
|
13193
|
+
const o = document.createElement('option');
|
|
13194
|
+
o.value = p.id;
|
|
13195
|
+
o.textContent = p.name || p.id;
|
|
13196
|
+
if (p.id === (typeof tf !== 'undefined' && tf.activeProjectId)) o.selected = true;
|
|
13197
|
+
projSel.appendChild(o);
|
|
13198
|
+
}
|
|
13199
|
+
} else if (state.projectPath) {
|
|
13200
|
+
const o = document.createElement('option');
|
|
13201
|
+
o.value = state.projectPath;
|
|
13202
|
+
o.textContent = state.projectPath.split(/[\\/]/).pop() || state.projectPath;
|
|
13203
|
+
projSel.appendChild(o);
|
|
13204
|
+
}
|
|
13205
|
+
|
|
13206
|
+
let chosenDest = null;
|
|
13207
|
+
|
|
13208
|
+
// Use AbortController so listeners are torn down atomically when the modal closes.
|
|
13209
|
+
const ac = new AbortController();
|
|
13210
|
+
const sig = { signal: ac.signal };
|
|
13211
|
+
|
|
13212
|
+
const selectDest = (dest) => {
|
|
13213
|
+
orgBtn.classList.toggle('lp-pp-dest-selected', dest === 'org');
|
|
13214
|
+
projBtn.classList.toggle('lp-pp-dest-selected', dest === 'project');
|
|
13215
|
+
chosenDest = dest;
|
|
13216
|
+
projSection.hidden = dest !== 'project';
|
|
13217
|
+
goBtn.disabled = false;
|
|
13218
|
+
};
|
|
13219
|
+
|
|
13220
|
+
orgBtn.addEventListener('click', () => selectDest('org'), sig);
|
|
13221
|
+
projBtn.addEventListener('click', () => selectDest('project'), sig);
|
|
13222
|
+
|
|
13223
|
+
const close = () => {
|
|
13224
|
+
ac.abort();
|
|
13225
|
+
modal.hidden = true;
|
|
13226
|
+
modal.classList.remove('open');
|
|
13227
|
+
};
|
|
13228
|
+
|
|
13229
|
+
cancelBtn.addEventListener('click', close, sig);
|
|
13230
|
+
|
|
13231
|
+
goBtn.addEventListener('click', async () => {
|
|
13232
|
+
const dest = chosenDest || 'project';
|
|
13233
|
+
close();
|
|
13234
|
+
const projectId = dest === 'project'
|
|
13235
|
+
? (projSel.value || ((typeof tf !== 'undefined' && tf.activeProjectId) || state.projectPath))
|
|
13236
|
+
: ((typeof tf !== 'undefined' && tf.activeProjectId) || state.projectPath);
|
|
13237
|
+
await tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, dest);
|
|
13238
|
+
}, sig);
|
|
13239
|
+
|
|
13240
|
+
// Dismiss on backdrop click.
|
|
13241
|
+
modal.addEventListener('click', (e) => { if (e.target === modal) close(); }, sig);
|
|
13242
|
+
|
|
13243
|
+
// Open.
|
|
13244
|
+
modal.hidden = false;
|
|
13245
|
+
modal.classList.add('open');
|
|
13246
|
+
orgBtn.focus();
|
|
13191
13247
|
}
|
|
13192
13248
|
|
|
13193
|
-
async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId) {
|
|
13249
|
+
async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, destination) {
|
|
13250
|
+
const dest = destination || 'project';
|
|
13194
13251
|
let msg;
|
|
13195
13252
|
if (selectedTitles && selectedTitles.length > 0) {
|
|
13196
13253
|
const levelWord = level === 'project' ? 'project' : level === 'org' ? 'company' : 'manager';
|
|
13254
|
+
const destWord = dest === 'org' ? 'company (org) level' : 'project level';
|
|
13197
13255
|
const list = selectedTitles.map((t) => `- "${t}"`).join('\n');
|
|
13198
|
-
msg = `Run share-with-others. I want to share these ${levelWord}-level learnings:\n${list}\nShow me the
|
|
13256
|
+
msg = `Run share-with-others. I want to share these ${levelWord}-level learnings to the ${destWord}:\n${list}\nShow me the de-personalisation plan for each, then apply it.`;
|
|
13199
13257
|
} else {
|
|
13200
13258
|
msg = 'Run share-with-others. Show me everything I own at each level, grouped by type, so I can decide what to move.';
|
|
13201
13259
|
}
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -4205,14 +4205,18 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4205
4205
|
.modal-hdr { padding: 18px 22px; border-bottom: 1px solid var(--line); }
|
|
4206
4206
|
.modal-hdr h2 { font-size: 18px; font-weight: 700; margin: 0 0 4px; }
|
|
4207
4207
|
.modal-hdr p { font-size: 13px; color: var(--muted); margin: 0; line-height: 1.5; }
|
|
4208
|
-
.modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
4209
|
-
.modal-close { float: right; background: var(--bg); border: none; width: 28px; height: 28px; border-radius: 50%; font-size: 16px; cursor: pointer; color: var(--muted); line-height: 1; }
|
|
4210
|
-
.modal-close:hover { color: var(--text); }
|
|
4211
|
-
.
|
|
4212
|
-
.aom-cancel-btn
|
|
4213
|
-
.
|
|
4214
|
-
.
|
|
4215
|
-
.modal-
|
|
4208
|
+
.modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
4209
|
+
.modal-close { float: right; background: var(--bg); border: none; width: 28px; height: 28px; border-radius: 50%; font-size: 16px; cursor: pointer; color: var(--muted); line-height: 1; }
|
|
4210
|
+
.modal-close:hover { color: var(--text); }
|
|
4211
|
+
.modal-close:focus, .modal-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4212
|
+
.aom-cancel-btn { background: var(--surface); border: 1px solid color-mix(in srgb, var(--accent-strong) 52%, var(--line)); border-radius: 20px; padding: 9px 20px; font-size: 14px; cursor: pointer; color: var(--accent-strong); }
|
|
4213
|
+
.aom-cancel-btn:hover { border-color: var(--accent-strong); color: var(--accent-strong); }
|
|
4214
|
+
.aom-cancel-btn:focus, .aom-cancel-btn:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4215
|
+
.modal-footer { padding: 12px 22px 20px; display: flex; align-items: center; justify-content: space-between; }
|
|
4216
|
+
.modal-next { padding: 9px 22px; background: var(--accent-strong); color: var(--bg); border: none; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
4217
|
+
.modal-next:hover { opacity: .9; }
|
|
4218
|
+
.modal-next:focus, .modal-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4219
|
+
.modal-back { font-size: 13px; color: var(--muted); background: none; border: none; cursor: pointer; }
|
|
4216
4220
|
.modal-step-dots { display: flex; gap: 6px; justify-content: center; margin-bottom: 18px; }
|
|
4217
4221
|
.modal-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--line); }
|
|
4218
4222
|
.modal-dot.on { background: var(--text); }
|
|
@@ -4669,7 +4673,8 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4669
4673
|
align-items: center;
|
|
4670
4674
|
justify-content: center;
|
|
4671
4675
|
}
|
|
4672
|
-
.np-close:hover { color: var(--text); }
|
|
4676
|
+
.np-close:hover { color: var(--text); }
|
|
4677
|
+
.np-close:focus, .np-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4673
4678
|
|
|
4674
4679
|
.np-step-dots {
|
|
4675
4680
|
display: flex;
|
|
@@ -4731,26 +4736,29 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4731
4736
|
justify-content: space-between;
|
|
4732
4737
|
}
|
|
4733
4738
|
|
|
4734
|
-
.np-next {
|
|
4735
|
-
padding: 9px 22px;
|
|
4736
|
-
background: var(--
|
|
4737
|
-
color:
|
|
4738
|
-
border: none;
|
|
4739
|
-
border-radius: 20px;
|
|
4740
|
-
font-size: 14px;
|
|
4741
|
-
font-weight: 600;
|
|
4742
|
-
cursor: pointer;
|
|
4743
|
-
}
|
|
4744
|
-
.np-next:hover { opacity: .88; }
|
|
4745
|
-
|
|
4746
|
-
.np-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4739
|
+
.np-next {
|
|
4740
|
+
padding: 9px 22px;
|
|
4741
|
+
background: var(--accent-strong);
|
|
4742
|
+
color: var(--bg);
|
|
4743
|
+
border: none;
|
|
4744
|
+
border-radius: 20px;
|
|
4745
|
+
font-size: 14px;
|
|
4746
|
+
font-weight: 600;
|
|
4747
|
+
cursor: pointer;
|
|
4748
|
+
}
|
|
4749
|
+
.np-next:hover { opacity: .88; }
|
|
4750
|
+
.np-next:focus, .np-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4751
|
+
.np-next:disabled { opacity: .55; cursor: not-allowed; }
|
|
4752
|
+
|
|
4753
|
+
.np-back {
|
|
4754
|
+
font-size: 13px;
|
|
4755
|
+
color: var(--accent-strong);
|
|
4756
|
+
background: none;
|
|
4757
|
+
border: none;
|
|
4758
|
+
cursor: pointer;
|
|
4759
|
+
}
|
|
4760
|
+
.np-back:hover { color: var(--accent-strong); text-decoration: underline; }
|
|
4761
|
+
.np-back:focus, .np-back:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4754
4762
|
|
|
4755
4763
|
/* ── Discard-guard confirm (shown when a dirty wizard is dismissed) ── */
|
|
4756
4764
|
.np-discard {
|
|
@@ -4775,10 +4783,11 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4775
4783
|
.np-discard-msg { font-size: 13px; color: var(--muted); margin: 0 0 18px; line-height: 1.5; }
|
|
4776
4784
|
.np-discard-row { display: flex; gap: 8px; justify-content: flex-end; }
|
|
4777
4785
|
.np-discard-row button { font: inherit; font-size: 13px; font-weight: 600; border-radius: 9px; padding: 8px 16px; cursor: pointer; }
|
|
4778
|
-
.np-discard-keep { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
|
|
4779
|
-
.np-discard-keep:hover { border-color: var(--muted); }
|
|
4780
|
-
.np-discard-yes { background: var(--danger, #d2261f); color: #fff; border: none; }
|
|
4781
|
-
.np-discard-yes:hover { opacity: .9; }
|
|
4786
|
+
.np-discard-keep { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
|
|
4787
|
+
.np-discard-keep:hover { border-color: var(--muted); }
|
|
4788
|
+
.np-discard-yes { background: var(--danger, #d2261f); color: #fff; border: none; }
|
|
4789
|
+
.np-discard-yes:hover { opacity: .9; }
|
|
4790
|
+
.np-discard-row button:focus, .np-discard-row button:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4782
4791
|
|
|
4783
4792
|
/* ── D11: Source attachment row (step 2) ── */
|
|
4784
4793
|
.np-source {
|
|
@@ -5265,8 +5274,9 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
5265
5274
|
/* ── New-project: folder picker row ── */
|
|
5266
5275
|
.np-folder-row { display:flex; gap:8px; }
|
|
5267
5276
|
.np-folder-row input { flex:1; }
|
|
5268
|
-
.np-browse-btn { padding:9px 16px; background:var(--
|
|
5269
|
-
.np-browse-btn:hover { opacity:.85; }
|
|
5277
|
+
.np-browse-btn { padding:9px 16px; background:var(--accent-strong); color:var(--bg); border:none; border-radius:9px; font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
|
5278
|
+
.np-browse-btn:hover { opacity:.85; }
|
|
5279
|
+
.np-browse-btn:focus, .np-browse-btn:focus-visible { outline:2px solid var(--accent-strong); outline-offset:2px; }
|
|
5270
5280
|
.np-field-hint { font-size:11px; color:var(--muted); margin-top:5px; line-height:1.5; }
|
|
5271
5281
|
|
|
5272
5282
|
@media (max-width: 520px) {
|