@codemem/server 0.30.0 → 0.31.1

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
@@ -3268,6 +3268,14 @@
3268
3268
  if (!resp.ok) throw new Error(detail || payloadError(payload) || text || "request failed");
3269
3269
  return payload;
3270
3270
  }
3271
+ var ProjectForgetConfirmationError = class extends Error {
3272
+ preview;
3273
+ constructor(preview) {
3274
+ super("Project forget confirmation required");
3275
+ this.name = "ProjectForgetConfirmationError";
3276
+ this.preview = preview;
3277
+ }
3278
+ };
3271
3279
  var SharingDomainGuardrailConfirmationError = class extends Error {
3272
3280
  requiredGuardrails;
3273
3281
  requiredGuardrailTokens;
@@ -3280,9 +3288,26 @@
3280
3288
  this.guardrailWarnings = input.guardrail_warnings ?? [];
3281
3289
  }
3282
3290
  };
3291
+ var LegacySharedReviewConfirmationError = class extends Error {
3292
+ preview;
3293
+ constructor(preview) {
3294
+ super("Legacy shared review confirmation required");
3295
+ this.name = "LegacySharedReviewConfirmationError";
3296
+ this.preview = preview;
3297
+ }
3298
+ };
3283
3299
  async function loadSharingDomainSettings() {
3284
3300
  return fetchJson("/api/sync/sharing-domains/settings");
3285
3301
  }
3302
+ async function loadProjectScopeInventory(input = {}) {
3303
+ const params = new URLSearchParams();
3304
+ for (const [key, value] of Object.entries(input)) {
3305
+ if (value == null || value === "") continue;
3306
+ params.set(key, String(value));
3307
+ }
3308
+ const query = params.toString();
3309
+ return fetchJson(`/api/sync/projects${query ? `?${query}` : ""}`);
3310
+ }
3286
3311
  async function saveSharingDomainProjectMapping(input) {
3287
3312
  const resp = await fetch("/api/sync/sharing-domains/project-mappings", {
3288
3313
  method: "PUT",
@@ -3301,12 +3326,65 @@
3301
3326
  guardrail_warnings: payload.guardrail_warnings ?? mapping.guardrail_warnings
3302
3327
  };
3303
3328
  }
3329
+ async function saveSharingDomainProjectMappings(input) {
3330
+ const resp = await fetch("/api/sync/sharing-domains/project-mappings/bulk", {
3331
+ method: "PUT",
3332
+ headers: { "Content-Type": "application/json" },
3333
+ body: JSON.stringify(input)
3334
+ });
3335
+ const { text, payload } = await readJsonPayload(resp);
3336
+ if (!resp.ok) {
3337
+ if (payload?.error === "guardrail_confirmation_required") throw new SharingDomainGuardrailConfirmationError(payload);
3338
+ throw new Error(payloadError(payload) || text || "request failed");
3339
+ }
3340
+ if (!Array.isArray(payload?.mappings)) throw new Error("response missing mappings");
3341
+ return payload.mappings;
3342
+ }
3304
3343
  async function deleteSharingDomainProjectMapping(id) {
3305
3344
  const resp = await fetch(`/api/sync/sharing-domains/project-mappings/${encodeURIComponent(id)}`, { method: "DELETE" });
3306
3345
  const { text, payload } = await readJsonPayload(resp);
3307
3346
  if (!resp.ok) throw new Error(payloadError(payload) || text || "request failed");
3308
3347
  return Boolean(payload?.deleted);
3309
3348
  }
3349
+ async function reassignProjectInventoryProject(input) {
3350
+ const resp = await fetch("/api/sync/projects/reassign-project", {
3351
+ method: "POST",
3352
+ headers: { "Content-Type": "application/json" },
3353
+ body: JSON.stringify(input)
3354
+ });
3355
+ const { text, payload } = await readJsonPayload(resp);
3356
+ if (!resp.ok) throw new Error(payloadError(payload) || text || "request failed");
3357
+ if (!payload?.workspace_identity) throw new Error("response missing project reassignment");
3358
+ return payload;
3359
+ }
3360
+ async function forgetProjectInventoryMemories(input) {
3361
+ const resp = await fetch("/api/sync/projects/forget", {
3362
+ method: "POST",
3363
+ headers: { "Content-Type": "application/json" },
3364
+ body: JSON.stringify(input)
3365
+ });
3366
+ const { text, payload } = await readJsonPayload(resp);
3367
+ if (!resp.ok) {
3368
+ if (payload?.error === "project_forget_confirmation_required" && payload.preview) throw new ProjectForgetConfirmationError(payload.preview);
3369
+ throw new Error(payloadError(payload) || text || "request failed");
3370
+ }
3371
+ if (!payload?.workspace_identity) throw new Error("response missing project forget result");
3372
+ return payload;
3373
+ }
3374
+ async function reassignLegacySharedReviewGroup$1(input) {
3375
+ const resp = await fetch("/api/sync/legacy-shared-review/reassign", {
3376
+ method: "POST",
3377
+ headers: { "Content-Type": "application/json" },
3378
+ body: JSON.stringify(input)
3379
+ });
3380
+ const { text, payload } = await readJsonPayload(resp);
3381
+ if (!resp.ok) {
3382
+ if (payload?.error === "legacy_review_confirmation_required" && payload.preview) throw new LegacySharedReviewConfirmationError(payload.preview);
3383
+ throw new Error(payloadError(payload) || text || "request failed");
3384
+ }
3385
+ if (!payload?.workspace_identity) throw new Error("response missing legacy review reassignment");
3386
+ return payload;
3387
+ }
3310
3388
  async function loadCoordinatorGroupPreferences(groupId) {
3311
3389
  const resp = await fetch(`/api/coordinator/admin/groups/${encodeURIComponent(groupId)}/preferences`);
3312
3390
  const { text, payload } = await readJsonPayload(resp);
@@ -3397,7 +3475,7 @@
3397
3475
  }
3398
3476
  //#endregion
3399
3477
  //#region src/lib/dom.ts
3400
- function el(tag, className, text) {
3478
+ function el$1(tag, className, text) {
3401
3479
  const node = document.createElement(tag);
3402
3480
  if (className) node.className = className;
3403
3481
  if (text !== void 0 && text !== null) node.textContent = String(text);
@@ -3445,8 +3523,9 @@
3445
3523
  //#region src/lib/state.ts
3446
3524
  var ALL_TAB_IDS = [
3447
3525
  "feed",
3448
- "health",
3526
+ "projects",
3449
3527
  "sync",
3528
+ "health",
3450
3529
  "coordinator-admin"
3451
3530
  ];
3452
3531
  var TAB_KEY = "codemem-tab";
@@ -13465,7 +13544,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
13465
13544
  }) : null, _$3(Tooltip, {
13466
13545
  label: formatDate(createdAtRaw),
13467
13546
  side: "left"
13468
- }, _$3("div", { className: "small feed-age" }, relative)), Boolean(item.owned_by_self) && memoryId > 0 ? _$3(FeedItemMenu, {
13547
+ }, _$3("div", { className: "small feed-age" }, relative)), item.owned_by_self && memoryId > 0 ? _$3(FeedItemMenu, {
13469
13548
  assignProjectDisabled: movingProject,
13470
13549
  disabled: deletingMemory,
13471
13550
  onAssignProject: () => void moveProject(),
@@ -13480,7 +13559,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
13480
13559
  })), _$3("div", { className: "feed-meta" }, metaText || "No tags, files, or provenance details attached."), bodyContent, _$3("div", { className: "feed-footer" }, _$3("div", { className: "feed-footer-left" }, tags.length ? _$3("div", { className: "feed-tags" }, tags.map((tag, index) => _$3(TagChip, {
13481
13560
  key: `${String(tag)}-${index}`,
13482
13561
  tag
13483
- }))) : null, Boolean(item.owned_by_self) && memoryId > 0 ? _$3("div", { className: "feed-visibility-controls" }, _$3("select", {
13562
+ }))) : null, item.owned_by_self && memoryId > 0 ? _$3("div", { className: "feed-visibility-controls" }, _$3("select", {
13484
13563
  "aria-label": `Visibility for ${String(item.title || "memory")}`,
13485
13564
  className: "feed-visibility-select",
13486
13565
  disabled: savingVisibility,
@@ -14344,6 +14423,552 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
14344
14423
  if (state.activeTab === "feed" && previousActorId !== nextActorId) updateFeedView(true);
14345
14424
  }
14346
14425
  //#endregion
14426
+ //#region src/tabs/projects.ts
14427
+ var STATUS_OPTIONS = [
14428
+ ["", "All projects"],
14429
+ ["needs_attention", "Needs review"],
14430
+ ["suggested", "Has suggestion"],
14431
+ ["local_only", "Stays on this device"],
14432
+ ["explicitly_mapped", "Already assigned"],
14433
+ ["legacy_review", "Older shared data"],
14434
+ ["unmapped", "Missing project identity"]
14435
+ ];
14436
+ var refreshProjects = null;
14437
+ var currentOffset = 0;
14438
+ var lastLimit = 250;
14439
+ var scopes = [];
14440
+ var openProjectDetails = /* @__PURE__ */ new Set();
14441
+ var openProjectClusters = /* @__PURE__ */ new Set();
14442
+ var draftDomainSelections = /* @__PURE__ */ new Map();
14443
+ var pendingConfirmations = /* @__PURE__ */ new Map();
14444
+ var pendingForgetConfirmations = /* @__PURE__ */ new Map();
14445
+ function el(id) {
14446
+ return document.getElementById(id);
14447
+ }
14448
+ function formatStatus(status) {
14449
+ return STATUS_OPTIONS.find(([value]) => value === status)?.[1] ?? status.replaceAll("_", " ");
14450
+ }
14451
+ function formatResolution(reason) {
14452
+ switch (reason) {
14453
+ case "exact_mapping": return "assigned to a Sharing domain";
14454
+ case "pattern_mapping": return "assigned by matching rule";
14455
+ case "explicit_override": return "manually assigned";
14456
+ default: return "stays on this device";
14457
+ }
14458
+ }
14459
+ function formatLatest(value) {
14460
+ if (!value) return "No recent sessions";
14461
+ const date = new Date(value);
14462
+ if (Number.isNaN(date.getTime())) return value;
14463
+ return date.toLocaleString();
14464
+ }
14465
+ function strongestSignal(project) {
14466
+ if (project.git_remote) return project.git_remote;
14467
+ if (project.cwd) return project.cwd;
14468
+ return project.workspace_identity;
14469
+ }
14470
+ function projectClusterKey(project) {
14471
+ if (project.git_remote) return `git:${project.git_remote}`;
14472
+ if (project.project) return `project:${project.project}`;
14473
+ return `identity:${project.workspace_identity}`;
14474
+ }
14475
+ function projectClusterLabel(project) {
14476
+ return project.project || project.display_project || strongestSignal(project);
14477
+ }
14478
+ function scopeLabel(scopeId) {
14479
+ if (!scopeId) return "—";
14480
+ const scope = scopes.find((item) => item.scope_id === scopeId);
14481
+ return scope?.label ? `${scope.label} (${scope.scope_id})` : scopeId;
14482
+ }
14483
+ function assignableScopes() {
14484
+ return scopes.filter((scope) => scope.scope_id !== "legacy-shared-review");
14485
+ }
14486
+ function guardrailHeading(warning) {
14487
+ switch (warning.code) {
14488
+ case "unknown_project_local_only": return "Current behavior";
14489
+ case "basename_collision_review": return "Name collision";
14490
+ case "scope_reassignment_old_copies": return "Previous copies";
14491
+ case "broad_org_domain_pattern":
14492
+ case "home_directory_org_domain_pattern": return "Broad mapping";
14493
+ default: return "Review item";
14494
+ }
14495
+ }
14496
+ async function saveProjectMapping(project, scopeId, confirmedGuardrailTokens = []) {
14497
+ try {
14498
+ await saveSharingDomainProjectMapping({
14499
+ ...project.mapping_id && project.resolution_reason === "exact_mapping" ? { id: project.mapping_id } : {},
14500
+ ...confirmedGuardrailTokens.length > 0 ? { confirmed_guardrail_tokens: confirmedGuardrailTokens } : {},
14501
+ project_pattern: project.display_project,
14502
+ scope_id: scopeId,
14503
+ workspace_identity: project.workspace_identity
14504
+ });
14505
+ pendingConfirmations.delete(project.workspace_identity);
14506
+ draftDomainSelections.delete(project.workspace_identity);
14507
+ showGlobalNotice("Project Sharing domain updated. Device access grants are unchanged.");
14508
+ refreshProjects?.();
14509
+ } catch (error) {
14510
+ if (error instanceof SharingDomainGuardrailConfirmationError) {
14511
+ pendingConfirmations.set(project.workspace_identity, {
14512
+ requiredGuardrailTokens: error.requiredGuardrailTokens,
14513
+ scopeId,
14514
+ warnings: error.guardrailWarnings
14515
+ });
14516
+ refreshProjects?.();
14517
+ return;
14518
+ }
14519
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to update project Sharing domain.", "warning");
14520
+ }
14521
+ }
14522
+ async function saveProjectClusterMapping(projects, scopeId) {
14523
+ const assignable = projects.filter((project) => project.identity_source !== "unmapped");
14524
+ if (assignable.length === 0) return;
14525
+ try {
14526
+ await saveSharingDomainProjectMappings({ mappings: assignable.map((project) => ({
14527
+ ...project.mapping_id && project.resolution_reason === "exact_mapping" ? { id: project.mapping_id } : {},
14528
+ project_pattern: project.display_project,
14529
+ scope_id: scopeId,
14530
+ workspace_identity: project.workspace_identity
14531
+ })) });
14532
+ showGlobalNotice(`Updated ${assignable.length} project identit${assignable.length === 1 ? "y" : "ies"}. Device access grants are unchanged.`);
14533
+ refreshProjects?.();
14534
+ } catch (error) {
14535
+ showGlobalNotice(error instanceof SharingDomainGuardrailConfirmationError ? "One or more identities in this group need review before bulk assignment. Expand the group and save those identities directly." : error instanceof Error ? error.message : "Unable to update project Sharing domains.", "warning");
14536
+ }
14537
+ }
14538
+ async function removeProjectMapping(project) {
14539
+ if (project.mapping_id == null) return;
14540
+ try {
14541
+ await deleteSharingDomainProjectMapping(project.mapping_id);
14542
+ pendingConfirmations.delete(project.workspace_identity);
14543
+ draftDomainSelections.delete(project.workspace_identity);
14544
+ showGlobalNotice("Project Sharing domain mapping removed. The next fallback now applies.");
14545
+ refreshProjects?.();
14546
+ } catch (error) {
14547
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to remove project Sharing domain mapping.", "warning");
14548
+ }
14549
+ }
14550
+ async function forgetProjectMemories(project, confirmed = false) {
14551
+ try {
14552
+ const pending = pendingForgetConfirmations.get(project.workspace_identity);
14553
+ const result = await forgetProjectInventoryMemories({
14554
+ ...confirmed && pending ? { confirmation_token: pending.confirmationToken } : {},
14555
+ confirmed,
14556
+ workspace_identity: project.workspace_identity
14557
+ });
14558
+ pendingForgetConfirmations.delete(project.workspace_identity);
14559
+ showGlobalNotice(`Forgot ${result.forgotten_memory_count.toLocaleString()} local memor${result.forgotten_memory_count === 1 ? "y" : "ies"}. ${result.peer_owned_memory_count.toLocaleString()} peer-owned memor${result.peer_owned_memory_count === 1 ? "y was" : "ies were"} left unchanged.`);
14560
+ refreshProjects?.();
14561
+ } catch (error) {
14562
+ if (error instanceof ProjectForgetConfirmationError) {
14563
+ pendingForgetConfirmations.set(project.workspace_identity, {
14564
+ confirmationToken: error.preview.confirmation_token,
14565
+ localOwnedMemoryCount: error.preview.local_owned_memory_count,
14566
+ peerOwnedMemoryCount: error.preview.peer_owned_memory_count
14567
+ });
14568
+ refreshProjects?.();
14569
+ return;
14570
+ }
14571
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to forget project memories.", "warning");
14572
+ }
14573
+ }
14574
+ async function reassignProject(project) {
14575
+ if (project.identity_source === "unmapped") return;
14576
+ const currentProject = String(project.project || project.display_project || "").trim();
14577
+ let suggestions = [];
14578
+ try {
14579
+ suggestions = (await loadProjects$1()).filter((name) => name && name !== currentProject);
14580
+ } catch {}
14581
+ const nextProject = await openSyncInputDialog({
14582
+ cancelLabel: "Cancel",
14583
+ confirmLabel: "Change project",
14584
+ 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.`,
14585
+ initialValue: currentProject,
14586
+ placeholder: "Project name",
14587
+ suggestions,
14588
+ title: "Change project",
14589
+ validate: (value) => {
14590
+ const trimmed = value.trim();
14591
+ if (!trimmed) return "Enter a project name.";
14592
+ if (trimmed === currentProject) return "Already assigned to this project.";
14593
+ return null;
14594
+ }
14595
+ });
14596
+ if (nextProject == null) return;
14597
+ try {
14598
+ const result = await reassignProjectInventoryProject({
14599
+ project: nextProject.trim(),
14600
+ workspace_identity: project.workspace_identity
14601
+ });
14602
+ showGlobalNotice(`Changed project to ${result.project} for ${result.moved_session_count} session${result.moved_session_count === 1 ? "" : "s"}.`);
14603
+ refreshProjects?.();
14604
+ } catch (error) {
14605
+ showGlobalNotice(error instanceof Error ? error.message : "Unable to change project.", "warning");
14606
+ }
14607
+ }
14608
+ function renderProjectActions(project) {
14609
+ const actions = document.createElement("div");
14610
+ actions.className = "project-inventory-actions";
14611
+ if (project.identity_source === "unmapped") {
14612
+ const note = document.createElement("div");
14613
+ note.className = "settings-note";
14614
+ note.textContent = "This project is missing a stable path, git remote, or workspace id. It stays Local only until it has a stable identity.";
14615
+ actions.appendChild(note);
14616
+ return actions;
14617
+ }
14618
+ const label = document.createElement("label");
14619
+ label.className = "sr-only";
14620
+ const selectId = `project-domain-${project.workspace_identity.replace(/[^a-z0-9_-]/gi, "-")}`;
14621
+ label.htmlFor = selectId;
14622
+ label.textContent = `Sharing domain for ${project.display_project}`;
14623
+ const select = document.createElement("select");
14624
+ select.id = selectId;
14625
+ select.className = "project-domain-select";
14626
+ const currentAssignable = assignableScopes().some((scope) => scope.scope_id === project.resolved_scope_id);
14627
+ if (!currentAssignable && project.resolved_scope_id) {
14628
+ const current = document.createElement("option");
14629
+ current.value = project.resolved_scope_id;
14630
+ current.textContent = `${scopeLabel(project.resolved_scope_id)} — not assignable`;
14631
+ current.disabled = true;
14632
+ select.appendChild(current);
14633
+ }
14634
+ for (const scope of assignableScopes()) {
14635
+ const option = document.createElement("option");
14636
+ option.value = scope.scope_id;
14637
+ option.textContent = scope.label ? `${scope.label} · ${scope.scope_id}` : scope.scope_id;
14638
+ select.appendChild(option);
14639
+ }
14640
+ select.value = draftDomainSelections.get(project.workspace_identity) ?? project.suggested_scope_id ?? project.resolved_scope_id;
14641
+ const save = document.createElement("button");
14642
+ save.className = "settings-button";
14643
+ save.type = "button";
14644
+ save.textContent = project.suggested_scope_id && select.value === project.suggested_scope_id ? "Confirm suggestion" : "Save domain";
14645
+ save.disabled = select.value === project.resolved_scope_id && !currentAssignable;
14646
+ save.addEventListener("click", () => void saveProjectMapping(project, select.value));
14647
+ select.addEventListener("change", () => {
14648
+ draftDomainSelections.set(project.workspace_identity, select.value);
14649
+ pendingConfirmations.delete(project.workspace_identity);
14650
+ save.textContent = "Save domain";
14651
+ save.disabled = false;
14652
+ refreshProjects?.();
14653
+ });
14654
+ const keepLocal = document.createElement("button");
14655
+ keepLocal.className = "settings-button";
14656
+ keepLocal.type = "button";
14657
+ keepLocal.textContent = "Keep local-only";
14658
+ keepLocal.addEventListener("click", () => void saveProjectMapping(project, "local-default"));
14659
+ const remove = document.createElement("button");
14660
+ remove.className = "settings-button";
14661
+ remove.type = "button";
14662
+ remove.textContent = "Remove mapping";
14663
+ remove.disabled = project.mapping_id == null || project.resolution_reason !== "exact_mapping";
14664
+ remove.addEventListener("click", () => void removeProjectMapping(project));
14665
+ const changeProject = document.createElement("button");
14666
+ changeProject.className = "settings-button";
14667
+ changeProject.type = "button";
14668
+ changeProject.textContent = "Change project…";
14669
+ changeProject.disabled = project.session_count === 0;
14670
+ if (changeProject.disabled) changeProject.title = "No sessions are available to reassign for this saved mapping.";
14671
+ changeProject.addEventListener("click", () => void reassignProject(project));
14672
+ const forget = document.createElement("button");
14673
+ forget.className = "settings-button danger";
14674
+ forget.type = "button";
14675
+ forget.textContent = "Forget local memories…";
14676
+ forget.disabled = (project.memory_count ?? 0) === 0;
14677
+ forget.addEventListener("click", () => void forgetProjectMemories(project));
14678
+ actions.append(label, select, save, keepLocal, remove, changeProject, forget);
14679
+ const pending = pendingConfirmations.get(project.workspace_identity);
14680
+ if (pending) {
14681
+ const warningBox = document.createElement("div");
14682
+ warningBox.className = "settings-note project-guardrail-confirmation";
14683
+ warningBox.setAttribute("role", "alert");
14684
+ const title = document.createElement("strong");
14685
+ title.textContent = "Confirmation required before saving this Sharing domain.";
14686
+ const intro = document.createElement("p");
14687
+ intro.textContent = "Codemem can save this change after you acknowledge the checks below. Verify the workspace details, then confirm to complete the save.";
14688
+ const list = document.createElement("ul");
14689
+ for (const warning of pending.warnings) {
14690
+ const item = document.createElement("li");
14691
+ const itemTitle = document.createElement("strong");
14692
+ itemTitle.textContent = `${guardrailHeading(warning)}: `;
14693
+ const message = document.createElement("span");
14694
+ message.textContent = warning.message;
14695
+ item.append(itemTitle, message);
14696
+ list.appendChild(item);
14697
+ }
14698
+ const confirm = document.createElement("button");
14699
+ confirm.className = "settings-button";
14700
+ confirm.type = "button";
14701
+ confirm.textContent = "I understand, save domain";
14702
+ confirm.addEventListener("click", () => {
14703
+ const currentPending = pendingConfirmations.get(project.workspace_identity);
14704
+ if (!currentPending || currentPending.scopeId !== select.value) return;
14705
+ saveProjectMapping(project, currentPending.scopeId, currentPending.requiredGuardrailTokens);
14706
+ });
14707
+ const cancel = document.createElement("button");
14708
+ cancel.className = "settings-button";
14709
+ cancel.type = "button";
14710
+ cancel.textContent = "Cancel";
14711
+ cancel.addEventListener("click", () => {
14712
+ pendingConfirmations.delete(project.workspace_identity);
14713
+ refreshProjects?.();
14714
+ });
14715
+ warningBox.append(title, intro, list, confirm, cancel);
14716
+ actions.appendChild(warningBox);
14717
+ }
14718
+ const pendingForget = pendingForgetConfirmations.get(project.workspace_identity);
14719
+ if (pendingForget) {
14720
+ const warningBox = document.createElement("div");
14721
+ warningBox.className = "settings-note project-guardrail-confirmation";
14722
+ warningBox.setAttribute("role", "alert");
14723
+ const title = document.createElement("strong");
14724
+ title.textContent = "Confirm project memory cleanup.";
14725
+ const intro = document.createElement("p");
14726
+ intro.textContent = `${pendingForget.localOwnedMemoryCount.toLocaleString()} locally owned memor${pendingForget.localOwnedMemoryCount === 1 ? "y" : "ies"} will be forgotten. ${pendingForget.peerOwnedMemoryCount.toLocaleString()} peer-owned memor${pendingForget.peerOwnedMemoryCount === 1 ? "y" : "ies"} will be left unchanged.`;
14727
+ const detail = document.createElement("p");
14728
+ detail.textContent = "Use this only to clean up wrongly attributed local project inventory; it forgets actual local memories on this device.";
14729
+ const confirm = document.createElement("button");
14730
+ confirm.className = "settings-button danger";
14731
+ confirm.type = "button";
14732
+ confirm.textContent = "I understand, forget local memories";
14733
+ confirm.addEventListener("click", () => void forgetProjectMemories(project, true));
14734
+ const cancel = document.createElement("button");
14735
+ cancel.className = "settings-button";
14736
+ cancel.type = "button";
14737
+ cancel.textContent = "Cancel";
14738
+ cancel.addEventListener("click", () => {
14739
+ pendingForgetConfirmations.delete(project.workspace_identity);
14740
+ refreshProjects?.();
14741
+ });
14742
+ warningBox.append(title, intro, detail, confirm, cancel);
14743
+ actions.appendChild(warningBox);
14744
+ }
14745
+ return actions;
14746
+ }
14747
+ function renderProjectRow(project) {
14748
+ const row = document.createElement("article");
14749
+ row.className = "project-inventory-row";
14750
+ const header = document.createElement("div");
14751
+ header.className = "project-inventory-row-header";
14752
+ const title = document.createElement("div");
14753
+ title.className = "project-inventory-title";
14754
+ title.textContent = project.display_project;
14755
+ header.appendChild(title);
14756
+ const domain = document.createElement("div");
14757
+ domain.className = "project-inventory-domain";
14758
+ domain.textContent = scopeLabel(project.resolved_scope_id);
14759
+ header.appendChild(domain);
14760
+ row.appendChild(header);
14761
+ const meta = document.createElement("div");
14762
+ meta.className = "project-inventory-meta";
14763
+ meta.textContent = `${formatResolution(project.resolution_reason)} · ${project.identity_source} · ${formatLatest(project.latest_session_at)}`;
14764
+ row.appendChild(meta);
14765
+ const signal = document.createElement("div");
14766
+ signal.className = "project-inventory-signal mono";
14767
+ signal.textContent = strongestSignal(project);
14768
+ row.appendChild(signal);
14769
+ if (project.suggested_scope_id && project.suggested_scope_id !== project.resolved_scope_id) {
14770
+ const suggestion = document.createElement("div");
14771
+ suggestion.className = "settings-note project-suggestion-note";
14772
+ suggestion.textContent = project.suggestion_reason ? `Suggestion: ${project.suggestion_reason}` : `Suggestion: map this project to ${scopeLabel(project.suggested_scope_id)}.`;
14773
+ row.appendChild(suggestion);
14774
+ }
14775
+ const warnings = project.guardrail_warnings.filter((warning) => warning.severity === "warning");
14776
+ if (warnings.length > 0) {
14777
+ const warningBox = document.createElement("div");
14778
+ warningBox.className = "settings-note project-attention-note";
14779
+ warningBox.textContent = `Needs attention: ${warnings.map((warning) => warning.message).join(" ")}`;
14780
+ row.appendChild(warningBox);
14781
+ }
14782
+ if (project.statuses.length > 0) {
14783
+ const badges = document.createElement("div");
14784
+ badges.className = "project-inventory-badges";
14785
+ for (const status of project.statuses) {
14786
+ const badge = document.createElement("span");
14787
+ badge.className = `project-status-badge ${status}`;
14788
+ badge.textContent = formatStatus(status);
14789
+ badges.appendChild(badge);
14790
+ }
14791
+ row.appendChild(badges);
14792
+ }
14793
+ const detail = document.createElement("details");
14794
+ detail.className = "project-inventory-details";
14795
+ detail.open = openProjectDetails.has(project.workspace_identity);
14796
+ detail.addEventListener("toggle", () => {
14797
+ if (detail.open) openProjectDetails.add(project.workspace_identity);
14798
+ else openProjectDetails.delete(project.workspace_identity);
14799
+ });
14800
+ const summary = document.createElement("summary");
14801
+ summary.textContent = "Identity and mapping details";
14802
+ detail.appendChild(summary);
14803
+ const list = document.createElement("dl");
14804
+ list.className = "project-detail-grid";
14805
+ const fields = [
14806
+ ["Workspace identity", project.workspace_identity],
14807
+ ["Project", project.project],
14808
+ ["CWD", project.cwd],
14809
+ ["Git remote", project.git_remote],
14810
+ ["Git branch", project.git_branch],
14811
+ ["Suggested domain", project.suggested_scope_id],
14812
+ ["Suggestion reason", project.suggestion_reason],
14813
+ ["Sessions", project.session_count],
14814
+ ["Memories", project.memory_count ?? "count unavailable"]
14815
+ ];
14816
+ for (const [label, value] of fields) {
14817
+ const dt = document.createElement("dt");
14818
+ dt.textContent = label;
14819
+ const dd = document.createElement("dd");
14820
+ dd.textContent = value == null || value === "" ? "—" : String(value);
14821
+ list.append(dt, dd);
14822
+ }
14823
+ detail.appendChild(list);
14824
+ detail.appendChild(renderProjectActions(project));
14825
+ row.appendChild(detail);
14826
+ return row;
14827
+ }
14828
+ function clusterDomainLabel(projects) {
14829
+ const uniqueScopes = [...new Set(projects.map((project) => project.resolved_scope_id))];
14830
+ return uniqueScopes.length === 1 ? scopeLabel(uniqueScopes[0]) : "Mixed Sharing domains";
14831
+ }
14832
+ function renderProjectCluster(projects) {
14833
+ if (projects.length === 1) return renderProjectRow(projects[0]);
14834
+ const clusterKey = projectClusterKey(projects[0]);
14835
+ const row = document.createElement("article");
14836
+ row.className = "project-inventory-row project-inventory-cluster";
14837
+ const header = document.createElement("div");
14838
+ header.className = "project-inventory-row-header";
14839
+ const title = document.createElement("div");
14840
+ title.className = "project-inventory-title";
14841
+ title.textContent = projectClusterLabel(projects[0]);
14842
+ header.appendChild(title);
14843
+ const domain = document.createElement("div");
14844
+ domain.className = "project-inventory-domain";
14845
+ domain.textContent = clusterDomainLabel(projects);
14846
+ header.appendChild(domain);
14847
+ row.appendChild(header);
14848
+ const memoryCount = projects.reduce((total, project) => total + (project.memory_count ?? 0), 0);
14849
+ const sessionCount = projects.reduce((total, project) => total + project.session_count, 0);
14850
+ const meta = document.createElement("div");
14851
+ meta.className = "project-inventory-meta";
14852
+ meta.textContent = `${projects.length} identities · ${sessionCount.toLocaleString()} sessions · ${memoryCount.toLocaleString()} memories`;
14853
+ row.appendChild(meta);
14854
+ const actions = document.createElement("div");
14855
+ actions.className = "project-inventory-actions";
14856
+ const hasGuardrailWarnings = projects.some((project) => project.guardrail_warnings.length > 0);
14857
+ const suggestedScopes = new Set(projects.map((project) => project.suggested_scope_id).filter((scopeId) => Boolean(scopeId)));
14858
+ const resolvedScopes = new Set(projects.map((project) => project.resolved_scope_id));
14859
+ const select = document.createElement("select");
14860
+ select.className = "project-domain-select";
14861
+ select.setAttribute("aria-label", `Sharing domain for ${projectClusterLabel(projects[0])} group`);
14862
+ const placeholder = document.createElement("option");
14863
+ placeholder.value = "";
14864
+ placeholder.textContent = "Choose domain…";
14865
+ select.appendChild(placeholder);
14866
+ for (const scope of assignableScopes()) {
14867
+ const option = document.createElement("option");
14868
+ option.value = scope.scope_id;
14869
+ option.textContent = scope.label ? `${scope.label} · ${scope.scope_id}` : scope.scope_id;
14870
+ select.appendChild(option);
14871
+ }
14872
+ select.value = "";
14873
+ const save = document.createElement("button");
14874
+ save.className = "settings-button";
14875
+ save.type = "button";
14876
+ save.textContent = `Save domain for ${projects.length} identities`;
14877
+ save.disabled = true;
14878
+ save.addEventListener("click", () => void saveProjectClusterMapping(projects, select.value));
14879
+ select.addEventListener("change", () => {
14880
+ save.disabled = !select.value || hasGuardrailWarnings;
14881
+ });
14882
+ actions.append(select, save);
14883
+ if (suggestedScopes.size > 1 || resolvedScopes.size > 1 || hasGuardrailWarnings) {
14884
+ const note = document.createElement("div");
14885
+ note.className = "settings-note project-attention-note";
14886
+ note.textContent = hasGuardrailWarnings ? "One or more identities in this group need individual review before bulk assignment." : "This group has mixed suggestions or current domains. Choose a domain explicitly before bulk assignment.";
14887
+ actions.appendChild(note);
14888
+ }
14889
+ row.appendChild(actions);
14890
+ const details = document.createElement("details");
14891
+ details.className = "project-inventory-details";
14892
+ details.open = openProjectClusters.has(clusterKey);
14893
+ details.addEventListener("toggle", () => {
14894
+ if (details.open) openProjectClusters.add(clusterKey);
14895
+ else openProjectClusters.delete(clusterKey);
14896
+ });
14897
+ const summary = document.createElement("summary");
14898
+ summary.textContent = "Show identities in this project";
14899
+ details.appendChild(summary);
14900
+ for (const project of projects) details.appendChild(renderProjectRow(project));
14901
+ row.appendChild(details);
14902
+ return row;
14903
+ }
14904
+ function projectClusters(projects) {
14905
+ const byKey = /* @__PURE__ */ new Map();
14906
+ for (const project of projects) {
14907
+ const key = projectClusterKey(project);
14908
+ byKey.set(key, [...byKey.get(key) ?? [], project]);
14909
+ }
14910
+ return [...byKey.values()];
14911
+ }
14912
+ function renderEmpty(message) {
14913
+ const list = el("projectsInventoryList");
14914
+ if (!list) return;
14915
+ list.textContent = "";
14916
+ const empty = document.createElement("div");
14917
+ empty.className = "settings-note";
14918
+ empty.textContent = message;
14919
+ list.appendChild(empty);
14920
+ }
14921
+ async function loadProjectsData() {
14922
+ const meta = el("projectsInventoryMeta");
14923
+ const list = el("projectsInventoryList");
14924
+ if (!meta || !list) return;
14925
+ meta.textContent = "Loading project inventory…";
14926
+ try {
14927
+ const [result, settings] = await Promise.all([loadProjectScopeInventory({
14928
+ limit: lastLimit,
14929
+ offset: currentOffset,
14930
+ q: el("projectsSearch")?.value.trim() || void 0,
14931
+ status: el("projectsStatusFilter")?.value || void 0
14932
+ }), loadSharingDomainSettings()]);
14933
+ scopes = settings.scopes;
14934
+ list.textContent = "";
14935
+ if (result.projects.length === 0) renderEmpty("No projects match those filters.");
14936
+ else for (const cluster of projectClusters(result.projects)) list.appendChild(renderProjectCluster(cluster));
14937
+ meta.textContent = result.total === 0 ? "0 project identities found" : `${result.total} project identit${result.total === 1 ? "y" : "ies"} found · showing ${result.offset + 1}-${Math.min(result.offset + result.projects.length, result.total)}`;
14938
+ const prev = el("projectsPrevPage");
14939
+ const next = el("projectsNextPage");
14940
+ if (prev) prev.disabled = result.offset === 0;
14941
+ if (next) next.disabled = !result.has_more;
14942
+ } catch (error) {
14943
+ meta.textContent = "Project inventory failed to load.";
14944
+ renderEmpty(error instanceof Error ? error.message : "Unable to load project inventory.");
14945
+ }
14946
+ }
14947
+ function initProjectsTab(refresh) {
14948
+ refreshProjects = refresh;
14949
+ const status = el("projectsStatusFilter");
14950
+ if (status && status.options.length === 0) for (const [value, label] of STATUS_OPTIONS) {
14951
+ const option = document.createElement("option");
14952
+ option.value = value;
14953
+ option.textContent = label;
14954
+ status.appendChild(option);
14955
+ }
14956
+ const requestRefresh = () => {
14957
+ currentOffset = 0;
14958
+ refreshProjects?.();
14959
+ };
14960
+ el("projectsSearch")?.addEventListener("input", requestRefresh);
14961
+ status?.addEventListener("change", requestRefresh);
14962
+ el("projectsPrevPage")?.addEventListener("click", () => {
14963
+ currentOffset = Math.max(0, currentOffset - lastLimit);
14964
+ refreshProjects?.();
14965
+ });
14966
+ el("projectsNextPage")?.addEventListener("click", () => {
14967
+ currentOffset += lastLimit;
14968
+ refreshProjects?.();
14969
+ });
14970
+ }
14971
+ //#endregion
14347
14972
  //#region src/tabs/settings/data/parse.ts
14348
14973
  function parseCommandArgv(raw, options) {
14349
14974
  const text = raw.trim();
@@ -15964,7 +16589,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
15964
16589
  return scopes.find((scope) => scope.scope_id === scopeId)?.label || scopeId;
15965
16590
  }
15966
16591
  function resetDrafts(settings) {
15967
- return Object.fromEntries(settings.projects.map((project) => [project.workspace_identity, project.resolved_scope_id]));
16592
+ return Object.fromEntries(settings.projects.map((project) => [project.workspace_identity, project.suggested_scope_id ?? project.resolved_scope_id]));
15968
16593
  }
15969
16594
  function canAssignProject(project) {
15970
16595
  return project.identity_source !== "unmapped";
@@ -15980,6 +16605,9 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
15980
16605
  function visibleProjectWarnings(project) {
15981
16606
  return (project.guardrail_warnings ?? []).filter((warning) => warning.severity === "warning" || warning.code === "unknown_project_local_only");
15982
16607
  }
16608
+ function isUsingSuggestion(project, currentValue) {
16609
+ return Boolean(project.suggested_scope_id && currentValue === project.suggested_scope_id && project.suggested_scope_id !== project.resolved_scope_id);
16610
+ }
15983
16611
  function settingsMappingWarnings(settings) {
15984
16612
  return settings.mappings.flatMap((mapping) => mapping.guardrail_warnings ?? []);
15985
16613
  }
@@ -16015,7 +16643,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16015
16643
  y$2(() => {
16016
16644
  reload();
16017
16645
  }, []);
16018
- const scopeOptions = T$1(() => (settings?.scopes ?? []).map((scope) => ({
16646
+ const scopeOptions = T$1(() => (settings?.scopes ?? []).filter((scope) => scope.scope_id !== "legacy-shared-review").map((scope) => ({
16019
16647
  value: scope.scope_id,
16020
16648
  label: domainLabel(scope)
16021
16649
  })), [settings?.scopes]);
@@ -16070,20 +16698,32 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16070
16698
  setSavingKey(null);
16071
16699
  }
16072
16700
  };
16701
+ const openProjects = () => {
16702
+ window.location.hash = "projects";
16703
+ };
16073
16704
  return /* @__PURE__ */ u("div", {
16074
16705
  className: "settings-group",
16075
16706
  children: [
16076
16707
  /* @__PURE__ */ u("h3", {
16077
16708
  className: "settings-group-title",
16078
- children: "Sharing domains"
16709
+ children: "Advanced Sharing-domain mappings"
16079
16710
  }),
16080
16711
  /* @__PURE__ */ u("div", {
16081
16712
  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."
16713
+ children: "Use Projects for day-to-day project/domain review. This fallback panel exposes the same mapping controls for troubleshooting and recovery."
16083
16714
  }),
16084
16715
  /* @__PURE__ */ u("div", {
16085
16716
  className: "small",
16086
- children: "Unmapped and unknown projects stay on Local only until you assign a domain."
16717
+ children: "Saving a mapping changes local scope resolution for future writes; it does not grant any peer or coordinator member access by itself."
16718
+ }),
16719
+ /* @__PURE__ */ u("div", {
16720
+ className: "section-actions",
16721
+ children: /* @__PURE__ */ u("button", {
16722
+ className: "settings-button",
16723
+ onClick: openProjects,
16724
+ type: "button",
16725
+ children: "Open Projects review"
16726
+ })
16087
16727
  }),
16088
16728
  settings ? /* @__PURE__ */ u(GuardrailMessages, {
16089
16729
  label: "Sharing domain mapping warnings",
@@ -16109,6 +16749,8 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16109
16749
  const unchanged = currentValue === project.resolved_scope_id;
16110
16750
  const assignable = canAssignProject(project);
16111
16751
  const currentScopeName = scopeName(settings.scopes, project.resolved_scope_id);
16752
+ const usingSuggestion = isUsingSuggestion(project, currentValue);
16753
+ const suggestedScopeName = project.suggested_scope_id ? scopeName(settings.scopes, project.suggested_scope_id) : null;
16112
16754
  const pendingConfirmation = confirmation?.workspaceIdentity === project.workspace_identity ? confirmation : null;
16113
16755
  const canRemoveProjectMapping = assignable && project.mapping_id != null && project.resolution_reason === "exact_mapping";
16114
16756
  return /* @__PURE__ */ u("div", {
@@ -16150,6 +16792,15 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16150
16792
  resolutionLabel(project.resolution_reason)
16151
16793
  ]
16152
16794
  }),
16795
+ project.suggested_scope_id && project.suggestion_reason ? /* @__PURE__ */ u("div", {
16796
+ className: "settings-note",
16797
+ role: "status",
16798
+ children: [
16799
+ /* @__PURE__ */ u("strong", { children: ["Suggested mapping: ", suggestedScopeName] }),
16800
+ /* @__PURE__ */ u("div", { children: project.suggestion_reason }),
16801
+ /* @__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." })
16802
+ ]
16803
+ }) : null,
16153
16804
  /* @__PURE__ */ u(GuardrailMessages, { warnings: visibleProjectWarnings(project) }),
16154
16805
  !assignable ? /* @__PURE__ */ u("div", {
16155
16806
  className: "settings-note",
@@ -16186,7 +16837,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
16186
16837
  disabled: !assignable || saving || unchanged,
16187
16838
  onClick: () => void saveProject(project),
16188
16839
  type: "button",
16189
- children: saving ? "Saving…" : "Save Sharing domain"
16840
+ children: saving ? "Saving…" : usingSuggestion ? "Confirm suggested mapping" : "Save Sharing domain"
16190
16841
  }), /* @__PURE__ */ u("button", {
16191
16842
  className: "settings-button",
16192
16843
  disabled: saving || !canRemoveProjectMapping,
@@ -17346,6 +17997,90 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17346
17997
  emptyMessage: "No Sharing-domain grants exist for this device yet. Project filters below cannot send data by themselves."
17347
17998
  };
17348
17999
  }
18000
+ var PERSONAL_SCOPE_WORDS = [
18001
+ "personal",
18002
+ "private",
18003
+ "home",
18004
+ "mine",
18005
+ "me"
18006
+ ];
18007
+ var OSS_SCOPE_WORDS = [
18008
+ "oss",
18009
+ "open source",
18010
+ "opensource",
18011
+ "community",
18012
+ "public"
18013
+ ];
18014
+ var WORK_LIKE_SCOPE_WORDS = ["work", "client"];
18015
+ var LOCAL_SCOPE_WORDS = [
18016
+ "local",
18017
+ "legacy",
18018
+ "review"
18019
+ ];
18020
+ function scopeSearchText(scope) {
18021
+ return [
18022
+ scope.scope_id,
18023
+ scope.label,
18024
+ scope.kind,
18025
+ scope.authority_type
18026
+ ].map((value) => cleanText(value) ?? "").join(" ").toLowerCase();
18027
+ }
18028
+ function scopeIdentitySearchText(scope) {
18029
+ return [
18030
+ scope.scope_id,
18031
+ scope.label,
18032
+ scope.kind
18033
+ ].map((value) => cleanText(value) ?? "").join(" ").toLowerCase();
18034
+ }
18035
+ function escapeRegex(value) {
18036
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18037
+ }
18038
+ function hasScopeWord(scope, words) {
18039
+ const text = scopeSearchText(scope);
18040
+ return words.some((word) => {
18041
+ const escaped = escapeRegex(word.toLowerCase());
18042
+ return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`).test(text);
18043
+ });
18044
+ }
18045
+ function isPersonalOrOssScope(scope) {
18046
+ return hasScopeWord(scope, PERSONAL_SCOPE_WORDS) || hasScopeWord(scope, OSS_SCOPE_WORDS);
18047
+ }
18048
+ function isLocalOrLegacyScope(scope) {
18049
+ const text = scopeIdentitySearchText(scope);
18050
+ return LOCAL_SCOPE_WORDS.some((word) => {
18051
+ const escaped = escapeRegex(word.toLowerCase());
18052
+ return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`).test(text);
18053
+ });
18054
+ }
18055
+ function isWorkLikeScope(scope) {
18056
+ return !isPersonalOrOssScope(scope) && (!isLocalOrLegacyScope(scope) || hasScopeWord(scope, WORK_LIKE_SCOPE_WORDS));
18057
+ }
18058
+ function derivePeerGrantRoleMismatchView(peer) {
18059
+ const scopes = Array.isArray(peer.authorized_scopes) ? peer.authorized_scopes : [];
18060
+ if (!Boolean(cleanText(peer.discovered_via_group_id) || cleanText(peer.discovered_via_coordinator_id)) || scopes.length === 0) return {
18061
+ badgeLabel: null,
18062
+ detail: "",
18063
+ isVisible: false,
18064
+ message: "",
18065
+ title: ""
18066
+ };
18067
+ const hasPersonalOrOssGrant = scopes.some(isPersonalOrOssScope);
18068
+ const hasWorkLikeGrant = scopes.some(isWorkLikeScope);
18069
+ if (!hasPersonalOrOssGrant || hasWorkLikeGrant) return {
18070
+ badgeLabel: null,
18071
+ detail: "",
18072
+ isVisible: false,
18073
+ message: "",
18074
+ title: ""
18075
+ };
18076
+ return {
18077
+ badgeLabel: "Review domain fit",
18078
+ detail: "Coordinator/group context helps find the device, and project filters only narrow already-authorized domains. Neither one grants a missing work/client domain.",
18079
+ isVisible: true,
18080
+ 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.",
18081
+ title: "Check this device's Sharing-domain role"
18082
+ };
18083
+ }
17349
18084
  function derivePeerProjectNarrowingView(scope) {
17350
18085
  const effectiveInclude = cleanList(scope?.effective_include);
17351
18086
  const effectiveExclude = cleanList(scope?.effective_exclude);
@@ -17714,23 +18449,23 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17714
18449
  let query = "";
17715
18450
  let activeIndex = 0;
17716
18451
  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");
18452
+ const container = el$1("div", "project-scope-picker");
18453
+ const selectedList = el$1("ul", "project-scope-picker-selected");
18454
+ const trigger = el$1("button", "project-scope-picker-trigger settings-button");
17720
18455
  trigger.type = "button";
17721
- const triggerLabel = el("span", null, placeholder || "Add project");
17722
- const triggerPlus = el("span", null, "+");
18456
+ const triggerLabel = el$1("span", null, placeholder || "Add project");
18457
+ const triggerPlus = el$1("span", null, "+");
17723
18458
  triggerPlus.setAttribute("aria-hidden", "true");
17724
18459
  trigger.append(triggerPlus, triggerLabel);
17725
18460
  trigger.setAttribute("aria-haspopup", "listbox");
17726
18461
  trigger.setAttribute("aria-expanded", "false");
17727
- const popover = el("div", "project-scope-picker-popover project-scope-picker-popover--inline");
18462
+ const popover = el$1("div", "project-scope-picker-popover project-scope-picker-popover--inline");
17728
18463
  popover.hidden = true;
17729
- const search = el("input", "project-scope-picker-search");
18464
+ const search = el$1("input", "project-scope-picker-search");
17730
18465
  search.type = "text";
17731
18466
  search.placeholder = "Search or create…";
17732
18467
  search.setAttribute("aria-label", "Search projects");
17733
- const results = el("div", "project-scope-picker-results");
18468
+ const results = el$1("div", "project-scope-picker-results");
17734
18469
  results.setAttribute("role", "listbox");
17735
18470
  popover.append(search, results);
17736
18471
  const knownPool = () => {
@@ -17783,14 +18518,14 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17783
18518
  const renderSelected = () => {
17784
18519
  selectedList.textContent = "";
17785
18520
  if (!values.length) {
17786
- const note = el("li", "project-scope-picker-selected-empty", emptyLabel);
18521
+ const note = el$1("li", "project-scope-picker-selected-empty", emptyLabel);
17787
18522
  selectedList.appendChild(note);
17788
18523
  return;
17789
18524
  }
17790
18525
  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", "×");
18526
+ const chip = el$1("li", "project-scope-picker-selected-chip");
18527
+ const label = el$1("span", null, value);
18528
+ const remove = el$1("button", "project-scope-picker-selected-remove", "×");
17794
18529
  remove.type = "button";
17795
18530
  remove.setAttribute("aria-label", `Remove ${value}`);
17796
18531
  remove.addEventListener("click", () => {
@@ -17809,19 +18544,19 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17809
18544
  const rowCount = rows.length + (withCreate ? 1 : 0);
17810
18545
  if (activeIndex >= rowCount) activeIndex = Math.max(0, rowCount - 1);
17811
18546
  if (!rows.length && !withCreate) {
17812
- const empty = el("div", "project-scope-picker-empty-row", "No projects yet. Type a name to create one.");
18547
+ const empty = el$1("div", "project-scope-picker-empty-row", "No projects yet. Type a name to create one.");
17813
18548
  results.appendChild(empty);
17814
18549
  return;
17815
18550
  }
17816
18551
  rows.forEach((project, index) => {
17817
18552
  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");
18553
+ const row = el$1("button", index === activeIndex ? "project-scope-picker-row project-scope-picker-row--active" : "project-scope-picker-row");
17819
18554
  row.type = "button";
17820
18555
  row.setAttribute("role", "option");
17821
18556
  row.setAttribute("aria-selected", selected ? "true" : "false");
17822
- const check = el("span", "project-scope-picker-row-check", selected ? "✓" : "");
18557
+ const check = el$1("span", "project-scope-picker-row-check", selected ? "✓" : "");
17823
18558
  check.setAttribute("aria-hidden", "true");
17824
- const labelEl = el("span", "project-scope-picker-row-label", project);
18559
+ const labelEl = el$1("span", "project-scope-picker-row-label", project);
17825
18560
  row.append(check, labelEl);
17826
18561
  row.addEventListener("click", () => {
17827
18562
  activeIndex = index;
@@ -17835,12 +18570,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17835
18570
  });
17836
18571
  if (withCreate) {
17837
18572
  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");
18573
+ 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
18574
  row.type = "button";
17840
18575
  row.setAttribute("role", "option");
17841
- const check = el("span", "project-scope-picker-row-check", "+");
18576
+ const check = el$1("span", "project-scope-picker-row-check", "+");
17842
18577
  check.setAttribute("aria-hidden", "true");
17843
- const labelEl = el("span", "project-scope-picker-row-label", `Create "${query.trim()}"`);
18578
+ const labelEl = el$1("span", "project-scope-picker-row-label", `Create "${query.trim()}"`);
17844
18579
  row.append(check, labelEl);
17845
18580
  row.addEventListener("click", () => createFromQuery());
17846
18581
  row.addEventListener("mouseenter", () => {
@@ -17920,11 +18655,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
17920
18655
  }
17921
18656
  container.hidden = false;
17922
18657
  actions.slice(0, 2).forEach((item) => {
17923
- const row = el("div", "sync-action");
17924
- const textWrap = el("div", "sync-action-text");
18658
+ const row = el$1("div", "sync-action");
18659
+ const textWrap = el$1("div", "sync-action-text");
17925
18660
  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");
18661
+ textWrap.appendChild(el$1("span", "sync-action-command", item.command));
18662
+ const btn = el$1("button", "settings-button sync-action-copy", "Copy");
17928
18663
  btn.addEventListener("click", () => copyToClipboard(item.command, btn));
17929
18664
  row.append(textWrap, btn);
17930
18665
  container.appendChild(row);
@@ -18645,6 +19380,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
18645
19380
  const directionHint = DIRECTION_GLYPH[derivePeerDirection(peer)];
18646
19381
  const peerStatus = peer.status || {};
18647
19382
  const authorizedDomains = derivePeerAuthorizedDomainsView(peer);
19383
+ const grantRoleMismatch = derivePeerGrantRoleMismatchView(peer);
18648
19384
  const scopeRejections = derivePeerScopeRejectionsView(peer);
18649
19385
  const scope = peer.project_scope || {};
18650
19386
  const projectNarrowing = derivePeerProjectNarrowingView(scope);
@@ -18900,6 +19636,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
18900
19636
  title: "Inbound ops rejected by the sharing-domain scope gate. Expand for reason codes.",
18901
19637
  children: scopeRejections.badgeLabel
18902
19638
  }) : null,
19639
+ grantRoleMismatch.badgeLabel ? /* @__PURE__ */ u("span", {
19640
+ className: "badge badge-offline",
19641
+ title: "Review whether explicit Sharing-domain grants match this device's intended role.",
19642
+ children: grantRoleMismatch.badgeLabel
19643
+ }) : null,
18903
19644
  peer.discovered_via_group_id ? /* @__PURE__ */ u("span", {
18904
19645
  className: "badge actor-badge",
18905
19646
  title: `Discovered through coordinator group ${peer.discovered_via_group_id}`,
@@ -19008,6 +19749,15 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
19008
19749
  children: domain.label
19009
19750
  }, domain.scopeId))
19010
19751
  }) : null,
19752
+ grantRoleMismatch.isVisible ? /* @__PURE__ */ u("div", {
19753
+ className: "settings-note",
19754
+ role: "status",
19755
+ children: [
19756
+ /* @__PURE__ */ u("strong", { children: grantRoleMismatch.title }),
19757
+ /* @__PURE__ */ u("div", { children: grantRoleMismatch.message }),
19758
+ /* @__PURE__ */ u("div", { children: grantRoleMismatch.detail })
19759
+ ]
19760
+ }) : null,
19011
19761
  /* @__PURE__ */ u("div", {
19012
19762
  className: "peer-meta",
19013
19763
  children: projectNarrowing.note
@@ -20497,6 +21247,32 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20497
21247
  }
20498
21248
  //#endregion
20499
21249
  //#region src/tabs/sync/components/sync-sharing-review.tsx
21250
+ function formatIdentitySource(source) {
21251
+ switch (source) {
21252
+ case "cwd": return "Matched by folder";
21253
+ case "git_remote": return "Matched by git remote";
21254
+ case "workspace_id": return "Matched by workspace";
21255
+ case "project": return "Matched by project name";
21256
+ case "unmapped": return "Missing project identity";
21257
+ default: return `Matched by ${source.replace(/_/g, " ")}`;
21258
+ }
21259
+ }
21260
+ function needsProjectCleanup(group) {
21261
+ const label = group.displayProject.toLowerCase();
21262
+ return label.includes("fatal:") || label.includes("not a git repository") || label === "unknown project" || group.identitySource === "unmapped";
21263
+ }
21264
+ function canReassignLegacyGroup(group) {
21265
+ return (group.reassignableMemoryCount ?? group.memoryCount) > 0;
21266
+ }
21267
+ function groupSearchText(group) {
21268
+ return [
21269
+ group.displayProject,
21270
+ group.identitySource,
21271
+ group.suggestedScopeId ?? "",
21272
+ group.suggestionReason ?? "",
21273
+ group.workspaceIdentity
21274
+ ].join(" ").toLowerCase();
21275
+ }
20500
21276
  function SharingReviewRow({ item, onReview }) {
20501
21277
  return /* @__PURE__ */ u("div", {
20502
21278
  className: "actor-row",
@@ -20530,31 +21306,354 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20530
21306
  })]
20531
21307
  });
20532
21308
  }
20533
- function LegacySharedReviewRow({ item }) {
21309
+ function LegacySharedReviewRow({ item, onReassign, onReview }) {
21310
+ const groups = item.groups ?? [];
21311
+ const targetScopes = item.targetScopes ?? [];
21312
+ const initialSelections = Object.fromEntries(groups.map((group) => [group.workspaceIdentity, group.suggestedScopeId ?? ""]));
21313
+ const [pending, setPending] = d$2({});
21314
+ const [errors, setErrors] = d$2({});
21315
+ const [selectedScopes, setSelectedScopes] = d$2(initialSelections);
21316
+ const [selectedGroups, setSelectedGroups] = d$2({});
21317
+ const [filter, setFilter] = d$2("all");
21318
+ const [query, setQuery] = d$2("");
21319
+ const [busyIdentity, setBusyIdentity] = d$2(null);
21320
+ const shownGroupCount = groups.length;
21321
+ const groupCount = item.totalGroupCount ?? shownGroupCount;
21322
+ const cleanupCount = groups.filter(needsProjectCleanup).length;
21323
+ const suggestedCount = groups.filter((group) => group.suggestedScopeId && !needsProjectCleanup(group) && canReassignLegacyGroup(group)).length;
21324
+ const selectedReassignableCount = groups.filter((group) => selectedGroups[group.workspaceIdentity] && canReassignLegacyGroup(group)).length;
21325
+ const selectedCount = groups.filter((group) => selectedGroups[group.workspaceIdentity] && canReassignLegacyGroup(group) && Boolean(selectedScopes[group.workspaceIdentity])).length;
21326
+ const visibleGroups = groups.filter((group) => {
21327
+ const cleanup = needsProjectCleanup(group);
21328
+ if (filter === "suggested" && (!group.suggestedScopeId || cleanup)) return false;
21329
+ if (filter === "cleanup" && !cleanup) return false;
21330
+ if (filter === "no-suggestion" && (group.suggestedScopeId || cleanup)) return false;
21331
+ if (filter === "selected" && !selectedGroups[group.workspaceIdentity]) return false;
21332
+ return !query.trim() || groupSearchText(group).includes(query.trim().toLowerCase());
21333
+ });
21334
+ async function applyGroup(group) {
21335
+ const selectedScopeId = selectedScopes[group.workspaceIdentity] || group.suggestedScopeId || "";
21336
+ if (!selectedScopeId || !onReassign || busyIdentity) return;
21337
+ setBusyIdentity(group.workspaceIdentity);
21338
+ setErrors((current) => {
21339
+ const next = { ...current };
21340
+ delete next[group.workspaceIdentity];
21341
+ return next;
21342
+ });
21343
+ try {
21344
+ const preview = await onReassign(group, selectedScopeId, Boolean(pending[group.workspaceIdentity]), pending[group.workspaceIdentity]?.confirmation_token);
21345
+ if (preview) setPending((current) => ({
21346
+ ...current,
21347
+ [group.workspaceIdentity]: preview
21348
+ }));
21349
+ else setPending((current) => {
21350
+ const next = { ...current };
21351
+ delete next[group.workspaceIdentity];
21352
+ return next;
21353
+ });
21354
+ } catch (error) {
21355
+ setErrors((current) => ({
21356
+ ...current,
21357
+ [group.workspaceIdentity]: error instanceof Error ? error.message : "Unable to reassign legacy review memories."
21358
+ }));
21359
+ } finally {
21360
+ setBusyIdentity(null);
21361
+ }
21362
+ }
21363
+ function updateSelectedScope(group, scopeId) {
21364
+ setSelectedScopes((current) => ({
21365
+ ...current,
21366
+ [group.workspaceIdentity]: scopeId
21367
+ }));
21368
+ setPending((current) => {
21369
+ const next = { ...current };
21370
+ delete next[group.workspaceIdentity];
21371
+ return next;
21372
+ });
21373
+ setErrors((current) => {
21374
+ const next = { ...current };
21375
+ delete next[group.workspaceIdentity];
21376
+ return next;
21377
+ });
21378
+ }
21379
+ function toggleGroup(group, checked) {
21380
+ setSelectedGroups((current) => ({
21381
+ ...current,
21382
+ [group.workspaceIdentity]: checked
21383
+ }));
21384
+ }
21385
+ function setVisibleSelected(checked) {
21386
+ setSelectedGroups((current) => ({
21387
+ ...current,
21388
+ ...Object.fromEntries(visibleGroups.filter(canReassignLegacyGroup).map((group) => [group.workspaceIdentity, checked]))
21389
+ }));
21390
+ }
21391
+ function selectSuggestedGroups() {
21392
+ setSelectedGroups((current) => ({
21393
+ ...current,
21394
+ ...Object.fromEntries(groups.filter((group) => group.suggestedScopeId && !needsProjectCleanup(group) && canReassignLegacyGroup(group)).map((group) => [group.workspaceIdentity, true]))
21395
+ }));
21396
+ }
21397
+ async function applySelectedGroups() {
21398
+ if (!onReassign || busyIdentity || selectedCount === 0) return;
21399
+ const selected = groups.filter((group) => selectedGroups[group.workspaceIdentity] && canReassignLegacyGroup(group) && Boolean(selectedScopes[group.workspaceIdentity]));
21400
+ if (selected.every((group) => pending[group.workspaceIdentity])) return;
21401
+ setBusyIdentity("bulk");
21402
+ try {
21403
+ for (const group of selected) {
21404
+ if (pending[group.workspaceIdentity]) continue;
21405
+ const selectedScopeId = selectedScopes[group.workspaceIdentity] || group.suggestedScopeId || "";
21406
+ if (!selectedScopeId) continue;
21407
+ try {
21408
+ const preview = await onReassign(group, selectedScopeId, false);
21409
+ setErrors((current) => {
21410
+ const next = { ...current };
21411
+ delete next[group.workspaceIdentity];
21412
+ return next;
21413
+ });
21414
+ if (preview) setPending((current) => ({
21415
+ ...current,
21416
+ [group.workspaceIdentity]: preview
21417
+ }));
21418
+ } catch (error) {
21419
+ setErrors((current) => ({
21420
+ ...current,
21421
+ [group.workspaceIdentity]: error instanceof Error ? error.message : "Unable to reassign legacy review memories."
21422
+ }));
21423
+ }
21424
+ }
21425
+ } finally {
21426
+ setBusyIdentity(null);
21427
+ }
21428
+ }
20534
21429
  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"
21430
+ className: "legacy-review-card",
21431
+ children: [
21432
+ /* @__PURE__ */ u("div", {
21433
+ className: "legacy-review-header",
21434
+ children: [/* @__PURE__ */ u("div", { children: [
21435
+ /* @__PURE__ */ u("div", {
21436
+ className: "actor-title",
21437
+ children: [/* @__PURE__ */ u("strong", { children: "Legacy shared review" }), /* @__PURE__ */ u("span", {
21438
+ className: "badge badge-offline",
21439
+ children: "Needs project review"
21440
+ })]
21441
+ }),
21442
+ /* @__PURE__ */ u("div", {
21443
+ className: "peer-meta legacy-review-summary",
21444
+ children: [
21445
+ groupCount.toLocaleString(),
21446
+ " older",
21447
+ " ",
21448
+ groupCount === 1 ? "project needs" : "projects need",
21449
+ " a Sharing domain. They contain",
21450
+ " ",
21451
+ item.memoryCount.toLocaleString(),
21452
+ " older shared memories total; review the projects, not individual memories.",
21453
+ shownGroupCount < groupCount ? ` Showing ${shownGroupCount.toLocaleString()} projects; open Projects to review the rest.` : ""
21454
+ ]
21455
+ }),
21456
+ /* @__PURE__ */ u("div", {
21457
+ className: "peer-meta legacy-review-warning",
21458
+ children: "Nothing moves automatically. Reassignment changes future sync authorization; it does not erase copies peers already received."
21459
+ })
21460
+ ] }), /* @__PURE__ */ u("div", {
21461
+ className: "actor-actions",
21462
+ children: /* @__PURE__ */ u("button", {
21463
+ type: "button",
21464
+ className: "settings-button",
21465
+ onClick: onReview,
21466
+ children: "Manage all projects"
21467
+ })
20543
21468
  })]
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
- })
21469
+ }),
21470
+ /* @__PURE__ */ u("div", {
21471
+ className: "legacy-review-toolbar",
21472
+ children: [/* @__PURE__ */ u("div", {
21473
+ className: "legacy-review-filter-row",
21474
+ children: [
21475
+ ["all", `Showing ${shownGroupCount.toLocaleString()}`],
21476
+ ["suggested", `Suggested ${suggestedCount.toLocaleString()}`],
21477
+ ["cleanup", `Needs cleanup ${cleanupCount.toLocaleString()}`],
21478
+ ["no-suggestion", "No suggestion"],
21479
+ ["selected", `Selected ${selectedCount.toLocaleString()}`]
21480
+ ].map(([value, label]) => /* @__PURE__ */ u("button", {
21481
+ className: filter === value ? "settings-button legacy-filter active" : "settings-button legacy-filter",
21482
+ onClick: () => setFilter(value),
21483
+ type: "button",
21484
+ children: label
21485
+ }, value))
21486
+ }), /* @__PURE__ */ u("div", {
21487
+ className: "legacy-review-bulk-row",
21488
+ children: [
21489
+ /* @__PURE__ */ u("input", {
21490
+ "aria-label": "Search legacy project groups",
21491
+ className: "peer-scope-input legacy-review-search",
21492
+ onInput: (event) => setQuery(event.currentTarget.value),
21493
+ placeholder: "Search project, signal, or destination…",
21494
+ value: query
21495
+ }),
21496
+ /* @__PURE__ */ u("button", {
21497
+ className: "settings-button",
21498
+ onClick: () => setVisibleSelected(true),
21499
+ type: "button",
21500
+ children: "Select visible"
21501
+ }),
21502
+ /* @__PURE__ */ u("button", {
21503
+ className: "settings-button",
21504
+ onClick: selectSuggestedGroups,
21505
+ type: "button",
21506
+ children: "Select suggested"
21507
+ }),
21508
+ /* @__PURE__ */ u("button", {
21509
+ className: "settings-button",
21510
+ onClick: () => setVisibleSelected(false),
21511
+ type: "button",
21512
+ children: "Clear visible"
21513
+ }),
21514
+ onReassign && targetScopes.length > 0 ? /* @__PURE__ */ u("button", {
21515
+ className: "settings-save",
21516
+ disabled: busyIdentity != null || selectedCount === 0 || groups.filter((group) => selectedGroups[group.workspaceIdentity] && canReassignLegacyGroup(group) && Boolean(selectedScopes[group.workspaceIdentity])).every((group) => pending[group.workspaceIdentity]),
21517
+ onClick: () => void applySelectedGroups(),
21518
+ type: "button",
21519
+ children: busyIdentity === "bulk" ? "Previewing…" : `Preview ${selectedCount.toLocaleString()} selected`
21520
+ }) : onReassign && selectedReassignableCount > 0 ? /* @__PURE__ */ u("span", {
21521
+ className: "legacy-review-cleanup-note",
21522
+ children: "Add or join a Sharing domain before bulk reassignment."
21523
+ }) : null,
21524
+ selectedCount > 0 && groups.filter((group) => selectedGroups[group.workspaceIdentity] && canReassignLegacyGroup(group) && Boolean(selectedScopes[group.workspaceIdentity])).every((group) => pending[group.workspaceIdentity]) ? /* @__PURE__ */ u("span", {
21525
+ className: "legacy-review-cleanup-note",
21526
+ children: "Bulk actions only preview. Confirm each project individually to avoid partial reassignment."
21527
+ }) : null
21528
+ ]
21529
+ })]
21530
+ }),
21531
+ groups.length > 0 ? /* @__PURE__ */ u("ul", {
21532
+ className: "legacy-review-groups",
21533
+ "aria-label": "Legacy shared review project groups",
21534
+ children: visibleGroups.map((group) => /* @__PURE__ */ u("li", {
21535
+ className: "legacy-review-group",
21536
+ children: [
21537
+ /* @__PURE__ */ u("div", {
21538
+ className: "legacy-review-group-main",
21539
+ children: [/* @__PURE__ */ u("div", {
21540
+ className: "legacy-review-title-wrap",
21541
+ children: [/* @__PURE__ */ u("input", {
21542
+ "aria-label": `Select ${group.displayProject}`,
21543
+ checked: Boolean(selectedGroups[group.workspaceIdentity]),
21544
+ className: "cm-checkbox",
21545
+ disabled: !canReassignLegacyGroup(group),
21546
+ onChange: (event) => toggleGroup(group, event.currentTarget.checked),
21547
+ type: "checkbox"
21548
+ }), /* @__PURE__ */ u("div", { children: [
21549
+ /* @__PURE__ */ u("div", {
21550
+ className: "legacy-review-group-title",
21551
+ children: group.displayProject
21552
+ }),
21553
+ /* @__PURE__ */ u("div", {
21554
+ className: "legacy-review-group-meta",
21555
+ children: [formatIdentitySource(group.identitySource), group.suggestedScopeId ? ` · suggested ${group.suggestedScopeId}` : ""]
21556
+ }),
21557
+ !canReassignLegacyGroup(group) ? /* @__PURE__ */ u("div", {
21558
+ className: "legacy-review-cleanup-note",
21559
+ children: "Peer-owned only. These older memories were received from another device, so this device cannot reassign them to a Sharing domain."
21560
+ }) : group.peerOwnedMemoryCount ? /* @__PURE__ */ u("div", {
21561
+ className: "legacy-review-cleanup-note",
21562
+ children: [
21563
+ (group.reassignableMemoryCount ?? group.memoryCount).toLocaleString(),
21564
+ " ",
21565
+ "local memor",
21566
+ (group.reassignableMemoryCount ?? group.memoryCount) === 1 ? "y" : "ies",
21567
+ " ",
21568
+ "can be reassigned; ",
21569
+ group.peerOwnedMemoryCount.toLocaleString(),
21570
+ " peer-owned memor",
21571
+ group.peerOwnedMemoryCount === 1 ? "y" : "ies",
21572
+ " will stay in review."
21573
+ ]
21574
+ }) : null,
21575
+ needsProjectCleanup(group) ? /* @__PURE__ */ u("div", {
21576
+ className: "legacy-review-cleanup-note",
21577
+ children: "Unclear project identity. Inspect or correct the project before trusting a domain."
21578
+ }) : null
21579
+ ] })]
21580
+ }), /* @__PURE__ */ u("span", {
21581
+ className: "legacy-review-count",
21582
+ children: [
21583
+ group.memoryCount.toLocaleString(),
21584
+ " memor",
21585
+ group.memoryCount === 1 ? "y" : "ies"
21586
+ ]
21587
+ })]
21588
+ }),
21589
+ group.suggestionReason ? /* @__PURE__ */ u("div", {
21590
+ className: "legacy-review-reason",
21591
+ children: group.suggestionReason
21592
+ }) : null,
21593
+ errors[group.workspaceIdentity] ? /* @__PURE__ */ u("div", {
21594
+ className: "legacy-review-confirmation",
21595
+ role: "alert",
21596
+ children: errors[group.workspaceIdentity]
21597
+ }) : null,
21598
+ pending[group.workspaceIdentity] ? /* @__PURE__ */ u("div", {
21599
+ className: "legacy-review-confirmation",
21600
+ role: "alert",
21601
+ children: [
21602
+ pending[group.workspaceIdentity].warning,
21603
+ " This will reassign",
21604
+ " ",
21605
+ pending[group.workspaceIdentity].reassignable_memory_count.toLocaleString(),
21606
+ " of",
21607
+ " ",
21608
+ pending[group.workspaceIdentity].memory_count.toLocaleString(),
21609
+ " memories",
21610
+ pending[group.workspaceIdentity].skipped_memory_count ? `; ${pending[group.workspaceIdentity].skipped_memory_count.toLocaleString()} peer-owned copies will be left unchanged` : "",
21611
+ "."
21612
+ ]
21613
+ }) : null,
21614
+ targetScopes.length > 0 && onReassign && canReassignLegacyGroup(group) ? /* @__PURE__ */ u("label", {
21615
+ className: "legacy-review-target",
21616
+ children: [/* @__PURE__ */ u("span", { children: "Destination Sharing domain" }), /* @__PURE__ */ u("select", {
21617
+ className: "peer-scope-input",
21618
+ disabled: busyIdentity != null,
21619
+ onChange: (event) => updateSelectedScope(group, event.currentTarget.value),
21620
+ value: selectedScopes[group.workspaceIdentity] || "",
21621
+ children: [/* @__PURE__ */ u("option", {
21622
+ value: "",
21623
+ children: "Choose domain…"
21624
+ }), targetScopes.map((scope) => /* @__PURE__ */ u("option", {
21625
+ value: scope.scopeId,
21626
+ children: [
21627
+ scope.label,
21628
+ " · ",
21629
+ scope.authorityType,
21630
+ scope.scopeId === group.suggestedScopeId ? " · suggested" : ""
21631
+ ]
21632
+ }, scope.scopeId))]
21633
+ })]
21634
+ }) : null,
21635
+ targetScopes.length > 0 && onReassign && canReassignLegacyGroup(group) ? /* @__PURE__ */ u("div", {
21636
+ className: "legacy-review-actions",
21637
+ children: /* @__PURE__ */ u("button", {
21638
+ type: "button",
21639
+ className: "settings-button",
21640
+ disabled: busyIdentity != null || !selectedScopes[group.workspaceIdentity],
21641
+ onClick: () => void applyGroup(group),
21642
+ children: busyIdentity === group.workspaceIdentity ? pending[group.workspaceIdentity] ? "Reassigning…" : "Previewing…" : pending[group.workspaceIdentity] ? "I understand, reassign memories" : "Preview reassignment"
21643
+ })
21644
+ }) : null
21645
+ ]
21646
+ }, group.workspaceIdentity))
21647
+ }) : null
21648
+ ]
20554
21649
  });
20555
21650
  }
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, {
21651
+ function SyncSharingReview({ items, legacyReview, onLegacyReassign, onLegacyReview, onReview }) {
21652
+ return /* @__PURE__ */ u(k$3, { children: [legacyReview ? /* @__PURE__ */ u(LegacySharedReviewRow, {
21653
+ item: legacyReview,
21654
+ onReassign: onLegacyReassign,
21655
+ onReview: onLegacyReview ?? onReview
21656
+ }) : null, items.map((item) => /* @__PURE__ */ u(SharingReviewRow, {
20558
21657
  item,
20559
21658
  onReview
20560
21659
  }, `${item.peerName}:${item.actorId}:${item.scopeLabel}`))] });
@@ -20566,6 +21665,28 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20566
21665
  state.feedQuery = "";
20567
21666
  window.location.hash = "feed";
20568
21667
  }
21668
+ function openProjectsReview() {
21669
+ window.location.hash = "projects";
21670
+ }
21671
+ async function reassignLegacySharedReviewGroup(group, scopeId, confirmedOldCopies, confirmationToken) {
21672
+ try {
21673
+ const result = await reassignLegacySharedReviewGroup$1({
21674
+ ...confirmationToken ? { confirmation_token: confirmationToken } : {},
21675
+ confirmed_old_copies: confirmedOldCopies,
21676
+ scope_id: scopeId,
21677
+ workspace_identity: group.workspaceIdentity
21678
+ });
21679
+ state.lastSyncLegacySharedReview = result.legacy_shared_review ?? null;
21680
+ showGlobalNotice(`Reassigned ${result.reassigned_memory_count.toLocaleString()} legacy review memor${result.reassigned_memory_count === 1 ? "y" : "ies"} to ${result.target_scope_label}.`);
21681
+ renderSyncSharingReview();
21682
+ return null;
21683
+ } catch (error) {
21684
+ if (error instanceof LegacySharedReviewConfirmationError) return error.preview;
21685
+ const message = error instanceof Error ? error.message : "Unable to reassign legacy review memories.";
21686
+ showGlobalNotice(message, "warning");
21687
+ throw new Error(message);
21688
+ }
21689
+ }
20569
21690
  function renderSyncSharingReview() {
20570
21691
  const panel = document.getElementById("syncSharingReview");
20571
21692
  const meta = document.getElementById("syncSharingReviewMeta");
@@ -20576,8 +21697,31 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20576
21697
  const legacyRaw = state.lastSyncLegacySharedReview;
20577
21698
  const legacyCount = Math.max(0, Number(legacyRaw?.memory_count ?? 0));
20578
21699
  const legacyReview = legacyRaw?.has_data ? {
21700
+ groups: Array.isArray(legacyRaw.groups) ? legacyRaw.groups.map((group) => {
21701
+ const raw = group;
21702
+ return {
21703
+ displayProject: String(raw.display_project || raw.project || "Unknown project"),
21704
+ identitySource: String(raw.identity_source || "unknown"),
21705
+ lastUpdatedAt: typeof raw.last_updated_at === "string" ? raw.last_updated_at : null,
21706
+ memoryCount: Number(raw.memory_count || 0),
21707
+ peerOwnedMemoryCount: Number(raw.peer_owned_memory_count || 0),
21708
+ reassignableMemoryCount: Number(raw.reassignable_memory_count || 0),
21709
+ suggestedScopeId: typeof raw.suggested_scope_id === "string" ? raw.suggested_scope_id : null,
21710
+ suggestionReason: typeof raw.suggestion_reason === "string" ? raw.suggestion_reason : null,
21711
+ workspaceIdentity: String(raw.workspace_identity || raw.display_project || "unknown")
21712
+ };
21713
+ }) : [],
20579
21714
  memoryCount: legacyCount,
20580
- scopeId: String(legacyRaw.scope_id || "legacy-shared-review")
21715
+ scopeId: String(legacyRaw.scope_id || "legacy-shared-review"),
21716
+ totalGroupCount: Number(legacyRaw.total_group_count || legacyRaw.groups?.length || 0),
21717
+ targetScopes: Array.isArray(legacyRaw.target_scopes) ? legacyRaw.target_scopes.map((scope) => {
21718
+ const raw = scope;
21719
+ return {
21720
+ authorityType: String(raw.authority_type || "unknown"),
21721
+ label: String(raw.label || raw.scope_id || "Unknown domain"),
21722
+ scopeId: String(raw.scope_id || "")
21723
+ };
21724
+ }).filter((scope) => scope.scopeId) : []
20581
21725
  } : null;
20582
21726
  if (!items.length && !legacyReview) {
20583
21727
  clearSyncMount(list);
@@ -20598,6 +21742,8 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20598
21742
  shareableCount: Number(item.shareable_count || 0)
20599
21743
  })),
20600
21744
  legacyReview,
21745
+ onLegacyReassign: reassignLegacySharedReviewGroup,
21746
+ onLegacyReview: openProjectsReview,
20601
21747
  onReview: openFeedSharingReview
20602
21748
  }));
20603
21749
  }
@@ -20783,13 +21929,121 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20783
21929
  try {
20784
21930
  const runtime = await loadRuntimeInfo();
20785
21931
  if (!runtime?.version) return;
20786
- setRuntimeLabel(runtime.version, "21eac98");
21932
+ setRuntimeLabel(runtime.version, "3400309");
20787
21933
  } catch {}
20788
21934
  }
20789
21935
  var lastAnnouncedRefreshState = null;
20790
21936
  var RECONNECT_POLL_MS = 1500;
21937
+ var LEGACY_UPGRADE_NOTICE_DISMISSED_KEY = "codemem-legacy-upgrade-notice-dismissed";
20791
21938
  var reconnectTimer = null;
20792
21939
  var reconnecting = false;
21940
+ var legacyUpgradeNoticeShown = false;
21941
+ var legacyUpgradeNoticePreviousFocus = null;
21942
+ function readNonNegativeCount(value, fallback = 0) {
21943
+ const count = Number(value);
21944
+ return Number.isFinite(count) ? Math.max(0, count) : fallback;
21945
+ }
21946
+ function readLegacyUpgradeReviewSummary(payload) {
21947
+ if (!payload || typeof payload !== "object") return null;
21948
+ const review = payload.legacy_shared_review ?? payload;
21949
+ if (!review || typeof review !== "object") return null;
21950
+ const raw = review;
21951
+ if (raw.has_data !== true) return null;
21952
+ const groups = Array.isArray(raw.groups) ? raw.groups : [];
21953
+ const groupCount = readNonNegativeCount(raw.total_group_count, groups.length);
21954
+ const memoryCount = readNonNegativeCount(raw.memory_count);
21955
+ if (groupCount <= 0 || memoryCount <= 0) return null;
21956
+ return {
21957
+ groupCount,
21958
+ memoryCount
21959
+ };
21960
+ }
21961
+ function isLegacyUpgradeNoticeDismissed() {
21962
+ try {
21963
+ return localStorage.getItem(LEGACY_UPGRADE_NOTICE_DISMISSED_KEY) === "1";
21964
+ } catch {
21965
+ return false;
21966
+ }
21967
+ }
21968
+ function dismissLegacyUpgradeNoticeIfRequested() {
21969
+ if (!document.getElementById("legacyUpgradeDontShow")?.checked) return;
21970
+ try {
21971
+ localStorage.setItem(LEGACY_UPGRADE_NOTICE_DISMISSED_KEY, "1");
21972
+ } catch {}
21973
+ }
21974
+ function legacyUpgradeFocusableElements() {
21975
+ const modal = $("legacyUpgradeModal");
21976
+ if (!modal || modal.hidden) return [];
21977
+ 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);
21978
+ }
21979
+ function focusLegacyUpgradeModal() {
21980
+ const focusable = legacyUpgradeFocusableElements();
21981
+ const primary = $("legacyUpgradeReviewGroups");
21982
+ (primary && focusable.includes(primary) ? primary : focusable[0])?.focus();
21983
+ }
21984
+ function handleLegacyUpgradeModalKeydown(event) {
21985
+ const modal = $("legacyUpgradeModal");
21986
+ if (!modal || modal.hidden) return;
21987
+ if (event.key === "Escape") {
21988
+ event.preventDefault();
21989
+ dismissLegacyUpgradeNoticeIfRequested();
21990
+ setLegacyUpgradeNotice(false);
21991
+ return;
21992
+ }
21993
+ if (event.key !== "Tab") return;
21994
+ const focusable = legacyUpgradeFocusableElements();
21995
+ if (focusable.length === 0) return;
21996
+ const first = focusable[0];
21997
+ const last = focusable[focusable.length - 1];
21998
+ if (event.shiftKey && document.activeElement === first) {
21999
+ event.preventDefault();
22000
+ last.focus();
22001
+ return;
22002
+ }
22003
+ if (!event.shiftKey && document.activeElement === last) {
22004
+ event.preventDefault();
22005
+ first.focus();
22006
+ }
22007
+ }
22008
+ function setLegacyUpgradeBackgroundInert(inert) {
22009
+ for (const element of document.querySelectorAll("header, .tab-bar, .tab-panel, #toastRoot, #viewerReconnectOverlay, #settingsDialogMount")) {
22010
+ if (element.id === "viewerReconnectOverlay" && element.hidden) continue;
22011
+ element.inert = inert;
22012
+ if (inert) element.setAttribute("aria-hidden", "true");
22013
+ else element.removeAttribute("aria-hidden");
22014
+ }
22015
+ }
22016
+ function setLegacyUpgradeNotice(open, summary) {
22017
+ const overlay = $("legacyUpgradeModalBackdrop");
22018
+ const modal = $("legacyUpgradeModal");
22019
+ const summaryEl = $("legacyUpgradeSummary");
22020
+ if (!overlay || !modal || !summaryEl) return;
22021
+ 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.`;
22022
+ overlay.hidden = !open;
22023
+ modal.hidden = !open;
22024
+ if (open) {
22025
+ legacyUpgradeNoticePreviousFocus = document.activeElement;
22026
+ document.addEventListener("keydown", handleLegacyUpgradeModalKeydown);
22027
+ focusLegacyUpgradeModal();
22028
+ setLegacyUpgradeBackgroundInert(true);
22029
+ return;
22030
+ }
22031
+ setLegacyUpgradeBackgroundInert(false);
22032
+ document.removeEventListener("keydown", handleLegacyUpgradeModalKeydown);
22033
+ if (legacyUpgradeNoticePreviousFocus && document.contains(legacyUpgradeNoticePreviousFocus)) legacyUpgradeNoticePreviousFocus.focus();
22034
+ legacyUpgradeNoticePreviousFocus = null;
22035
+ }
22036
+ function maybeShowLegacyUpgradeNotice(summary) {
22037
+ if (!summary || legacyUpgradeNoticeShown || isLegacyUpgradeNoticeDismissed()) return;
22038
+ legacyUpgradeNoticeShown = true;
22039
+ setLegacyUpgradeNotice(true, summary);
22040
+ }
22041
+ async function checkLegacyUpgradeNotice() {
22042
+ if (legacyUpgradeNoticeShown || isLegacyUpgradeNoticeDismissed()) return;
22043
+ try {
22044
+ maybeShowLegacyUpgradeNotice(readLegacyUpgradeReviewSummary(await loadSyncStatus(false, "", { includeJoinRequests: false })));
22045
+ } catch {}
22046
+ }
20793
22047
  function setReconnectOverlay(open, detail) {
20794
22048
  const overlay = $("viewerReconnectOverlay");
20795
22049
  const detailEl = $("viewerReconnectDetail");
@@ -20980,10 +22234,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
20980
22234
  state.lastCoordinatorAdminStatus = null;
20981
22235
  }));
20982
22236
  if (refreshTab === "feed") promises.push(loadFeedData());
22237
+ if (refreshTab === "projects") promises.push(loadProjectsData());
20983
22238
  if (refreshTab === "sync" || refreshTab === "health") promises.push(loadSyncData());
20984
22239
  if (refreshTab === "coordinator-admin") promises.push(loadCoordinatorAdminData());
20985
22240
  if (state.syncPairingOpen) promises.push(loadPairingData());
20986
22241
  await Promise.all(promises);
22242
+ maybeShowLegacyUpgradeNotice(readLegacyUpgradeReviewSummary(state.lastSyncLegacySharedReview));
20987
22243
  const nextTab = resolveAccessibleTab(state.activeTab, state.lastCoordinatorAdminStatus);
20988
22244
  if (nextTab !== state.activeTab) setActiveTab(nextTab);
20989
22245
  renderTabs(state.activeTab);
@@ -21006,6 +22262,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
21006
22262
  setTheme(getTheme());
21007
22263
  initTabs();
21008
22264
  initFeedTab();
22265
+ initProjectsTab(() => refresh());
21009
22266
  initSyncTab(() => refresh());
21010
22267
  initCoordinatorAdminTab();
21011
22268
  initSettings(stopPolling, startPolling, () => refresh());
@@ -21028,7 +22285,23 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
21028
22285
  } catch {}
21029
22286
  if (!reconnecting) setReconnectOverlay(false);
21030
22287
  });
22288
+ $("legacyUpgradeReviewGroups")?.addEventListener("click", () => {
22289
+ dismissLegacyUpgradeNoticeIfRequested();
22290
+ setLegacyUpgradeNotice(false);
22291
+ window.location.hash = "sync";
22292
+ setTimeout(() => document.getElementById("syncSharingReview")?.scrollIntoView({ block: "start" }), 120);
22293
+ });
22294
+ $("legacyUpgradeReviewProjects")?.addEventListener("click", () => {
22295
+ dismissLegacyUpgradeNoticeIfRequested();
22296
+ setLegacyUpgradeNotice(false);
22297
+ window.location.hash = "projects";
22298
+ });
22299
+ $("legacyUpgradeNotNow")?.addEventListener("click", () => {
22300
+ dismissLegacyUpgradeNoticeIfRequested();
22301
+ setLegacyUpgradeNotice(false);
22302
+ });
21031
22303
  loadRuntimeLabel();
22304
+ checkLegacyUpgradeNotice();
21032
22305
  refresh();
21033
22306
  startPolling();
21034
22307
  //#endregion