@codemem/server 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/static/app.js CHANGED
@@ -3280,9 +3280,26 @@
3280
3280
  this.guardrailWarnings = input.guardrail_warnings ?? [];
3281
3281
  }
3282
3282
  };
3283
+ var LegacySharedReviewConfirmationError = class extends Error {
3284
+ preview;
3285
+ constructor(preview) {
3286
+ super("Legacy shared review confirmation required");
3287
+ this.name = "LegacySharedReviewConfirmationError";
3288
+ this.preview = preview;
3289
+ }
3290
+ };
3283
3291
  async function loadSharingDomainSettings() {
3284
3292
  return fetchJson("/api/sync/sharing-domains/settings");
3285
3293
  }
3294
+ async function loadProjectScopeInventory(input = {}) {
3295
+ const params = new URLSearchParams();
3296
+ for (const [key, value] of Object.entries(input)) {
3297
+ if (value == null || value === "") continue;
3298
+ params.set(key, String(value));
3299
+ }
3300
+ const query = params.toString();
3301
+ return fetchJson(`/api/sync/projects${query ? `?${query}` : ""}`);
3302
+ }
3286
3303
  async function saveSharingDomainProjectMapping(input) {
3287
3304
  const resp = await fetch("/api/sync/sharing-domains/project-mappings", {
3288
3305
  method: "PUT",
@@ -3307,6 +3324,31 @@
3307
3324
  if (!resp.ok) throw new Error(payloadError(payload) || text || "request failed");
3308
3325
  return Boolean(payload?.deleted);
3309
3326
  }
3327
+ async function reassignProjectInventoryProject(input) {
3328
+ const resp = await fetch("/api/sync/projects/reassign-project", {
3329
+ method: "POST",
3330
+ headers: { "Content-Type": "application/json" },
3331
+ body: JSON.stringify(input)
3332
+ });
3333
+ const { text, payload } = await readJsonPayload(resp);
3334
+ if (!resp.ok) throw new Error(payloadError(payload) || text || "request failed");
3335
+ if (!payload?.workspace_identity) throw new Error("response missing project reassignment");
3336
+ return payload;
3337
+ }
3338
+ async function reassignLegacySharedReviewGroup$1(input) {
3339
+ const resp = await fetch("/api/sync/legacy-shared-review/reassign", {
3340
+ method: "POST",
3341
+ headers: { "Content-Type": "application/json" },
3342
+ body: JSON.stringify(input)
3343
+ });
3344
+ const { text, payload } = await readJsonPayload(resp);
3345
+ if (!resp.ok) {
3346
+ if (payload?.error === "legacy_review_confirmation_required" && payload.preview) throw new LegacySharedReviewConfirmationError(payload.preview);
3347
+ throw new Error(payloadError(payload) || text || "request failed");
3348
+ }
3349
+ if (!payload?.workspace_identity) throw new Error("response missing legacy review reassignment");
3350
+ return payload;
3351
+ }
3310
3352
  async function loadCoordinatorGroupPreferences(groupId) {
3311
3353
  const resp = await fetch(`/api/coordinator/admin/groups/${encodeURIComponent(groupId)}/preferences`);
3312
3354
  const { text, payload } = await readJsonPayload(resp);
@@ -3397,7 +3439,7 @@
3397
3439
  }
3398
3440
  //#endregion
3399
3441
  //#region src/lib/dom.ts
3400
- function el(tag, className, text) {
3442
+ function el$1(tag, className, text) {
3401
3443
  const node = document.createElement(tag);
3402
3444
  if (className) node.className = className;
3403
3445
  if (text !== void 0 && text !== null) node.textContent = String(text);
@@ -3445,8 +3487,9 @@
3445
3487
  //#region src/lib/state.ts
3446
3488
  var ALL_TAB_IDS = [
3447
3489
  "feed",
3448
- "health",
3490
+ "projects",
3449
3491
  "sync",
3492
+ "health",
3450
3493
  "coordinator-admin"
3451
3494
  ];
3452
3495
  var TAB_KEY = "codemem-tab";
@@ -14344,6 +14387,385 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
14344
14387
  if (state.activeTab === "feed" && previousActorId !== nextActorId) updateFeedView(true);
14345
14388
  }
14346
14389
  //#endregion
14390
+ //#region src/tabs/projects.ts
14391
+ var STATUS_OPTIONS = [
14392
+ ["", "All projects"],
14393
+ ["needs_attention", "Needs review"],
14394
+ ["suggested", "Has suggestion"],
14395
+ ["local_only", "Stays on this device"],
14396
+ ["explicitly_mapped", "Already assigned"],
14397
+ ["legacy_review", "Older shared data"],
14398
+ ["unmapped", "Missing project identity"]
14399
+ ];
14400
+ var refreshProjects = null;
14401
+ var currentOffset = 0;
14402
+ var lastLimit = 25;
14403
+ var scopes = [];
14404
+ var openProjectDetails = /* @__PURE__ */ new Set();
14405
+ var draftDomainSelections = /* @__PURE__ */ new Map();
14406
+ var pendingConfirmations = /* @__PURE__ */ new Map();
14407
+ function el(id) {
14408
+ return document.getElementById(id);
14409
+ }
14410
+ function formatStatus(status) {
14411
+ return STATUS_OPTIONS.find(([value]) => value === status)?.[1] ?? status.replaceAll("_", " ");
14412
+ }
14413
+ function formatResolution(reason) {
14414
+ switch (reason) {
14415
+ case "exact_mapping": return "assigned to a Sharing domain";
14416
+ case "pattern_mapping": return "assigned by matching rule";
14417
+ case "explicit_override": return "manually assigned";
14418
+ default: return "stays on this device";
14419
+ }
14420
+ }
14421
+ function formatLatest(value) {
14422
+ if (!value) return "No recent sessions";
14423
+ const date = new Date(value);
14424
+ if (Number.isNaN(date.getTime())) return value;
14425
+ return date.toLocaleString();
14426
+ }
14427
+ function strongestSignal(project) {
14428
+ if (project.git_remote) return project.git_remote;
14429
+ if (project.cwd) return project.cwd;
14430
+ return project.workspace_identity;
14431
+ }
14432
+ function scopeLabel(scopeId) {
14433
+ if (!scopeId) return "—";
14434
+ const scope = scopes.find((item) => item.scope_id === scopeId);
14435
+ return scope?.label ? `${scope.label} (${scope.scope_id})` : scopeId;
14436
+ }
14437
+ function assignableScopes() {
14438
+ return scopes.filter((scope) => scope.scope_id !== "legacy-shared-review");
14439
+ }
14440
+ function guardrailHeading(warning) {
14441
+ switch (warning.code) {
14442
+ case "unknown_project_local_only": return "Current behavior";
14443
+ case "basename_collision_review": return "Name collision";
14444
+ case "scope_reassignment_old_copies": return "Previous copies";
14445
+ case "broad_org_domain_pattern":
14446
+ case "home_directory_org_domain_pattern": return "Broad mapping";
14447
+ default: return "Review item";
14448
+ }
14449
+ }
14450
+ async function saveProjectMapping(project, scopeId, confirmedGuardrailTokens = []) {
14451
+ try {
14452
+ await saveSharingDomainProjectMapping({
14453
+ ...project.mapping_id && project.resolution_reason === "exact_mapping" ? { id: project.mapping_id } : {},
14454
+ ...confirmedGuardrailTokens.length > 0 ? { confirmed_guardrail_tokens: confirmedGuardrailTokens } : {},
14455
+ project_pattern: project.display_project,
14456
+ scope_id: scopeId,
14457
+ workspace_identity: project.workspace_identity
14458
+ });
14459
+ pendingConfirmations.delete(project.workspace_identity);
14460
+ draftDomainSelections.delete(project.workspace_identity);
14461
+ showGlobalNotice("Project Sharing domain updated. Device access grants are unchanged.");
14462
+ refreshProjects?.();
14463
+ } catch (error) {
14464
+ if (error instanceof SharingDomainGuardrailConfirmationError) {
14465
+ pendingConfirmations.set(project.workspace_identity, {
14466
+ requiredGuardrailTokens: error.requiredGuardrailTokens,
14467
+ scopeId,
14468
+ warnings: error.guardrailWarnings
14469
+ });
14470
+ refreshProjects?.();
14471
+ return;
14472
+ }
14473
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to update project Sharing domain.", "warning");
14474
+ }
14475
+ }
14476
+ async function removeProjectMapping(project) {
14477
+ if (project.mapping_id == null) return;
14478
+ try {
14479
+ await deleteSharingDomainProjectMapping(project.mapping_id);
14480
+ pendingConfirmations.delete(project.workspace_identity);
14481
+ draftDomainSelections.delete(project.workspace_identity);
14482
+ showGlobalNotice("Project Sharing domain mapping removed. The next fallback now applies.");
14483
+ refreshProjects?.();
14484
+ } catch (error) {
14485
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to remove project Sharing domain mapping.", "warning");
14486
+ }
14487
+ }
14488
+ async function reassignProject(project) {
14489
+ if (project.identity_source === "unmapped") return;
14490
+ const currentProject = String(project.project || project.display_project || "").trim();
14491
+ let suggestions = [];
14492
+ try {
14493
+ suggestions = (await loadProjects$1()).filter((name) => name && name !== currentProject);
14494
+ } catch {}
14495
+ const nextProject = await openSyncInputDialog({
14496
+ cancelLabel: "Cancel",
14497
+ confirmLabel: "Change project",
14498
+ description: `This will update ${project.session_count} session${project.session_count === 1 ? "" : "s"} and ${project.memory_count ?? 0} memor${project.memory_count === 1 ? "y" : "ies"} by changing the stored project. Sharing-domain assignment stays unchanged.`,
14499
+ initialValue: currentProject,
14500
+ placeholder: "Project name",
14501
+ suggestions,
14502
+ title: "Change project",
14503
+ validate: (value) => {
14504
+ const trimmed = value.trim();
14505
+ if (!trimmed) return "Enter a project name.";
14506
+ if (trimmed === currentProject) return "Already assigned to this project.";
14507
+ return null;
14508
+ }
14509
+ });
14510
+ if (nextProject == null) return;
14511
+ try {
14512
+ const result = await reassignProjectInventoryProject({
14513
+ project: nextProject.trim(),
14514
+ workspace_identity: project.workspace_identity
14515
+ });
14516
+ showGlobalNotice(`Changed project to ${result.project} for ${result.moved_session_count} session${result.moved_session_count === 1 ? "" : "s"}.`);
14517
+ refreshProjects?.();
14518
+ } catch (error) {
14519
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to change project.", "warning");
14520
+ }
14521
+ }
14522
+ function renderProjectActions(project) {
14523
+ const actions = document.createElement("div");
14524
+ actions.className = "project-inventory-actions";
14525
+ if (project.identity_source === "unmapped") {
14526
+ const note = document.createElement("div");
14527
+ note.className = "settings-note";
14528
+ note.textContent = "This project is missing a stable path, git remote, or workspace id. It stays Local only until it has a stable identity.";
14529
+ actions.appendChild(note);
14530
+ return actions;
14531
+ }
14532
+ const label = document.createElement("label");
14533
+ label.className = "sr-only";
14534
+ const selectId = `project-domain-${project.workspace_identity.replace(/[^a-z0-9_-]/gi, "-")}`;
14535
+ label.htmlFor = selectId;
14536
+ label.textContent = `Sharing domain for ${project.display_project}`;
14537
+ const select = document.createElement("select");
14538
+ select.id = selectId;
14539
+ select.className = "project-domain-select";
14540
+ const currentAssignable = assignableScopes().some((scope) => scope.scope_id === project.resolved_scope_id);
14541
+ if (!currentAssignable && project.resolved_scope_id) {
14542
+ const current = document.createElement("option");
14543
+ current.value = project.resolved_scope_id;
14544
+ current.textContent = `${scopeLabel(project.resolved_scope_id)} — not assignable`;
14545
+ current.disabled = true;
14546
+ select.appendChild(current);
14547
+ }
14548
+ for (const scope of assignableScopes()) {
14549
+ const option = document.createElement("option");
14550
+ option.value = scope.scope_id;
14551
+ option.textContent = scope.label ? `${scope.label} · ${scope.scope_id}` : scope.scope_id;
14552
+ select.appendChild(option);
14553
+ }
14554
+ select.value = draftDomainSelections.get(project.workspace_identity) ?? project.suggested_scope_id ?? project.resolved_scope_id;
14555
+ const save = document.createElement("button");
14556
+ save.className = "settings-button";
14557
+ save.type = "button";
14558
+ save.textContent = project.suggested_scope_id && select.value === project.suggested_scope_id ? "Confirm suggestion" : "Save domain";
14559
+ save.disabled = select.value === project.resolved_scope_id && !currentAssignable;
14560
+ save.addEventListener("click", () => void saveProjectMapping(project, select.value));
14561
+ select.addEventListener("change", () => {
14562
+ draftDomainSelections.set(project.workspace_identity, select.value);
14563
+ pendingConfirmations.delete(project.workspace_identity);
14564
+ save.textContent = "Save domain";
14565
+ save.disabled = false;
14566
+ refreshProjects?.();
14567
+ });
14568
+ const keepLocal = document.createElement("button");
14569
+ keepLocal.className = "settings-button";
14570
+ keepLocal.type = "button";
14571
+ keepLocal.textContent = "Keep local-only";
14572
+ keepLocal.addEventListener("click", () => void saveProjectMapping(project, "local-default"));
14573
+ const remove = document.createElement("button");
14574
+ remove.className = "settings-button";
14575
+ remove.type = "button";
14576
+ remove.textContent = "Remove mapping";
14577
+ remove.disabled = project.mapping_id == null || project.resolution_reason !== "exact_mapping";
14578
+ remove.addEventListener("click", () => void removeProjectMapping(project));
14579
+ const changeProject = document.createElement("button");
14580
+ changeProject.className = "settings-button";
14581
+ changeProject.type = "button";
14582
+ changeProject.textContent = "Change project…";
14583
+ changeProject.disabled = project.session_count === 0;
14584
+ if (changeProject.disabled) changeProject.title = "No sessions are available to reassign for this saved mapping.";
14585
+ changeProject.addEventListener("click", () => void reassignProject(project));
14586
+ actions.append(label, select, save, keepLocal, remove, changeProject);
14587
+ const pending = pendingConfirmations.get(project.workspace_identity);
14588
+ if (pending) {
14589
+ const warningBox = document.createElement("div");
14590
+ warningBox.className = "settings-note project-guardrail-confirmation";
14591
+ warningBox.setAttribute("role", "alert");
14592
+ const title = document.createElement("strong");
14593
+ title.textContent = "Confirmation required before saving this Sharing domain.";
14594
+ const intro = document.createElement("p");
14595
+ intro.textContent = "Codemem can save this change after you acknowledge the checks below. Verify the workspace details, then confirm to complete the save.";
14596
+ const list = document.createElement("ul");
14597
+ for (const warning of pending.warnings) {
14598
+ const item = document.createElement("li");
14599
+ const itemTitle = document.createElement("strong");
14600
+ itemTitle.textContent = `${guardrailHeading(warning)}: `;
14601
+ const message = document.createElement("span");
14602
+ message.textContent = warning.message;
14603
+ item.append(itemTitle, message);
14604
+ list.appendChild(item);
14605
+ }
14606
+ const confirm = document.createElement("button");
14607
+ confirm.className = "settings-button";
14608
+ confirm.type = "button";
14609
+ confirm.textContent = "I understand, save domain";
14610
+ confirm.addEventListener("click", () => {
14611
+ const currentPending = pendingConfirmations.get(project.workspace_identity);
14612
+ if (!currentPending || currentPending.scopeId !== select.value) return;
14613
+ saveProjectMapping(project, currentPending.scopeId, currentPending.requiredGuardrailTokens);
14614
+ });
14615
+ const cancel = document.createElement("button");
14616
+ cancel.className = "settings-button";
14617
+ cancel.type = "button";
14618
+ cancel.textContent = "Cancel";
14619
+ cancel.addEventListener("click", () => {
14620
+ pendingConfirmations.delete(project.workspace_identity);
14621
+ refreshProjects?.();
14622
+ });
14623
+ warningBox.append(title, intro, list, confirm, cancel);
14624
+ actions.appendChild(warningBox);
14625
+ }
14626
+ return actions;
14627
+ }
14628
+ function renderProjectRow(project) {
14629
+ const row = document.createElement("article");
14630
+ row.className = "project-inventory-row";
14631
+ const header = document.createElement("div");
14632
+ header.className = "project-inventory-row-header";
14633
+ const title = document.createElement("div");
14634
+ title.className = "project-inventory-title";
14635
+ title.textContent = project.display_project;
14636
+ header.appendChild(title);
14637
+ const domain = document.createElement("div");
14638
+ domain.className = "project-inventory-domain";
14639
+ domain.textContent = scopeLabel(project.resolved_scope_id);
14640
+ header.appendChild(domain);
14641
+ row.appendChild(header);
14642
+ const meta = document.createElement("div");
14643
+ meta.className = "project-inventory-meta";
14644
+ meta.textContent = `${formatResolution(project.resolution_reason)} · ${project.identity_source} · ${formatLatest(project.latest_session_at)}`;
14645
+ row.appendChild(meta);
14646
+ const signal = document.createElement("div");
14647
+ signal.className = "project-inventory-signal mono";
14648
+ signal.textContent = strongestSignal(project);
14649
+ row.appendChild(signal);
14650
+ if (project.suggested_scope_id && project.suggested_scope_id !== project.resolved_scope_id) {
14651
+ const suggestion = document.createElement("div");
14652
+ suggestion.className = "settings-note project-suggestion-note";
14653
+ suggestion.textContent = project.suggestion_reason ? `Suggestion: ${project.suggestion_reason}` : `Suggestion: map this project to ${scopeLabel(project.suggested_scope_id)}.`;
14654
+ row.appendChild(suggestion);
14655
+ }
14656
+ const warnings = project.guardrail_warnings.filter((warning) => warning.severity === "warning");
14657
+ if (warnings.length > 0) {
14658
+ const warningBox = document.createElement("div");
14659
+ warningBox.className = "settings-note project-attention-note";
14660
+ warningBox.textContent = `Needs attention: ${warnings.map((warning) => warning.message).join(" ")}`;
14661
+ row.appendChild(warningBox);
14662
+ }
14663
+ if (project.statuses.length > 0) {
14664
+ const badges = document.createElement("div");
14665
+ badges.className = "project-inventory-badges";
14666
+ for (const status of project.statuses) {
14667
+ const badge = document.createElement("span");
14668
+ badge.className = `project-status-badge ${status}`;
14669
+ badge.textContent = formatStatus(status);
14670
+ badges.appendChild(badge);
14671
+ }
14672
+ row.appendChild(badges);
14673
+ }
14674
+ const detail = document.createElement("details");
14675
+ detail.className = "project-inventory-details";
14676
+ detail.open = openProjectDetails.has(project.workspace_identity);
14677
+ detail.addEventListener("toggle", () => {
14678
+ if (detail.open) openProjectDetails.add(project.workspace_identity);
14679
+ else openProjectDetails.delete(project.workspace_identity);
14680
+ });
14681
+ const summary = document.createElement("summary");
14682
+ summary.textContent = "Identity and mapping details";
14683
+ detail.appendChild(summary);
14684
+ const list = document.createElement("dl");
14685
+ list.className = "project-detail-grid";
14686
+ const fields = [
14687
+ ["Workspace identity", project.workspace_identity],
14688
+ ["Project", project.project],
14689
+ ["CWD", project.cwd],
14690
+ ["Git remote", project.git_remote],
14691
+ ["Git branch", project.git_branch],
14692
+ ["Suggested domain", project.suggested_scope_id],
14693
+ ["Suggestion reason", project.suggestion_reason],
14694
+ ["Sessions", project.session_count],
14695
+ ["Memories", project.memory_count ?? "count unavailable"]
14696
+ ];
14697
+ for (const [label, value] of fields) {
14698
+ const dt = document.createElement("dt");
14699
+ dt.textContent = label;
14700
+ const dd = document.createElement("dd");
14701
+ dd.textContent = value == null || value === "" ? "—" : String(value);
14702
+ list.append(dt, dd);
14703
+ }
14704
+ detail.appendChild(list);
14705
+ detail.appendChild(renderProjectActions(project));
14706
+ row.appendChild(detail);
14707
+ return row;
14708
+ }
14709
+ function renderEmpty(message) {
14710
+ const list = el("projectsInventoryList");
14711
+ if (!list) return;
14712
+ list.textContent = "";
14713
+ const empty = document.createElement("div");
14714
+ empty.className = "settings-note";
14715
+ empty.textContent = message;
14716
+ list.appendChild(empty);
14717
+ }
14718
+ async function loadProjectsData() {
14719
+ const meta = el("projectsInventoryMeta");
14720
+ const list = el("projectsInventoryList");
14721
+ if (!meta || !list) return;
14722
+ meta.textContent = "Loading project inventory…";
14723
+ try {
14724
+ const [result, settings] = await Promise.all([loadProjectScopeInventory({
14725
+ limit: lastLimit,
14726
+ offset: currentOffset,
14727
+ q: el("projectsSearch")?.value.trim() || void 0,
14728
+ status: el("projectsStatusFilter")?.value || void 0
14729
+ }), loadSharingDomainSettings()]);
14730
+ scopes = settings.scopes;
14731
+ list.textContent = "";
14732
+ if (result.projects.length === 0) renderEmpty("No projects match those filters.");
14733
+ else for (const project of result.projects) list.appendChild(renderProjectRow(project));
14734
+ meta.textContent = result.total === 0 ? "0 projects found" : `${result.total} project${result.total === 1 ? "" : "s"} found · showing ${result.offset + 1}-${Math.min(result.offset + result.projects.length, result.total)}`;
14735
+ const prev = el("projectsPrevPage");
14736
+ const next = el("projectsNextPage");
14737
+ if (prev) prev.disabled = result.offset === 0;
14738
+ if (next) next.disabled = !result.has_more;
14739
+ } catch (error) {
14740
+ meta.textContent = "Project inventory failed to load.";
14741
+ renderEmpty(error instanceof Error ? error.message : "Unable to load project inventory.");
14742
+ }
14743
+ }
14744
+ function initProjectsTab(refresh) {
14745
+ refreshProjects = refresh;
14746
+ const status = el("projectsStatusFilter");
14747
+ if (status && status.options.length === 0) for (const [value, label] of STATUS_OPTIONS) {
14748
+ const option = document.createElement("option");
14749
+ option.value = value;
14750
+ option.textContent = label;
14751
+ status.appendChild(option);
14752
+ }
14753
+ const requestRefresh = () => {
14754
+ currentOffset = 0;
14755
+ refreshProjects?.();
14756
+ };
14757
+ el("projectsSearch")?.addEventListener("input", requestRefresh);
14758
+ status?.addEventListener("change", requestRefresh);
14759
+ el("projectsPrevPage")?.addEventListener("click", () => {
14760
+ currentOffset = Math.max(0, currentOffset - lastLimit);
14761
+ refreshProjects?.();
14762
+ });
14763
+ el("projectsNextPage")?.addEventListener("click", () => {
14764
+ currentOffset += lastLimit;
14765
+ refreshProjects?.();
14766
+ });
14767
+ }
14768
+ //#endregion
14347
14769
  //#region src/tabs/settings/data/parse.ts
14348
14770
  function parseCommandArgv(raw, options) {
14349
14771
  const text = raw.trim();
@@ -15964,7 +16386,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
15964
16386
  return scopes.find((scope) => scope.scope_id === scopeId)?.label || scopeId;
15965
16387
  }
15966
16388
  function resetDrafts(settings) {
15967
- return Object.fromEntries(settings.projects.map((project) => [project.workspace_identity, project.resolved_scope_id]));
16389
+ return Object.fromEntries(settings.projects.map((project) => [project.workspace_identity, project.suggested_scope_id ?? project.resolved_scope_id]));
15968
16390
  }
15969
16391
  function canAssignProject(project) {
15970
16392
  return project.identity_source !== "unmapped";
@@ -15980,6 +16402,9 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
15980
16402
  function visibleProjectWarnings(project) {
15981
16403
  return (project.guardrail_warnings ?? []).filter((warning) => warning.severity === "warning" || warning.code === "unknown_project_local_only");
15982
16404
  }
16405
+ function isUsingSuggestion(project, currentValue) {
16406
+ return Boolean(project.suggested_scope_id && currentValue === project.suggested_scope_id && project.suggested_scope_id !== project.resolved_scope_id);
16407
+ }
15983
16408
  function settingsMappingWarnings(settings) {
15984
16409
  return settings.mappings.flatMap((mapping) => mapping.guardrail_warnings ?? []);
15985
16410
  }
@@ -16015,7 +16440,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16015
16440
  y$2(() => {
16016
16441
  reload();
16017
16442
  }, []);
16018
- const scopeOptions = T$1(() => (settings?.scopes ?? []).map((scope) => ({
16443
+ const scopeOptions = T$1(() => (settings?.scopes ?? []).filter((scope) => scope.scope_id !== "legacy-shared-review").map((scope) => ({
16019
16444
  value: scope.scope_id,
16020
16445
  label: domainLabel(scope)
16021
16446
  })), [settings?.scopes]);
@@ -16070,20 +16495,32 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16070
16495
  setSavingKey(null);
16071
16496
  }
16072
16497
  };
16498
+ const openProjects = () => {
16499
+ window.location.hash = "projects";
16500
+ };
16073
16501
  return /* @__PURE__ */ u("div", {
16074
16502
  className: "settings-group",
16075
16503
  children: [
16076
16504
  /* @__PURE__ */ u("h3", {
16077
16505
  className: "settings-group-title",
16078
- children: "Sharing domains"
16506
+ children: "Advanced Sharing-domain mappings"
16079
16507
  }),
16080
16508
  /* @__PURE__ */ u("div", {
16081
16509
  className: "small",
16082
- children: "Map known projects to their default Sharing domain. This changes local scope resolution for future writes; it does not grant any peer or coordinator member access by itself."
16510
+ children: "Use Projects for day-to-day project/domain review. This fallback panel exposes the same mapping controls for troubleshooting and recovery."
16083
16511
  }),
16084
16512
  /* @__PURE__ */ u("div", {
16085
16513
  className: "small",
16086
- children: "Unmapped and unknown projects stay on Local only until you assign a domain."
16514
+ children: "Saving a mapping changes local scope resolution for future writes; it does not grant any peer or coordinator member access by itself."
16515
+ }),
16516
+ /* @__PURE__ */ u("div", {
16517
+ className: "section-actions",
16518
+ children: /* @__PURE__ */ u("button", {
16519
+ className: "settings-button",
16520
+ onClick: openProjects,
16521
+ type: "button",
16522
+ children: "Open Projects review"
16523
+ })
16087
16524
  }),
16088
16525
  settings ? /* @__PURE__ */ u(GuardrailMessages, {
16089
16526
  label: "Sharing domain mapping warnings",
@@ -16109,6 +16546,8 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16109
16546
  const unchanged = currentValue === project.resolved_scope_id;
16110
16547
  const assignable = canAssignProject(project);
16111
16548
  const currentScopeName = scopeName(settings.scopes, project.resolved_scope_id);
16549
+ const usingSuggestion = isUsingSuggestion(project, currentValue);
16550
+ const suggestedScopeName = project.suggested_scope_id ? scopeName(settings.scopes, project.suggested_scope_id) : null;
16112
16551
  const pendingConfirmation = confirmation?.workspaceIdentity === project.workspace_identity ? confirmation : null;
16113
16552
  const canRemoveProjectMapping = assignable && project.mapping_id != null && project.resolution_reason === "exact_mapping";
16114
16553
  return /* @__PURE__ */ u("div", {
@@ -16150,6 +16589,15 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16150
16589
  resolutionLabel(project.resolution_reason)
16151
16590
  ]
16152
16591
  }),
16592
+ project.suggested_scope_id && project.suggestion_reason ? /* @__PURE__ */ u("div", {
16593
+ className: "settings-note",
16594
+ role: "status",
16595
+ children: [
16596
+ /* @__PURE__ */ u("strong", { children: ["Suggested mapping: ", suggestedScopeName] }),
16597
+ /* @__PURE__ */ u("div", { children: project.suggestion_reason }),
16598
+ /* @__PURE__ */ u("div", { children: "Folders, paths, and git remotes are hints only. Saving this mapping requires your confirmation and does not grant any peer access." })
16599
+ ]
16600
+ }) : null,
16153
16601
  /* @__PURE__ */ u(GuardrailMessages, { warnings: visibleProjectWarnings(project) }),
16154
16602
  !assignable ? /* @__PURE__ */ u("div", {
16155
16603
  className: "settings-note",
@@ -16186,7 +16634,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16186
16634
  disabled: !assignable || saving || unchanged,
16187
16635
  onClick: () => void saveProject(project),
16188
16636
  type: "button",
16189
- children: saving ? "Saving…" : "Save Sharing domain"
16637
+ children: saving ? "Saving…" : usingSuggestion ? "Confirm suggested mapping" : "Save Sharing domain"
16190
16638
  }), /* @__PURE__ */ u("button", {
16191
16639
  className: "settings-button",
16192
16640
  disabled: saving || !canRemoveProjectMapping,
@@ -17346,6 +17794,90 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17346
17794
  emptyMessage: "No Sharing-domain grants exist for this device yet. Project filters below cannot send data by themselves."
17347
17795
  };
17348
17796
  }
17797
+ var PERSONAL_SCOPE_WORDS = [
17798
+ "personal",
17799
+ "private",
17800
+ "home",
17801
+ "mine",
17802
+ "me"
17803
+ ];
17804
+ var OSS_SCOPE_WORDS = [
17805
+ "oss",
17806
+ "open source",
17807
+ "opensource",
17808
+ "community",
17809
+ "public"
17810
+ ];
17811
+ var WORK_LIKE_SCOPE_WORDS = ["work", "client"];
17812
+ var LOCAL_SCOPE_WORDS = [
17813
+ "local",
17814
+ "legacy",
17815
+ "review"
17816
+ ];
17817
+ function scopeSearchText(scope) {
17818
+ return [
17819
+ scope.scope_id,
17820
+ scope.label,
17821
+ scope.kind,
17822
+ scope.authority_type
17823
+ ].map((value) => cleanText(value) ?? "").join(" ").toLowerCase();
17824
+ }
17825
+ function scopeIdentitySearchText(scope) {
17826
+ return [
17827
+ scope.scope_id,
17828
+ scope.label,
17829
+ scope.kind
17830
+ ].map((value) => cleanText(value) ?? "").join(" ").toLowerCase();
17831
+ }
17832
+ function escapeRegex(value) {
17833
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17834
+ }
17835
+ function hasScopeWord(scope, words) {
17836
+ const text = scopeSearchText(scope);
17837
+ return words.some((word) => {
17838
+ const escaped = escapeRegex(word.toLowerCase());
17839
+ return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`).test(text);
17840
+ });
17841
+ }
17842
+ function isPersonalOrOssScope(scope) {
17843
+ return hasScopeWord(scope, PERSONAL_SCOPE_WORDS) || hasScopeWord(scope, OSS_SCOPE_WORDS);
17844
+ }
17845
+ function isLocalOrLegacyScope(scope) {
17846
+ const text = scopeIdentitySearchText(scope);
17847
+ return LOCAL_SCOPE_WORDS.some((word) => {
17848
+ const escaped = escapeRegex(word.toLowerCase());
17849
+ return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`).test(text);
17850
+ });
17851
+ }
17852
+ function isWorkLikeScope(scope) {
17853
+ return !isPersonalOrOssScope(scope) && (!isLocalOrLegacyScope(scope) || hasScopeWord(scope, WORK_LIKE_SCOPE_WORDS));
17854
+ }
17855
+ function derivePeerGrantRoleMismatchView(peer) {
17856
+ const scopes = Array.isArray(peer.authorized_scopes) ? peer.authorized_scopes : [];
17857
+ if (!Boolean(cleanText(peer.discovered_via_group_id) || cleanText(peer.discovered_via_coordinator_id)) || scopes.length === 0) return {
17858
+ badgeLabel: null,
17859
+ detail: "",
17860
+ isVisible: false,
17861
+ message: "",
17862
+ title: ""
17863
+ };
17864
+ const hasPersonalOrOssGrant = scopes.some(isPersonalOrOssScope);
17865
+ const hasWorkLikeGrant = scopes.some(isWorkLikeScope);
17866
+ if (!hasPersonalOrOssGrant || hasWorkLikeGrant) return {
17867
+ badgeLabel: null,
17868
+ detail: "",
17869
+ isVisible: false,
17870
+ message: "",
17871
+ title: ""
17872
+ };
17873
+ return {
17874
+ badgeLabel: "Review domain fit",
17875
+ detail: "Coordinator/group context helps find the device, and project filters only narrow already-authorized domains. Neither one grants a missing work/client domain.",
17876
+ isVisible: true,
17877
+ message: "This coordinator-discovered device has personal or OSS Sharing-domain grants, but no separate work/client-like domain grant. If this device is meant for work/client sync, add that Sharing domain explicitly before treating sync as validated.",
17878
+ title: "Check this device's Sharing-domain role"
17879
+ };
17880
+ }
17349
17881
  function derivePeerProjectNarrowingView(scope) {
17350
17882
  const effectiveInclude = cleanList(scope?.effective_include);
17351
17883
  const effectiveExclude = cleanList(scope?.effective_exclude);
@@ -17714,23 +18246,23 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17714
18246
  let query = "";
17715
18247
  let activeIndex = 0;
17716
18248
  let popoverOpen = false;
17717
- const container = el("div", "project-scope-picker");
17718
- const selectedList = el("ul", "project-scope-picker-selected");
17719
- const trigger = el("button", "project-scope-picker-trigger settings-button");
18249
+ const container = el$1("div", "project-scope-picker");
18250
+ const selectedList = el$1("ul", "project-scope-picker-selected");
18251
+ const trigger = el$1("button", "project-scope-picker-trigger settings-button");
17720
18252
  trigger.type = "button";
17721
- const triggerLabel = el("span", null, placeholder || "Add project");
17722
- const triggerPlus = el("span", null, "+");
18253
+ const triggerLabel = el$1("span", null, placeholder || "Add project");
18254
+ const triggerPlus = el$1("span", null, "+");
17723
18255
  triggerPlus.setAttribute("aria-hidden", "true");
17724
18256
  trigger.append(triggerPlus, triggerLabel);
17725
18257
  trigger.setAttribute("aria-haspopup", "listbox");
17726
18258
  trigger.setAttribute("aria-expanded", "false");
17727
- const popover = el("div", "project-scope-picker-popover project-scope-picker-popover--inline");
18259
+ const popover = el$1("div", "project-scope-picker-popover project-scope-picker-popover--inline");
17728
18260
  popover.hidden = true;
17729
- const search = el("input", "project-scope-picker-search");
18261
+ const search = el$1("input", "project-scope-picker-search");
17730
18262
  search.type = "text";
17731
18263
  search.placeholder = "Search or create…";
17732
18264
  search.setAttribute("aria-label", "Search projects");
17733
- const results = el("div", "project-scope-picker-results");
18265
+ const results = el$1("div", "project-scope-picker-results");
17734
18266
  results.setAttribute("role", "listbox");
17735
18267
  popover.append(search, results);
17736
18268
  const knownPool = () => {
@@ -17783,14 +18315,14 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17783
18315
  const renderSelected = () => {
17784
18316
  selectedList.textContent = "";
17785
18317
  if (!values.length) {
17786
- const note = el("li", "project-scope-picker-selected-empty", emptyLabel);
18318
+ const note = el$1("li", "project-scope-picker-selected-empty", emptyLabel);
17787
18319
  selectedList.appendChild(note);
17788
18320
  return;
17789
18321
  }
17790
18322
  values.forEach((value) => {
17791
- const chip = el("li", "project-scope-picker-selected-chip");
17792
- const label = el("span", null, value);
17793
- const remove = el("button", "project-scope-picker-selected-remove", "×");
18323
+ const chip = el$1("li", "project-scope-picker-selected-chip");
18324
+ const label = el$1("span", null, value);
18325
+ const remove = el$1("button", "project-scope-picker-selected-remove", "×");
17794
18326
  remove.type = "button";
17795
18327
  remove.setAttribute("aria-label", `Remove ${value}`);
17796
18328
  remove.addEventListener("click", () => {
@@ -17809,19 +18341,19 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17809
18341
  const rowCount = rows.length + (withCreate ? 1 : 0);
17810
18342
  if (activeIndex >= rowCount) activeIndex = Math.max(0, rowCount - 1);
17811
18343
  if (!rows.length && !withCreate) {
17812
- const empty = el("div", "project-scope-picker-empty-row", "No projects yet. Type a name to create one.");
18344
+ const empty = el$1("div", "project-scope-picker-empty-row", "No projects yet. Type a name to create one.");
17813
18345
  results.appendChild(empty);
17814
18346
  return;
17815
18347
  }
17816
18348
  rows.forEach((project, index) => {
17817
18349
  const selected = values.includes(project);
17818
- const row = el("button", index === activeIndex ? "project-scope-picker-row project-scope-picker-row--active" : "project-scope-picker-row");
18350
+ const row = el$1("button", index === activeIndex ? "project-scope-picker-row project-scope-picker-row--active" : "project-scope-picker-row");
17819
18351
  row.type = "button";
17820
18352
  row.setAttribute("role", "option");
17821
18353
  row.setAttribute("aria-selected", selected ? "true" : "false");
17822
- const check = el("span", "project-scope-picker-row-check", selected ? "✓" : "");
18354
+ const check = el$1("span", "project-scope-picker-row-check", selected ? "✓" : "");
17823
18355
  check.setAttribute("aria-hidden", "true");
17824
- const labelEl = el("span", "project-scope-picker-row-label", project);
18356
+ const labelEl = el$1("span", "project-scope-picker-row-label", project);
17825
18357
  row.append(check, labelEl);
17826
18358
  row.addEventListener("click", () => {
17827
18359
  activeIndex = index;
@@ -17835,12 +18367,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17835
18367
  });
17836
18368
  if (withCreate) {
17837
18369
  const index = rows.length;
17838
- const row = el("button", index === activeIndex ? "project-scope-picker-row project-scope-picker-row--create project-scope-picker-row--active" : "project-scope-picker-row project-scope-picker-row--create");
18370
+ const row = el$1("button", index === activeIndex ? "project-scope-picker-row project-scope-picker-row--create project-scope-picker-row--active" : "project-scope-picker-row project-scope-picker-row--create");
17839
18371
  row.type = "button";
17840
18372
  row.setAttribute("role", "option");
17841
- const check = el("span", "project-scope-picker-row-check", "+");
18373
+ const check = el$1("span", "project-scope-picker-row-check", "+");
17842
18374
  check.setAttribute("aria-hidden", "true");
17843
- const labelEl = el("span", "project-scope-picker-row-label", `Create "${query.trim()}"`);
18375
+ const labelEl = el$1("span", "project-scope-picker-row-label", `Create "${query.trim()}"`);
17844
18376
  row.append(check, labelEl);
17845
18377
  row.addEventListener("click", () => createFromQuery());
17846
18378
  row.addEventListener("mouseenter", () => {
@@ -17920,11 +18452,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17920
18452
  }
17921
18453
  container.hidden = false;
17922
18454
  actions.slice(0, 2).forEach((item) => {
17923
- const row = el("div", "sync-action");
17924
- const textWrap = el("div", "sync-action-text");
18455
+ const row = el$1("div", "sync-action");
18456
+ const textWrap = el$1("div", "sync-action-text");
17925
18457
  textWrap.textContent = item.label;
17926
- textWrap.appendChild(el("span", "sync-action-command", item.command));
17927
- const btn = el("button", "settings-button sync-action-copy", "Copy");
18458
+ textWrap.appendChild(el$1("span", "sync-action-command", item.command));
18459
+ const btn = el$1("button", "settings-button sync-action-copy", "Copy");
17928
18460
  btn.addEventListener("click", () => copyToClipboard(item.command, btn));
17929
18461
  row.append(textWrap, btn);
17930
18462
  container.appendChild(row);
@@ -18645,6 +19177,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
18645
19177
  const directionHint = DIRECTION_GLYPH[derivePeerDirection(peer)];
18646
19178
  const peerStatus = peer.status || {};
18647
19179
  const authorizedDomains = derivePeerAuthorizedDomainsView(peer);
19180
+ const grantRoleMismatch = derivePeerGrantRoleMismatchView(peer);
18648
19181
  const scopeRejections = derivePeerScopeRejectionsView(peer);
18649
19182
  const scope = peer.project_scope || {};
18650
19183
  const projectNarrowing = derivePeerProjectNarrowingView(scope);
@@ -18900,6 +19433,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
18900
19433
  title: "Inbound ops rejected by the sharing-domain scope gate. Expand for reason codes.",
18901
19434
  children: scopeRejections.badgeLabel
18902
19435
  }) : null,
19436
+ grantRoleMismatch.badgeLabel ? /* @__PURE__ */ u("span", {
19437
+ className: "badge badge-offline",
19438
+ title: "Review whether explicit Sharing-domain grants match this device's intended role.",
19439
+ children: grantRoleMismatch.badgeLabel
19440
+ }) : null,
18903
19441
  peer.discovered_via_group_id ? /* @__PURE__ */ u("span", {
18904
19442
  className: "badge actor-badge",
18905
19443
  title: `Discovered through coordinator group ${peer.discovered_via_group_id}`,
@@ -19008,6 +19546,15 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
19008
19546
  children: domain.label
19009
19547
  }, domain.scopeId))
19010
19548
  }) : null,
19549
+ grantRoleMismatch.isVisible ? /* @__PURE__ */ u("div", {
19550
+ className: "settings-note",
19551
+ role: "status",
19552
+ children: [
19553
+ /* @__PURE__ */ u("strong", { children: grantRoleMismatch.title }),
19554
+ /* @__PURE__ */ u("div", { children: grantRoleMismatch.message }),
19555
+ /* @__PURE__ */ u("div", { children: grantRoleMismatch.detail })
19556
+ ]
19557
+ }) : null,
19011
19558
  /* @__PURE__ */ u("div", {
19012
19559
  className: "peer-meta",
19013
19560
  children: projectNarrowing.note
@@ -20497,6 +21044,29 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20497
21044
  }
20498
21045
  //#endregion
20499
21046
  //#region src/tabs/sync/components/sync-sharing-review.tsx
21047
+ function formatIdentitySource(source) {
21048
+ switch (source) {
21049
+ case "cwd": return "Matched by folder";
21050
+ case "git_remote": return "Matched by git remote";
21051
+ case "workspace_id": return "Matched by workspace";
21052
+ case "project": return "Matched by project name";
21053
+ case "unmapped": return "Missing project identity";
21054
+ default: return `Matched by ${source.replace(/_/g, " ")}`;
21055
+ }
21056
+ }
21057
+ function needsProjectCleanup(group) {
21058
+ const label = group.displayProject.toLowerCase();
21059
+ return label.includes("fatal:") || label.includes("not a git repository") || label === "unknown project" || group.identitySource === "unmapped";
21060
+ }
21061
+ function groupSearchText(group) {
21062
+ return [
21063
+ group.displayProject,
21064
+ group.identitySource,
21065
+ group.suggestedScopeId ?? "",
21066
+ group.suggestionReason ?? "",
21067
+ group.workspaceIdentity
21068
+ ].join(" ").toLowerCase();
21069
+ }
20500
21070
  function SharingReviewRow({ item, onReview }) {
20501
21071
  return /* @__PURE__ */ u("div", {
20502
21072
  className: "actor-row",
@@ -20530,31 +21100,305 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20530
21100
  })]
20531
21101
  });
20532
21102
  }
20533
- function LegacySharedReviewRow({ item }) {
21103
+ function LegacySharedReviewRow({ item, onReassign, onReview }) {
21104
+ const groups = item.groups ?? [];
21105
+ const targetScopes = item.targetScopes ?? [];
21106
+ const firstTargetScopeId = targetScopes[0]?.scopeId ?? null;
21107
+ const initialSelections = Object.fromEntries(groups.map((group) => [group.workspaceIdentity, group.suggestedScopeId ?? firstTargetScopeId ?? ""]));
21108
+ const [pending, setPending] = d$2({});
21109
+ const [selectedScopes, setSelectedScopes] = d$2(initialSelections);
21110
+ const [selectedGroups, setSelectedGroups] = d$2({});
21111
+ const [filter, setFilter] = d$2("all");
21112
+ const [query, setQuery] = d$2("");
21113
+ const [busyIdentity, setBusyIdentity] = d$2(null);
21114
+ const shownGroupCount = groups.length;
21115
+ const groupCount = item.totalGroupCount ?? shownGroupCount;
21116
+ const cleanupCount = groups.filter(needsProjectCleanup).length;
21117
+ const suggestedCount = groups.filter((group) => group.suggestedScopeId && !needsProjectCleanup(group)).length;
21118
+ const selectedCount = groups.filter((group) => selectedGroups[group.workspaceIdentity]).length;
21119
+ const visibleGroups = groups.filter((group) => {
21120
+ const cleanup = needsProjectCleanup(group);
21121
+ if (filter === "suggested" && (!group.suggestedScopeId || cleanup)) return false;
21122
+ if (filter === "cleanup" && !cleanup) return false;
21123
+ if (filter === "no-suggestion" && (group.suggestedScopeId || cleanup)) return false;
21124
+ if (filter === "selected" && !selectedGroups[group.workspaceIdentity]) return false;
21125
+ return !query.trim() || groupSearchText(group).includes(query.trim().toLowerCase());
21126
+ });
21127
+ async function applyGroup(group) {
21128
+ const selectedScopeId = selectedScopes[group.workspaceIdentity] || group.suggestedScopeId || "";
21129
+ if (!selectedScopeId || !onReassign || busyIdentity) return;
21130
+ setBusyIdentity(group.workspaceIdentity);
21131
+ try {
21132
+ const preview = await onReassign(group, selectedScopeId, Boolean(pending[group.workspaceIdentity]));
21133
+ if (preview) setPending((current) => ({
21134
+ ...current,
21135
+ [group.workspaceIdentity]: preview
21136
+ }));
21137
+ else setPending((current) => {
21138
+ const next = { ...current };
21139
+ delete next[group.workspaceIdentity];
21140
+ return next;
21141
+ });
21142
+ } finally {
21143
+ setBusyIdentity(null);
21144
+ }
21145
+ }
21146
+ function updateSelectedScope(group, scopeId) {
21147
+ setSelectedScopes((current) => ({
21148
+ ...current,
21149
+ [group.workspaceIdentity]: scopeId
21150
+ }));
21151
+ setPending((current) => {
21152
+ const next = { ...current };
21153
+ delete next[group.workspaceIdentity];
21154
+ return next;
21155
+ });
21156
+ }
21157
+ function toggleGroup(group, checked) {
21158
+ setSelectedGroups((current) => ({
21159
+ ...current,
21160
+ [group.workspaceIdentity]: checked
21161
+ }));
21162
+ }
21163
+ function setVisibleSelected(checked) {
21164
+ setSelectedGroups((current) => ({
21165
+ ...current,
21166
+ ...Object.fromEntries(visibleGroups.map((group) => [group.workspaceIdentity, checked]))
21167
+ }));
21168
+ }
21169
+ function selectSuggestedGroups() {
21170
+ setSelectedGroups((current) => ({
21171
+ ...current,
21172
+ ...Object.fromEntries(groups.filter((group) => group.suggestedScopeId && !needsProjectCleanup(group)).map((group) => [group.workspaceIdentity, true]))
21173
+ }));
21174
+ }
21175
+ async function applySelectedGroups() {
21176
+ if (!onReassign || busyIdentity || selectedCount === 0) return;
21177
+ const selected = groups.filter((group) => selectedGroups[group.workspaceIdentity]);
21178
+ const allPreviewed = selected.every((group) => pending[group.workspaceIdentity]);
21179
+ setBusyIdentity("bulk");
21180
+ try {
21181
+ for (const group of selected) {
21182
+ const selectedScopeId = selectedScopes[group.workspaceIdentity] || group.suggestedScopeId || "";
21183
+ if (!selectedScopeId) continue;
21184
+ const preview = await onReassign(group, selectedScopeId, allPreviewed);
21185
+ if (preview) setPending((current) => ({
21186
+ ...current,
21187
+ [group.workspaceIdentity]: preview
21188
+ }));
21189
+ else if (allPreviewed) {
21190
+ setPending((current) => {
21191
+ const next = { ...current };
21192
+ delete next[group.workspaceIdentity];
21193
+ return next;
21194
+ });
21195
+ setSelectedGroups((current) => ({
21196
+ ...current,
21197
+ [group.workspaceIdentity]: false
21198
+ }));
21199
+ }
21200
+ }
21201
+ } finally {
21202
+ setBusyIdentity(null);
21203
+ }
21204
+ }
20534
21205
  return /* @__PURE__ */ u("div", {
20535
- className: "actor-row",
20536
- children: /* @__PURE__ */ u("div", {
20537
- className: "actor-details",
20538
- children: [/* @__PURE__ */ u("div", {
20539
- className: "actor-title",
20540
- children: [/* @__PURE__ */ u("strong", { children: "Legacy shared review" }), /* @__PURE__ */ u("span", {
20541
- className: "badge badge-offline",
20542
- children: "Needs review"
21206
+ className: "legacy-review-card",
21207
+ children: [
21208
+ /* @__PURE__ */ u("div", {
21209
+ className: "legacy-review-header",
21210
+ children: [/* @__PURE__ */ u("div", { children: [
21211
+ /* @__PURE__ */ u("div", {
21212
+ className: "actor-title",
21213
+ children: [/* @__PURE__ */ u("strong", { children: "Legacy shared review" }), /* @__PURE__ */ u("span", {
21214
+ className: "badge badge-offline",
21215
+ children: "Needs project review"
21216
+ })]
21217
+ }),
21218
+ /* @__PURE__ */ u("div", {
21219
+ className: "peer-meta legacy-review-summary",
21220
+ children: [
21221
+ groupCount.toLocaleString(),
21222
+ " older",
21223
+ " ",
21224
+ groupCount === 1 ? "project needs" : "projects need",
21225
+ " a Sharing domain. They contain",
21226
+ " ",
21227
+ item.memoryCount.toLocaleString(),
21228
+ " older shared memories total; review the projects, not individual memories.",
21229
+ shownGroupCount < groupCount ? ` Showing the first ${shownGroupCount.toLocaleString()} projects by memory count.` : ""
21230
+ ]
21231
+ }),
21232
+ /* @__PURE__ */ u("div", {
21233
+ className: "peer-meta legacy-review-warning",
21234
+ children: "Nothing moves automatically. Reassignment changes future sync authorization; it does not erase copies peers already received."
21235
+ })
21236
+ ] }), /* @__PURE__ */ u("div", {
21237
+ className: "actor-actions",
21238
+ children: /* @__PURE__ */ u("button", {
21239
+ type: "button",
21240
+ className: "settings-button",
21241
+ onClick: onReview,
21242
+ children: "Manage all projects"
21243
+ })
20543
21244
  })]
20544
- }), /* @__PURE__ */ u("div", {
20545
- className: "peer-meta",
20546
- children: [
20547
- item.memoryCount.toLocaleString(),
20548
- " historical shared memories are in ",
20549
- item.scopeId,
20550
- ". 0.30 placed ambiguous older shared data there conservatively; review mappings before promoting it. Remapping or revocation does not erase data already copied to peers."
20551
- ]
20552
- })]
20553
- })
21245
+ }),
21246
+ /* @__PURE__ */ u("div", {
21247
+ className: "legacy-review-toolbar",
21248
+ children: [/* @__PURE__ */ u("div", {
21249
+ className: "legacy-review-filter-row",
21250
+ children: [
21251
+ ["all", `Showing ${shownGroupCount.toLocaleString()}`],
21252
+ ["suggested", `Suggested ${suggestedCount.toLocaleString()}`],
21253
+ ["cleanup", `Needs cleanup ${cleanupCount.toLocaleString()}`],
21254
+ ["no-suggestion", "No suggestion"],
21255
+ ["selected", `Selected ${selectedCount.toLocaleString()}`]
21256
+ ].map(([value, label]) => /* @__PURE__ */ u("button", {
21257
+ className: filter === value ? "settings-button legacy-filter active" : "settings-button legacy-filter",
21258
+ onClick: () => setFilter(value),
21259
+ type: "button",
21260
+ children: label
21261
+ }, value))
21262
+ }), /* @__PURE__ */ u("div", {
21263
+ className: "legacy-review-bulk-row",
21264
+ children: [
21265
+ /* @__PURE__ */ u("input", {
21266
+ "aria-label": "Search legacy project groups",
21267
+ className: "peer-scope-input legacy-review-search",
21268
+ onInput: (event) => setQuery(event.currentTarget.value),
21269
+ placeholder: "Search project, signal, or destination…",
21270
+ value: query
21271
+ }),
21272
+ /* @__PURE__ */ u("button", {
21273
+ className: "settings-button",
21274
+ onClick: () => setVisibleSelected(true),
21275
+ type: "button",
21276
+ children: "Select visible"
21277
+ }),
21278
+ /* @__PURE__ */ u("button", {
21279
+ className: "settings-button",
21280
+ onClick: selectSuggestedGroups,
21281
+ type: "button",
21282
+ children: "Select suggested"
21283
+ }),
21284
+ /* @__PURE__ */ u("button", {
21285
+ className: "settings-button",
21286
+ onClick: () => setVisibleSelected(false),
21287
+ type: "button",
21288
+ children: "Clear visible"
21289
+ }),
21290
+ onReassign && targetScopes.length > 0 ? /* @__PURE__ */ u("button", {
21291
+ className: "settings-save",
21292
+ disabled: busyIdentity != null || selectedCount === 0,
21293
+ onClick: () => void applySelectedGroups(),
21294
+ type: "button",
21295
+ children: busyIdentity === "bulk" ? "Working…" : selectedCount > 0 && groups.filter((group) => selectedGroups[group.workspaceIdentity]).every((group) => pending[group.workspaceIdentity]) ? `Apply ${selectedCount.toLocaleString()} selected` : `Preview ${selectedCount.toLocaleString()} selected`
21296
+ }) : onReassign && selectedCount > 0 ? /* @__PURE__ */ u("span", {
21297
+ className: "legacy-review-cleanup-note",
21298
+ children: "Add or join a Sharing domain before bulk reassignment."
21299
+ }) : null
21300
+ ]
21301
+ })]
21302
+ }),
21303
+ groups.length > 0 ? /* @__PURE__ */ u("ul", {
21304
+ className: "legacy-review-groups",
21305
+ "aria-label": "Legacy shared review project groups",
21306
+ children: visibleGroups.map((group) => /* @__PURE__ */ u("li", {
21307
+ className: "legacy-review-group",
21308
+ children: [
21309
+ /* @__PURE__ */ u("div", {
21310
+ className: "legacy-review-group-main",
21311
+ children: [/* @__PURE__ */ u("div", {
21312
+ className: "legacy-review-title-wrap",
21313
+ children: [/* @__PURE__ */ u("input", {
21314
+ "aria-label": `Select ${group.displayProject}`,
21315
+ checked: Boolean(selectedGroups[group.workspaceIdentity]),
21316
+ className: "cm-checkbox",
21317
+ onChange: (event) => toggleGroup(group, event.currentTarget.checked),
21318
+ type: "checkbox"
21319
+ }), /* @__PURE__ */ u("div", { children: [
21320
+ /* @__PURE__ */ u("div", {
21321
+ className: "legacy-review-group-title",
21322
+ children: group.displayProject
21323
+ }),
21324
+ /* @__PURE__ */ u("div", {
21325
+ className: "legacy-review-group-meta",
21326
+ children: [formatIdentitySource(group.identitySource), group.suggestedScopeId ? ` · suggested ${group.suggestedScopeId}` : ""]
21327
+ }),
21328
+ needsProjectCleanup(group) ? /* @__PURE__ */ u("div", {
21329
+ className: "legacy-review-cleanup-note",
21330
+ children: "Unclear project identity. Inspect or correct the project before trusting a domain."
21331
+ }) : null
21332
+ ] })]
21333
+ }), /* @__PURE__ */ u("span", {
21334
+ className: "legacy-review-count",
21335
+ children: [
21336
+ group.memoryCount.toLocaleString(),
21337
+ " memor",
21338
+ group.memoryCount === 1 ? "y" : "ies"
21339
+ ]
21340
+ })]
21341
+ }),
21342
+ group.suggestionReason ? /* @__PURE__ */ u("div", {
21343
+ className: "legacy-review-reason",
21344
+ children: group.suggestionReason
21345
+ }) : null,
21346
+ pending[group.workspaceIdentity] ? /* @__PURE__ */ u("div", {
21347
+ className: "legacy-review-confirmation",
21348
+ role: "alert",
21349
+ children: [
21350
+ pending[group.workspaceIdentity].warning,
21351
+ " This will reassign",
21352
+ " ",
21353
+ pending[group.workspaceIdentity].reassignable_memory_count.toLocaleString(),
21354
+ " of",
21355
+ " ",
21356
+ pending[group.workspaceIdentity].memory_count.toLocaleString(),
21357
+ " memories",
21358
+ pending[group.workspaceIdentity].skipped_memory_count ? `; ${pending[group.workspaceIdentity].skipped_memory_count.toLocaleString()} peer-owned copies will be left unchanged` : "",
21359
+ "."
21360
+ ]
21361
+ }) : null,
21362
+ targetScopes.length > 0 && onReassign ? /* @__PURE__ */ u("label", {
21363
+ className: "legacy-review-target",
21364
+ children: [/* @__PURE__ */ u("span", { children: "Destination Sharing domain" }), /* @__PURE__ */ u("select", {
21365
+ className: "peer-scope-input",
21366
+ disabled: busyIdentity != null,
21367
+ onChange: (event) => updateSelectedScope(group, event.currentTarget.value),
21368
+ value: selectedScopes[group.workspaceIdentity] || group.suggestedScopeId || "",
21369
+ children: targetScopes.map((scope) => /* @__PURE__ */ u("option", {
21370
+ value: scope.scopeId,
21371
+ children: [
21372
+ scope.label,
21373
+ " · ",
21374
+ scope.authorityType,
21375
+ scope.scopeId === group.suggestedScopeId ? " · suggested" : ""
21376
+ ]
21377
+ }, scope.scopeId))
21378
+ })]
21379
+ }) : null,
21380
+ targetScopes.length > 0 && onReassign ? /* @__PURE__ */ u("div", {
21381
+ className: "legacy-review-actions",
21382
+ children: /* @__PURE__ */ u("button", {
21383
+ type: "button",
21384
+ className: "settings-button",
21385
+ disabled: busyIdentity != null,
21386
+ onClick: () => void applyGroup(group),
21387
+ children: busyIdentity === group.workspaceIdentity ? "Reassigning…" : pending[group.workspaceIdentity] ? "I understand, reassign memories" : "Preview reassignment"
21388
+ })
21389
+ }) : null
21390
+ ]
21391
+ }, group.workspaceIdentity))
21392
+ }) : null
21393
+ ]
20554
21394
  });
20555
21395
  }
20556
- function SyncSharingReview({ items, legacyReview, onReview }) {
20557
- return /* @__PURE__ */ u(k$3, { children: [legacyReview ? /* @__PURE__ */ u(LegacySharedReviewRow, { item: legacyReview }) : null, items.map((item) => /* @__PURE__ */ u(SharingReviewRow, {
21396
+ function SyncSharingReview({ items, legacyReview, onLegacyReassign, onLegacyReview, onReview }) {
21397
+ return /* @__PURE__ */ u(k$3, { children: [legacyReview ? /* @__PURE__ */ u(LegacySharedReviewRow, {
21398
+ item: legacyReview,
21399
+ onReassign: onLegacyReassign,
21400
+ onReview: onLegacyReview ?? onReview
21401
+ }) : null, items.map((item) => /* @__PURE__ */ u(SharingReviewRow, {
20558
21402
  item,
20559
21403
  onReview
20560
21404
  }, `${item.peerName}:${item.actorId}:${item.scopeLabel}`))] });
@@ -20566,6 +21410,26 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20566
21410
  state.feedQuery = "";
20567
21411
  window.location.hash = "feed";
20568
21412
  }
21413
+ function openProjectsReview() {
21414
+ window.location.hash = "projects";
21415
+ }
21416
+ async function reassignLegacySharedReviewGroup(group, scopeId, confirmedOldCopies) {
21417
+ try {
21418
+ const result = await reassignLegacySharedReviewGroup$1({
21419
+ confirmed_old_copies: confirmedOldCopies,
21420
+ scope_id: scopeId,
21421
+ workspace_identity: group.workspaceIdentity
21422
+ });
21423
+ state.lastSyncLegacySharedReview = result.legacy_shared_review ?? null;
21424
+ showGlobalNotice(`Reassigned ${result.reassigned_memory_count.toLocaleString()} legacy review memor${result.reassigned_memory_count === 1 ? "y" : "ies"} to ${result.target_scope_label}.`);
21425
+ renderSyncSharingReview();
21426
+ return null;
21427
+ } catch (error) {
21428
+ if (error instanceof LegacySharedReviewConfirmationError) return error.preview;
21429
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to reassign legacy review memories.", "warning");
21430
+ return null;
21431
+ }
21432
+ }
20569
21433
  function renderSyncSharingReview() {
20570
21434
  const panel = document.getElementById("syncSharingReview");
20571
21435
  const meta = document.getElementById("syncSharingReviewMeta");
@@ -20576,8 +21440,29 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20576
21440
  const legacyRaw = state.lastSyncLegacySharedReview;
20577
21441
  const legacyCount = Math.max(0, Number(legacyRaw?.memory_count ?? 0));
20578
21442
  const legacyReview = legacyRaw?.has_data ? {
21443
+ groups: Array.isArray(legacyRaw.groups) ? legacyRaw.groups.map((group) => {
21444
+ const raw = group;
21445
+ return {
21446
+ displayProject: String(raw.display_project || raw.project || "Unknown project"),
21447
+ identitySource: String(raw.identity_source || "unknown"),
21448
+ lastUpdatedAt: typeof raw.last_updated_at === "string" ? raw.last_updated_at : null,
21449
+ memoryCount: Number(raw.memory_count || 0),
21450
+ suggestedScopeId: typeof raw.suggested_scope_id === "string" ? raw.suggested_scope_id : null,
21451
+ suggestionReason: typeof raw.suggestion_reason === "string" ? raw.suggestion_reason : null,
21452
+ workspaceIdentity: String(raw.workspace_identity || raw.display_project || "unknown")
21453
+ };
21454
+ }) : [],
20579
21455
  memoryCount: legacyCount,
20580
- scopeId: String(legacyRaw.scope_id || "legacy-shared-review")
21456
+ scopeId: String(legacyRaw.scope_id || "legacy-shared-review"),
21457
+ totalGroupCount: Number(legacyRaw.total_group_count || legacyRaw.groups?.length || 0),
21458
+ targetScopes: Array.isArray(legacyRaw.target_scopes) ? legacyRaw.target_scopes.map((scope) => {
21459
+ const raw = scope;
21460
+ return {
21461
+ authorityType: String(raw.authority_type || "unknown"),
21462
+ label: String(raw.label || raw.scope_id || "Unknown domain"),
21463
+ scopeId: String(raw.scope_id || "")
21464
+ };
21465
+ }).filter((scope) => scope.scopeId) : []
20581
21466
  } : null;
20582
21467
  if (!items.length && !legacyReview) {
20583
21468
  clearSyncMount(list);
@@ -20598,6 +21483,8 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20598
21483
  shareableCount: Number(item.shareable_count || 0)
20599
21484
  })),
20600
21485
  legacyReview,
21486
+ onLegacyReassign: reassignLegacySharedReviewGroup,
21487
+ onLegacyReview: openProjectsReview,
20601
21488
  onReview: openFeedSharingReview
20602
21489
  }));
20603
21490
  }
@@ -20783,13 +21670,121 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20783
21670
  try {
20784
21671
  const runtime = await loadRuntimeInfo();
20785
21672
  if (!runtime?.version) return;
20786
- setRuntimeLabel(runtime.version, "21eac98");
21673
+ setRuntimeLabel(runtime.version, "2ee815b");
20787
21674
  } catch {}
20788
21675
  }
20789
21676
  var lastAnnouncedRefreshState = null;
20790
21677
  var RECONNECT_POLL_MS = 1500;
21678
+ var LEGACY_UPGRADE_NOTICE_DISMISSED_KEY = "codemem-legacy-upgrade-notice-dismissed";
20791
21679
  var reconnectTimer = null;
20792
21680
  var reconnecting = false;
21681
+ var legacyUpgradeNoticeShown = false;
21682
+ var legacyUpgradeNoticePreviousFocus = null;
21683
+ function readNonNegativeCount(value, fallback = 0) {
21684
+ const count = Number(value);
21685
+ return Number.isFinite(count) ? Math.max(0, count) : fallback;
21686
+ }
21687
+ function readLegacyUpgradeReviewSummary(payload) {
21688
+ if (!payload || typeof payload !== "object") return null;
21689
+ const review = payload.legacy_shared_review ?? payload;
21690
+ if (!review || typeof review !== "object") return null;
21691
+ const raw = review;
21692
+ if (raw.has_data !== true) return null;
21693
+ const groups = Array.isArray(raw.groups) ? raw.groups : [];
21694
+ const groupCount = readNonNegativeCount(raw.total_group_count, groups.length);
21695
+ const memoryCount = readNonNegativeCount(raw.memory_count);
21696
+ if (groupCount <= 0 || memoryCount <= 0) return null;
21697
+ return {
21698
+ groupCount,
21699
+ memoryCount
21700
+ };
21701
+ }
21702
+ function isLegacyUpgradeNoticeDismissed() {
21703
+ try {
21704
+ return localStorage.getItem(LEGACY_UPGRADE_NOTICE_DISMISSED_KEY) === "1";
21705
+ } catch {
21706
+ return false;
21707
+ }
21708
+ }
21709
+ function dismissLegacyUpgradeNoticeIfRequested() {
21710
+ if (!document.getElementById("legacyUpgradeDontShow")?.checked) return;
21711
+ try {
21712
+ localStorage.setItem(LEGACY_UPGRADE_NOTICE_DISMISSED_KEY, "1");
21713
+ } catch {}
21714
+ }
21715
+ function legacyUpgradeFocusableElements() {
21716
+ const modal = $("legacyUpgradeModal");
21717
+ if (!modal || modal.hidden) return [];
21718
+ return Array.from(modal.querySelectorAll("a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])")).filter((element) => !element.hidden && element.offsetParent !== null);
21719
+ }
21720
+ function focusLegacyUpgradeModal() {
21721
+ const focusable = legacyUpgradeFocusableElements();
21722
+ const primary = $("legacyUpgradeReviewGroups");
21723
+ (primary && focusable.includes(primary) ? primary : focusable[0])?.focus();
21724
+ }
21725
+ function handleLegacyUpgradeModalKeydown(event) {
21726
+ const modal = $("legacyUpgradeModal");
21727
+ if (!modal || modal.hidden) return;
21728
+ if (event.key === "Escape") {
21729
+ event.preventDefault();
21730
+ dismissLegacyUpgradeNoticeIfRequested();
21731
+ setLegacyUpgradeNotice(false);
21732
+ return;
21733
+ }
21734
+ if (event.key !== "Tab") return;
21735
+ const focusable = legacyUpgradeFocusableElements();
21736
+ if (focusable.length === 0) return;
21737
+ const first = focusable[0];
21738
+ const last = focusable[focusable.length - 1];
21739
+ if (event.shiftKey && document.activeElement === first) {
21740
+ event.preventDefault();
21741
+ last.focus();
21742
+ return;
21743
+ }
21744
+ if (!event.shiftKey && document.activeElement === last) {
21745
+ event.preventDefault();
21746
+ first.focus();
21747
+ }
21748
+ }
21749
+ function setLegacyUpgradeBackgroundInert(inert) {
21750
+ for (const element of document.querySelectorAll("header, .tab-bar, .tab-panel, #toastRoot, #viewerReconnectOverlay, #settingsDialogMount")) {
21751
+ if (element.id === "viewerReconnectOverlay" && element.hidden) continue;
21752
+ element.inert = inert;
21753
+ if (inert) element.setAttribute("aria-hidden", "true");
21754
+ else element.removeAttribute("aria-hidden");
21755
+ }
21756
+ }
21757
+ function setLegacyUpgradeNotice(open, summary) {
21758
+ const overlay = $("legacyUpgradeModalBackdrop");
21759
+ const modal = $("legacyUpgradeModal");
21760
+ const summaryEl = $("legacyUpgradeSummary");
21761
+ if (!overlay || !modal || !summaryEl) return;
21762
+ if (summary) summaryEl.textContent = `${summary.groupCount.toLocaleString()} older ${summary.groupCount === 1 ? "project needs" : "projects need"} a Sharing domain. They contain ${summary.memoryCount.toLocaleString()} older shared memories total; you will review the projects, not individual memories.`;
21763
+ overlay.hidden = !open;
21764
+ modal.hidden = !open;
21765
+ if (open) {
21766
+ legacyUpgradeNoticePreviousFocus = document.activeElement;
21767
+ document.addEventListener("keydown", handleLegacyUpgradeModalKeydown);
21768
+ focusLegacyUpgradeModal();
21769
+ setLegacyUpgradeBackgroundInert(true);
21770
+ return;
21771
+ }
21772
+ setLegacyUpgradeBackgroundInert(false);
21773
+ document.removeEventListener("keydown", handleLegacyUpgradeModalKeydown);
21774
+ if (legacyUpgradeNoticePreviousFocus && document.contains(legacyUpgradeNoticePreviousFocus)) legacyUpgradeNoticePreviousFocus.focus();
21775
+ legacyUpgradeNoticePreviousFocus = null;
21776
+ }
21777
+ function maybeShowLegacyUpgradeNotice(summary) {
21778
+ if (!summary || legacyUpgradeNoticeShown || isLegacyUpgradeNoticeDismissed()) return;
21779
+ legacyUpgradeNoticeShown = true;
21780
+ setLegacyUpgradeNotice(true, summary);
21781
+ }
21782
+ async function checkLegacyUpgradeNotice() {
21783
+ if (legacyUpgradeNoticeShown || isLegacyUpgradeNoticeDismissed()) return;
21784
+ try {
21785
+ maybeShowLegacyUpgradeNotice(readLegacyUpgradeReviewSummary(await loadSyncStatus(false, "", { includeJoinRequests: false })));
21786
+ } catch {}
21787
+ }
20793
21788
  function setReconnectOverlay(open, detail) {
20794
21789
  const overlay = $("viewerReconnectOverlay");
20795
21790
  const detailEl = $("viewerReconnectDetail");
@@ -20980,10 +21975,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20980
21975
  state.lastCoordinatorAdminStatus = null;
20981
21976
  }));
20982
21977
  if (refreshTab === "feed") promises.push(loadFeedData());
21978
+ if (refreshTab === "projects") promises.push(loadProjectsData());
20983
21979
  if (refreshTab === "sync" || refreshTab === "health") promises.push(loadSyncData());
20984
21980
  if (refreshTab === "coordinator-admin") promises.push(loadCoordinatorAdminData());
20985
21981
  if (state.syncPairingOpen) promises.push(loadPairingData());
20986
21982
  await Promise.all(promises);
21983
+ maybeShowLegacyUpgradeNotice(readLegacyUpgradeReviewSummary(state.lastSyncLegacySharedReview));
20987
21984
  const nextTab = resolveAccessibleTab(state.activeTab, state.lastCoordinatorAdminStatus);
20988
21985
  if (nextTab !== state.activeTab) setActiveTab(nextTab);
20989
21986
  renderTabs(state.activeTab);
@@ -21006,6 +22003,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
21006
22003
  setTheme(getTheme());
21007
22004
  initTabs();
21008
22005
  initFeedTab();
22006
+ initProjectsTab(() => refresh());
21009
22007
  initSyncTab(() => refresh());
21010
22008
  initCoordinatorAdminTab();
21011
22009
  initSettings(stopPolling, startPolling, () => refresh());
@@ -21028,7 +22026,23 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
21028
22026
  } catch {}
21029
22027
  if (!reconnecting) setReconnectOverlay(false);
21030
22028
  });
22029
+ $("legacyUpgradeReviewGroups")?.addEventListener("click", () => {
22030
+ dismissLegacyUpgradeNoticeIfRequested();
22031
+ setLegacyUpgradeNotice(false);
22032
+ window.location.hash = "sync";
22033
+ setTimeout(() => document.getElementById("syncSharingReview")?.scrollIntoView({ block: "start" }), 120);
22034
+ });
22035
+ $("legacyUpgradeReviewProjects")?.addEventListener("click", () => {
22036
+ dismissLegacyUpgradeNoticeIfRequested();
22037
+ setLegacyUpgradeNotice(false);
22038
+ window.location.hash = "projects";
22039
+ });
22040
+ $("legacyUpgradeNotNow")?.addEventListener("click", () => {
22041
+ dismissLegacyUpgradeNoticeIfRequested();
22042
+ setLegacyUpgradeNotice(false);
22043
+ });
21031
22044
  loadRuntimeLabel();
22045
+ checkLegacyUpgradeNotice();
21032
22046
  refresh();
21033
22047
  startPolling();
21034
22048
  //#endregion