fraim-hub 2.0.260 → 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.260",
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.260",
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
+ }
@@ -658,6 +658,7 @@ function taskPaneProjectEntries() {
658
658
 
659
659
  function taskPaneJobEntries() {
660
660
  const jobs = [
661
+ { id: '__freeform__', title: 'Ad-hoc instructions', intent: 'Start from the instructions below.' },
661
662
  ...((state.bootstrap && state.bootstrap.jobs) || []),
662
663
  ...((state.bootstrap && state.bootstrap.managerTemplates) || []),
663
664
  ];
@@ -727,7 +728,7 @@ function ensureTaskPaneLauncher() {
727
728
  input.id = id;
728
729
  if (tag === 'textarea') {
729
730
  input.rows = 4;
730
- input.placeholder = 'Add the outcome you want from this Word context.';
731
+ input.placeholder = 'Tell the employee what to do with this Office context.';
731
732
  input.addEventListener('input', () => { input.dataset.userTouched = '1'; });
732
733
  }
733
734
  label.appendChild(span);
@@ -783,10 +784,11 @@ function renderTaskPaneLauncher() {
783
784
  const current = document.getElementById('task-pane-current-job');
784
785
  const context = document.getElementById('task-pane-context-summary');
785
786
  const conv = typeof activeConversation === 'function' ? activeConversation() : null;
787
+ const priorJobId = jobSelect && jobSelect.value;
786
788
 
787
789
  taskPaneSetOptions(projectSelect, projects, (project) => project.folderPath, (project) => project.name || friendlyProjectShortName(project.folderPath), state.projectPath);
788
790
  taskPaneSetOptions(employeeSelect, agents, (agent) => agent.id, (agent) => agent.label || agent.id, state.selectedEmployeeId);
789
- taskPaneSetOptions(jobSelect, jobs, (job) => job.id, (job) => job.title || job.id, null);
791
+ taskPaneSetOptions(jobSelect, jobs, (job) => job.id, (job) => job.title || job.id, priorJobId || '__freeform__');
790
792
 
791
793
  if (instructions && instructions.dataset.userTouched !== '1') {
792
794
  instructions.value = taskPaneInstructionSeed();
@@ -835,13 +837,16 @@ async function taskPaneStartSelectedJob() {
835
837
  const statusEl = document.getElementById('task-pane-start-status');
836
838
  const selectedProject = projectSelect && projectSelect.value;
837
839
  const requestedEmployeeId = employeeSelect && employeeSelect.value;
840
+ const requestedJobId = jobSelect && jobSelect.value;
838
841
  if (selectedProject && tfCanonicalProjectPath(selectedProject) !== tfCanonicalProjectPath(state.projectPath)) {
839
842
  await taskPaneSwitchProject(selectedProject);
840
843
  const updatedEmployeeSelect = document.getElementById('task-pane-employee-select');
841
844
  if (updatedEmployeeSelect && requestedEmployeeId) updatedEmployeeSelect.value = requestedEmployeeId;
845
+ const updatedJobSelect = document.getElementById('task-pane-job-select');
846
+ if (updatedJobSelect && requestedJobId) updatedJobSelect.value = requestedJobId;
842
847
  }
843
848
  const jobs = taskPaneJobEntries();
844
- const job = jobs.find((entry) => entry.id === (jobSelect && jobSelect.value));
849
+ const job = jobs.find((entry) => entry.id === requestedJobId);
845
850
  const employeeId = requestedEmployeeId || (employeeSelect && employeeSelect.value);
846
851
  if (!job || !employeeId) return;
847
852
  if (start) start.disabled = true;
@@ -10889,7 +10894,13 @@ function tfRenderProjectContextTop() {
10889
10894
  { ico: '🧠', label: 'Sleep on learnings', onClick: () => tfRunShareLearnings('project') },
10890
10895
  { ico: '↑', label: 'Share with others', onClick: () => {
10891
10896
  const h = document.getElementById('project-learnings');
10892
- 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');
10893
10904
  }},
10894
10905
  ]);
10895
10906
  }
@@ -12457,7 +12468,15 @@ function tfRenderManager() {
12457
12468
  // promotion); only the share action.
12458
12469
  tfEnsureAccAction('manager-learn-acc', '↑', 'Share with others', () => {
12459
12470
  const h = document.getElementById('manager-learnings');
12460
- 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');
12461
12480
  });
12462
12481
  // Issue #540 R4-R7: render manager team pool on every manager-tab activation.
12463
12482
  renderManagerTeamPool();
@@ -13114,6 +13133,15 @@ function tfUpdateShareBar(host, scope, level) {
13114
13133
  cancelBtn.addEventListener('click', () => {
13115
13134
  sel.clear();
13116
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
+ }
13117
13145
  tfReRenderScope(scope, level);
13118
13146
  });
13119
13147
  bar.appendChild(cancelBtn);
@@ -13133,64 +13161,120 @@ async function tfRunShareWithOthers(scope, level, selectedTitles) {
13133
13161
  if (typeof showStatus === 'function') showStatus('Pick a project folder first, then share learnings.', true);
13134
13162
  return;
13135
13163
  }
13136
- // When called from the Manager tab, tf.activeProjectId is whatever was last
13137
- // open in the Projects tab not necessarily the project the manager has in
13138
- // mind. With multiple projects loaded, ask first so the run lands in the
13139
- // right place. With one project there is no ambiguity.
13140
- const projects = (typeof tf !== 'undefined' && tf.projects) || [];
13141
- if (projects.length > 1 && tf.area === 'manager') {
13142
- tfShareWithOthersPicker(scope, level, selectedTitles, job);
13143
- return;
13144
- }
13145
- 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);
13146
13169
  }
13147
13170
 
13148
- // Project picker shown when the manager clicks "Share" from the Manager tab
13149
- // and more than one project is loaded.
13150
- function tfShareWithOthersPicker(scope, level, selectedTitles, job) {
13151
- 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);
13152
13175
  if (!host) return;
13153
- // Reuse the existing share bar slot if present; otherwise append a picker card.
13154
- let picker = host.querySelector('.lp-project-picker');
13155
- if (picker) return; // already shown
13156
- 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');
13157
13180
  picker.className = 'lp-project-picker';
13158
- const labelId = 'lp-pp-label-' + Math.random().toString(36).slice(2, 8);
13159
- const label = document.createElement('label');
13160
- label.className = 'lp-pp-label';
13161
- label.id = labelId;
13162
- label.textContent = 'Which project should this run live under?';
13163
- picker.appendChild(label);
13164
- const sel = document.createElement('select');
13165
- sel.className = 'lp-pp-sel';
13166
- sel.setAttribute('aria-labelledby', labelId);
13167
- for (const p of tf.projects) {
13168
- const o = document.createElement('option'); o.value = p.id;
13169
- o.textContent = p.name || p.id;
13170
- if (p.id === tf.activeProjectId) o.selected = true;
13171
- sel.appendChild(o);
13172
- }
13173
- 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 ─────────────────────────────────────────────────────────
13174
13236
  const bar = document.createElement('div'); bar.className = 'lp-pp-bar';
13175
- const go = document.createElement('button'); go.type = 'button'; go.className = 'rb-approve'; go.textContent = 'Start';
13176
- 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';
13177
13241
  bar.appendChild(go); bar.appendChild(cancel);
13178
13242
  picker.appendChild(bar);
13179
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
+
13180
13258
  cancel.addEventListener('click', () => picker.remove());
13259
+
13181
13260
  go.addEventListener('click', async () => {
13182
13261
  go.disabled = true;
13183
13262
  picker.remove();
13184
- 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);
13185
13267
  });
13186
13268
  }
13187
13269
 
13188
- async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId) {
13270
+ async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, destination) {
13271
+ const dest = destination || 'project';
13189
13272
  let msg;
13190
13273
  if (selectedTitles && selectedTitles.length > 0) {
13191
13274
  const levelWord = level === 'project' ? 'project' : level === 'org' ? 'company' : 'manager';
13275
+ const destWord = dest === 'org' ? 'company (org) level' : 'project level';
13192
13276
  const list = selectedTitles.map((t) => `- "${t}"`).join('\n');
13193
- 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.`;
13194
13278
  } else {
13195
13279
  msg = 'Run share-with-others. Show me everything I own at each level, grouped by type, so I can decide what to move.';
13196
13280
  }
@@ -2866,24 +2866,27 @@ button.small { padding: 4px 10px; font-size: 12px; }
2866
2866
  body:is([data-surface="task-pane"],[data-surface="extension"]). No media queries — this is a surface
2867
2867
  flag, not a viewport breakpoint. */
2868
2868
 
2869
- body:is([data-surface="task-pane"],[data-surface="extension"]) {
2870
- font-size: 13px;
2871
- overflow: auto;
2872
- }
2869
+ body:is([data-surface="task-pane"],[data-surface="extension"]) {
2870
+ font-size: 13px;
2871
+ overflow: auto;
2872
+ background: var(--bg);
2873
+ color: var(--text);
2874
+ }
2873
2875
  body:is([data-surface="task-pane"],[data-surface="extension"]) .hub-tabs,
2874
2876
  body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .hub-area,
2875
2877
  body:is([data-surface="task-pane"],[data-surface="extension"]) .proj-tabs {
2876
2878
  display: none !important;
2877
2879
  }
2878
2880
  .task-pane-launcher { display: none; }
2879
- body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .task-pane-launcher {
2880
- display: flex;
2881
- flex-direction: column;
2882
- gap: 10px;
2883
- padding: 12px;
2884
- border-bottom: 1px solid var(--border, rgba(0,0,0,0.07));
2885
- background: var(--surface, #fff);
2886
- }
2881
+ body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .task-pane-launcher {
2882
+ display: flex;
2883
+ flex-direction: column;
2884
+ gap: 10px;
2885
+ padding: 12px;
2886
+ border-bottom: 1px solid var(--line, rgba(0,0,0,0.07));
2887
+ background: var(--surface, #fff);
2888
+ color: var(--text, #171717);
2889
+ }
2887
2890
  body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-active #area-projects {
2888
2891
  display: flex !important;
2889
2892
  min-height: 100vh;
@@ -2940,17 +2943,27 @@ body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-act
2940
2943
  font-weight: 600;
2941
2944
  color: var(--muted);
2942
2945
  }
2943
- .tp-launcher-field select,
2944
- .tp-launcher-field textarea {
2945
- width: 100%;
2946
- min-width: 0;
2947
- border: 1px solid var(--border, rgba(0,0,0,0.12));
2948
- border-radius: 8px;
2949
- background: var(--panel, #fff);
2950
- color: var(--text, #171717);
2951
- font: inherit;
2952
- font-size: 12px;
2953
- }
2946
+ .tp-launcher-field select,
2947
+ .tp-launcher-field textarea {
2948
+ width: 100%;
2949
+ min-width: 0;
2950
+ border: 1px solid var(--line, rgba(0,0,0,0.12));
2951
+ border-radius: 8px;
2952
+ background: var(--surface, #fff);
2953
+ color: var(--text, #171717);
2954
+ font: inherit;
2955
+ font-size: 12px;
2956
+ }
2957
+ .tp-launcher-field select:focus,
2958
+ .tp-launcher-field textarea:focus {
2959
+ outline: 2px solid var(--accent, #0071e3);
2960
+ outline-offset: 1px;
2961
+ border-color: var(--accent, #0071e3);
2962
+ }
2963
+ .tp-launcher-field textarea::placeholder {
2964
+ color: var(--muted);
2965
+ opacity: 1;
2966
+ }
2954
2967
  .tp-launcher-field select {
2955
2968
  height: 34px;
2956
2969
  padding: 0 8px;
@@ -2987,8 +3000,9 @@ body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-act
2987
3000
  gap: 10px;
2988
3001
  min-width: 0;
2989
3002
  padding: 10px 12px;
2990
- border-bottom: 1px solid var(--border, rgba(0,0,0,0.07));
3003
+ border-bottom: 1px solid var(--line, rgba(0,0,0,0.07));
2991
3004
  background: var(--surface, #fff);
3005
+ color: var(--text, #171717);
2992
3006
  }
2993
3007
  .tp-run-copy {
2994
3008
  min-width: 0;
@@ -3022,6 +3036,31 @@ body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-new-jo
3022
3036
  font-size: 12px;
3023
3037
  white-space: nowrap;
3024
3038
  }
3039
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) {
3040
+ background: var(--bg);
3041
+ }
3042
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .task-pane-launcher,
3043
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-active .task-pane-run-nav {
3044
+ background: #242424;
3045
+ border-color: rgba(255, 255, 255, 0.16);
3046
+ }
3047
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field select,
3048
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field textarea {
3049
+ background: #171717;
3050
+ color: #f2f2f2;
3051
+ border-color: rgba(255, 255, 255, 0.22);
3052
+ }
3053
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field select option {
3054
+ background: #171717;
3055
+ color: #f2f2f2;
3056
+ }
3057
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-context,
3058
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-status,
3059
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field,
3060
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-run-eyebrow,
3061
+ :root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-run-meta {
3062
+ color: #c7c7c7;
3063
+ }
3025
3064
  body:is([data-surface="task-pane"],[data-surface="extension"]) .page {
3026
3065
  padding: 10px 12px 8px;
3027
3066
  gap: 10px;
@@ -4166,14 +4205,18 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4166
4205
  .modal-hdr { padding: 18px 22px; border-bottom: 1px solid var(--line); }
4167
4206
  .modal-hdr h2 { font-size: 18px; font-weight: 700; margin: 0 0 4px; }
4168
4207
  .modal-hdr p { font-size: 13px; color: var(--muted); margin: 0; line-height: 1.5; }
4169
- .modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
4170
- .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; }
4171
- .modal-close:hover { color: var(--text); }
4172
- .aom-cancel-btn { background: none; border: 1px solid var(--line); border-radius: 20px; padding: 9px 20px; font-size: 14px; cursor: pointer; color: var(--muted); }
4173
- .aom-cancel-btn:hover { color: var(--text); }
4174
- .modal-footer { padding: 12px 22px 20px; display: flex; align-items: center; justify-content: space-between; }
4175
- .modal-next { padding: 9px 22px; background: var(--text); color: #fff; border: none; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
4176
- .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; }
4177
4220
  .modal-step-dots { display: flex; gap: 6px; justify-content: center; margin-bottom: 18px; }
4178
4221
  .modal-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--line); }
4179
4222
  .modal-dot.on { background: var(--text); }
@@ -4630,7 +4673,8 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4630
4673
  align-items: center;
4631
4674
  justify-content: center;
4632
4675
  }
4633
- .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; }
4634
4678
 
4635
4679
  .np-step-dots {
4636
4680
  display: flex;
@@ -4692,26 +4736,29 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4692
4736
  justify-content: space-between;
4693
4737
  }
4694
4738
 
4695
- .np-next {
4696
- padding: 9px 22px;
4697
- background: var(--text);
4698
- color: #fff;
4699
- border: none;
4700
- border-radius: 20px;
4701
- font-size: 14px;
4702
- font-weight: 600;
4703
- cursor: pointer;
4704
- }
4705
- .np-next:hover { opacity: .88; }
4706
-
4707
- .np-back {
4708
- font-size: 13px;
4709
- color: var(--muted);
4710
- background: none;
4711
- border: none;
4712
- cursor: pointer;
4713
- }
4714
- .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; }
4715
4762
 
4716
4763
  /* ── Discard-guard confirm (shown when a dirty wizard is dismissed) ── */
4717
4764
  .np-discard {
@@ -4736,10 +4783,11 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4736
4783
  .np-discard-msg { font-size: 13px; color: var(--muted); margin: 0 0 18px; line-height: 1.5; }
4737
4784
  .np-discard-row { display: flex; gap: 8px; justify-content: flex-end; }
4738
4785
  .np-discard-row button { font: inherit; font-size: 13px; font-weight: 600; border-radius: 9px; padding: 8px 16px; cursor: pointer; }
4739
- .np-discard-keep { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
4740
- .np-discard-keep:hover { border-color: var(--muted); }
4741
- .np-discard-yes { background: var(--danger, #d2261f); color: #fff; border: none; }
4742
- .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; }
4743
4791
 
4744
4792
  /* ── D11: Source attachment row (step 2) ── */
4745
4793
  .np-source {
@@ -5226,8 +5274,9 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
5226
5274
  /* ── New-project: folder picker row ── */
5227
5275
  .np-folder-row { display:flex; gap:8px; }
5228
5276
  .np-folder-row input { flex:1; }
5229
- .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; }
5230
- .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; }
5231
5280
  .np-field-hint { font-size:11px; color:var(--muted); margin-top:5px; line-height:1.5; }
5232
5281
 
5233
5282
  @media (max-width: 520px) {