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.
@@ -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: managerSecondaryLearningsBase(),
79
- managerCacheDisplayBase: (0, project_fraim_paths_1.getUserFraimDisplayPath)('manager/learnings'),
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 managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').contentRoot, relativePath);
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)(`manager/${relativePath}`)
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 managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').contentRoot, relativePath);
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)(`manager/${relativePath}`),
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.261",
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.261",
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",
@@ -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">
@@ -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
+ }
@@ -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) { h.dataset.lpShareMode = h.dataset.lpShareMode === 'true' ? 'false' : 'true'; tfReRenderScope('manager', 'project'); }
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) { h.dataset.lpShareMode = h.dataset.lpShareMode === 'true' ? 'false' : 'true'; tfReRenderScope('manager', 'machine'); }
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
- if (!state.projectPath) {
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
- // Project picker shown when the manager clicks "Share" from the Manager tab
13154
- // and more than one project is loaded.
13155
- function tfShareWithOthersPicker(scope, level, selectedTitles, job) {
13156
- const host = document.getElementById('manager-learnings');
13157
- if (!host) return;
13158
- // Reuse the existing share bar slot if present; otherwise append a picker card.
13159
- let picker = host.querySelector('.lp-project-picker');
13160
- if (picker) return; // already shown
13161
- picker = document.createElement('div');
13162
- picker.className = 'lp-project-picker';
13163
- const labelId = 'lp-pp-label-' + Math.random().toString(36).slice(2, 8);
13164
- const label = document.createElement('label');
13165
- label.className = 'lp-pp-label';
13166
- label.id = labelId;
13167
- label.textContent = 'Which project should this run live under?';
13168
- picker.appendChild(label);
13169
- const sel = document.createElement('select');
13170
- sel.className = 'lp-pp-sel';
13171
- sel.setAttribute('aria-labelledby', labelId);
13172
- for (const p of tf.projects) {
13173
- const o = document.createElement('option'); o.value = p.id;
13174
- o.textContent = p.name || p.id;
13175
- if (p.id === tf.activeProjectId) o.selected = true;
13176
- sel.appendChild(o);
13177
- }
13178
- picker.appendChild(sel);
13179
- const bar = document.createElement('div'); bar.className = 'lp-pp-bar';
13180
- const go = document.createElement('button'); go.type = 'button'; go.className = 'rb-approve'; go.textContent = 'Start';
13181
- const cancel = document.createElement('button'); cancel.type = 'button'; cancel.className = 'rb-secondary'; cancel.textContent = 'Cancel';
13182
- bar.appendChild(go); bar.appendChild(cancel);
13183
- picker.appendChild(bar);
13184
- host.appendChild(picker);
13185
- cancel.addEventListener('click', () => picker.remove());
13186
- go.addEventListener('click', async () => {
13187
- go.disabled = true;
13188
- picker.remove();
13189
- await tfLaunchShareWithOthers(scope, level, selectedTitles, job, sel.value);
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 destination options and de-personalisation plan for each.`;
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
  }
@@ -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
- .aom-cancel-btn { background: none; border: 1px solid var(--line); border-radius: 20px; padding: 9px 20px; font-size: 14px; cursor: pointer; color: var(--muted); }
4212
- .aom-cancel-btn:hover { color: var(--text); }
4213
- .modal-footer { padding: 12px 22px 20px; display: flex; align-items: center; justify-content: space-between; }
4214
- .modal-next { padding: 9px 22px; background: var(--text); color: #fff; border: none; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
4215
- .modal-back { font-size: 13px; color: var(--muted); background: none; border: none; cursor: pointer; }
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(--text);
4737
- color: #fff;
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-back {
4747
- font-size: 13px;
4748
- color: var(--muted);
4749
- background: none;
4750
- border: none;
4751
- cursor: pointer;
4752
- }
4753
- .np-back:hover { color: var(--text); }
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(--text); color:#fff; border:none; border-radius:9px; font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; flex-shrink:0; }
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) {