fraim-hub 2.0.261 → 2.0.262

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.262",
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.262",
167
167
  "mongodb": "^7.0.0",
168
168
  "node-cron": "4.2.1",
169
169
  "node-edge-tts": "^1.2.10",
@@ -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);
@@ -13138,64 +13161,120 @@ async function tfRunShareWithOthers(scope, level, selectedTitles) {
13138
13161
  if (typeof showStatus === 'function') showStatus('Pick a project folder first, then share learnings.', true);
13139
13162
  return;
13140
13163
  }
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);
13164
+ // Always show the destination picker when sharing from the Manager tab
13165
+ // the manager must choose Company vs. This project regardless of how many
13166
+ // projects are loaded.
13167
+ const hostId = scope === 'manager' && level === 'machine' ? 'manager-learnings' : 'project-learnings';
13168
+ tfShareDestinationPicker(hostId, scope, level, selectedTitles, job);
13151
13169
  }
13152
13170
 
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');
13171
+ // Destination picker: first ask Company vs. This project, then (for project)
13172
+ // optionally ask which project when more than one is loaded.
13173
+ function tfShareDestinationPicker(hostId, scope, level, selectedTitles, job) {
13174
+ const host = document.getElementById(hostId);
13157
13175
  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');
13176
+ // Remove any existing picker before showing a fresh one.
13177
+ host.querySelectorAll('.lp-project-picker').forEach((el) => el.remove());
13178
+
13179
+ const picker = document.createElement('div');
13162
13180
  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);
13181
+
13182
+ // ── Step 1: destination scope ──────────────────────────────────────────
13183
+ const destLabel = document.createElement('div');
13184
+ destLabel.className = 'lp-pp-label';
13185
+ destLabel.textContent = 'Share to:';
13186
+ picker.appendChild(destLabel);
13187
+
13188
+ const destRow = document.createElement('div');
13189
+ destRow.className = 'lp-pp-dest-row';
13190
+
13191
+ const makeDestBtn = (value, text, desc) => {
13192
+ const btn = document.createElement('button');
13193
+ btn.type = 'button';
13194
+ btn.className = 'lp-pp-dest-btn';
13195
+ btn.dataset.dest = value;
13196
+ const name = document.createElement('span'); name.className = 'lp-pp-dest-name'; name.textContent = text;
13197
+ const sub = document.createElement('span'); sub.className = 'lp-pp-dest-sub'; sub.textContent = desc;
13198
+ btn.appendChild(name); btn.appendChild(sub);
13199
+ return btn;
13200
+ };
13201
+
13202
+ const companyBtn = makeDestBtn('org', 'Company', 'Everyone in the organization');
13203
+ const projectBtn = makeDestBtn('project', 'This project', 'Visible on a specific project');
13204
+ destRow.appendChild(companyBtn);
13205
+ destRow.appendChild(projectBtn);
13206
+ picker.appendChild(destRow);
13207
+
13208
+ // ── Step 2: project picker (hidden until "This project" chosen) ────────
13209
+ const projectSection = document.createElement('div');
13210
+ projectSection.className = 'lp-pp-project-section';
13211
+ projectSection.hidden = true;
13212
+
13213
+ const projects = (typeof tf !== 'undefined' && tf.projects) || [];
13214
+ if (projects.length > 1) {
13215
+ const projLabel = document.createElement('label');
13216
+ projLabel.className = 'lp-pp-label';
13217
+ const projLabelId = 'lp-pp-proj-' + Math.random().toString(36).slice(2, 8);
13218
+ projLabel.id = projLabelId;
13219
+ projLabel.textContent = 'Which project?';
13220
+ const projSel = document.createElement('select');
13221
+ projSel.className = 'lp-pp-sel';
13222
+ projSel.setAttribute('aria-labelledby', projLabelId);
13223
+ for (const p of projects) {
13224
+ const o = document.createElement('option'); o.value = p.id;
13225
+ o.textContent = p.name || p.id;
13226
+ if (p.id === tf.activeProjectId) o.selected = true;
13227
+ projSel.appendChild(o);
13228
+ }
13229
+ projectSection.appendChild(projLabel);
13230
+ projectSection.appendChild(projSel);
13231
+ projectSection._projSel = projSel;
13232
+ }
13233
+ picker.appendChild(projectSection);
13234
+
13235
+ // ── Action row ─────────────────────────────────────────────────────────
13179
13236
  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';
13237
+ const go = document.createElement('button');
13238
+ go.type = 'button'; go.className = 'rb-approve'; go.textContent = 'Start'; go.disabled = true;
13239
+ const cancel = document.createElement('button');
13240
+ cancel.type = 'button'; cancel.className = 'rb-secondary'; cancel.textContent = 'Cancel';
13182
13241
  bar.appendChild(go); bar.appendChild(cancel);
13183
13242
  picker.appendChild(bar);
13184
13243
  host.appendChild(picker);
13244
+
13245
+ let chosenDest = null;
13246
+
13247
+ const selectDest = (btn) => {
13248
+ destRow.querySelectorAll('.lp-pp-dest-btn').forEach((b) => b.classList.remove('lp-pp-dest-selected'));
13249
+ btn.classList.add('lp-pp-dest-selected');
13250
+ chosenDest = btn.dataset.dest;
13251
+ projectSection.hidden = chosenDest !== 'project';
13252
+ go.disabled = false;
13253
+ };
13254
+
13255
+ companyBtn.addEventListener('click', () => selectDest(companyBtn));
13256
+ projectBtn.addEventListener('click', () => selectDest(projectBtn));
13257
+
13185
13258
  cancel.addEventListener('click', () => picker.remove());
13259
+
13186
13260
  go.addEventListener('click', async () => {
13187
13261
  go.disabled = true;
13188
13262
  picker.remove();
13189
- await tfLaunchShareWithOthers(scope, level, selectedTitles, job, sel.value);
13263
+ const projectId = chosenDest === 'project'
13264
+ ? (projectSection._projSel ? projectSection._projSel.value : (tf.activeProjectId || state.projectPath))
13265
+ : (tf.activeProjectId || state.projectPath);
13266
+ await tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, chosenDest);
13190
13267
  });
13191
13268
  }
13192
13269
 
13193
- async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId) {
13270
+ async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, destination) {
13271
+ const dest = destination || 'project';
13194
13272
  let msg;
13195
13273
  if (selectedTitles && selectedTitles.length > 0) {
13196
13274
  const levelWord = level === 'project' ? 'project' : level === 'org' ? 'company' : 'manager';
13275
+ const destWord = dest === 'org' ? 'company (org) level' : 'project level';
13197
13276
  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.`;
13277
+ 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
13278
  } else {
13200
13279
  msg = 'Run share-with-others. Show me everything I own at each level, grouped by type, so I can decide what to move.';
13201
13280
  }
@@ -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) {