@ai-matrx/associations 0.6.2 → 0.7.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.
@@ -1109,6 +1109,12 @@ var useEntityRelationships = useAssociations;
1109
1109
 
1110
1110
  // src/react/hooks/useContainerLinks.ts
1111
1111
  var import_react3 = require("react");
1112
+ function metadataRecord(metadata) {
1113
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
1114
+ return metadata == null ? {} : null;
1115
+ }
1116
+ return Object.fromEntries(Object.entries(metadata));
1117
+ }
1112
1118
  function useContainerLinks(args) {
1113
1119
  const { containerType, containerId, orgId } = args;
1114
1120
  const store = useAssociationsStore();
@@ -1230,6 +1236,31 @@ function useContainerLinks(args) {
1230
1236
  }
1231
1237
  return result;
1232
1238
  };
1239
+ const setPinned = async (token, resourceId, pinned, role) => {
1240
+ if (!containerId) return { ok: false, error: "Missing container id" };
1241
+ const exactRole = role ?? null;
1242
+ const link = incoming.find(
1243
+ (candidate) => candidate.token === token && candidate.resourceId === resourceId && candidate.role === exactRole
1244
+ );
1245
+ if (!link) return { ok: false, error: "Association edge not found" };
1246
+ const currentMetadata = metadataRecord(link.metadata);
1247
+ if (!currentMetadata) {
1248
+ return {
1249
+ ok: false,
1250
+ error: "Association metadata is not an object and cannot be updated safely"
1251
+ };
1252
+ }
1253
+ return store.add({
1254
+ sourceType: token,
1255
+ sourceId: resourceId,
1256
+ targetType: containerType,
1257
+ targetId: containerId,
1258
+ ...orgId != null ? { orgId } : {},
1259
+ ...link.label != null ? { label: link.label } : {},
1260
+ ...role !== void 0 ? { role } : {},
1261
+ metadata: { ...currentMetadata, pinned }
1262
+ });
1263
+ };
1233
1264
  const reload = async () => {
1234
1265
  await Promise.all([
1235
1266
  reloadGeneric(),
@@ -1247,7 +1278,8 @@ function useContainerLinks(args) {
1247
1278
  attachedIdsFor,
1248
1279
  linksFor,
1249
1280
  attach,
1250
- detach
1281
+ detach,
1282
+ setPinned
1251
1283
  };
1252
1284
  }
1253
1285
 
@@ -1414,7 +1446,7 @@ function useUniversalEntitySearch(args) {
1414
1446
  const timer = setTimeout(async () => {
1415
1447
  if (runRef.current !== run) return;
1416
1448
  setLoading(true);
1417
- const activeTokens = tokensKey ? tokensKey.split(",") : store.registry.curatedTokens();
1449
+ const activeTokens = tokensKey ? tokensKey.split(",") : store.registry.listableTokens();
1418
1450
  let next = [];
1419
1451
  if (trimmed || emptyQueryMode === "candidates") {
1420
1452
  next = await store.candidates.searchCandidatesAcrossTokens({
@@ -2452,7 +2484,7 @@ function UniversalAssociationPicker(props) {
2452
2484
  const store = useAssociationsStore();
2453
2485
  const { pickerOverrides } = useAssociationsUiPorts();
2454
2486
  const notifier = useNotifier();
2455
- const tokens = props.tokens ?? store.registry.curatedTokens();
2487
+ const tokens = props.tokens ?? store.registry.listableTokens();
2456
2488
  const [query, setQuery] = (0, import_react14.useState)("");
2457
2489
  const [browseToken, setBrowseToken] = (0, import_react14.useState)(null);
2458
2490
  const [busyKey, setBusyKey] = (0, import_react14.useState)(null);
@@ -2628,6 +2660,9 @@ function UniversalAssociationPicker(props) {
2628
2660
  var import_react15 = require("react");
2629
2661
  var import_design_system5 = require("@ai-matrx/design-system");
2630
2662
  var import_jsx_runtime9 = require("react/jsx-runtime");
2663
+ function metadataIsPinned(metadata) {
2664
+ return metadata !== null && typeof metadata === "object" && !Array.isArray(metadata) && Reflect.get(metadata, "pinned") === true;
2665
+ }
2631
2666
  function AttachedItemsSheet(props) {
2632
2667
  const store = useAssociationsStore();
2633
2668
  const info = store.registry.getEntityInfo(props.token);
@@ -2663,7 +2698,8 @@ function AttachedItemsSheet(props) {
2663
2698
  enabled: props.open,
2664
2699
  links: props.links,
2665
2700
  ...props.onAdd ? { onAdd: props.onAdd } : {},
2666
- onDetach: props.onDetach
2701
+ onDetach: props.onDetach,
2702
+ ...props.onSetPinned ? { onSetPinned: props.onSetPinned } : {}
2667
2703
  }
2668
2704
  )
2669
2705
  }
@@ -2674,7 +2710,8 @@ function AttachedItemsBody({
2674
2710
  enabled,
2675
2711
  links,
2676
2712
  onAdd,
2677
- onDetach
2713
+ onDetach,
2714
+ onSetPinned
2678
2715
  }) {
2679
2716
  const store = useAssociationsStore();
2680
2717
  const notifier = useNotifier();
@@ -2716,11 +2753,12 @@ function AttachedItemsBody({
2716
2753
  cancelled = true;
2717
2754
  };
2718
2755
  }, [token, idKey, enabled, store]);
2719
- const detach = async (resourceId) => {
2756
+ const detach = async (link) => {
2757
+ const resourceId = link.resourceId;
2720
2758
  if (busyId) return;
2721
2759
  setBusyId(resourceId);
2722
2760
  try {
2723
- const res = await onDetach(resourceId);
2761
+ const res = await onDetach(resourceId, link.role);
2724
2762
  if (!res.ok) {
2725
2763
  notifier.error(`Couldn't detach${res.error ? `: ${res.error}` : ""}`);
2726
2764
  }
@@ -2728,6 +2766,20 @@ function AttachedItemsBody({
2728
2766
  setBusyId(null);
2729
2767
  }
2730
2768
  };
2769
+ const setPinned = async (link, pinned) => {
2770
+ if (!onSetPinned || busyId) return;
2771
+ setBusyId(link.resourceId);
2772
+ try {
2773
+ const res = await onSetPinned(link.resourceId, pinned, link.role);
2774
+ if (!res.ok) {
2775
+ notifier.error(
2776
+ `Couldn't ${pinned ? "pin" : "unpin"}${res.error ? `: ${res.error}` : ""}`
2777
+ );
2778
+ }
2779
+ } finally {
2780
+ setBusyId(null);
2781
+ }
2782
+ };
2731
2783
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
2732
2784
  titleError ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "mb-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[12px] text-destructive", children: [
2733
2785
  "Showing names recorded at attach time \u2014 they may be out of date.",
@@ -2742,6 +2794,7 @@ function AttachedItemsBody({
2742
2794
  const missing = !titles.has(link.resourceId);
2743
2795
  const name = resolved ?? link.label ?? (missing ? null : "Untitled");
2744
2796
  const busy = busyId === link.resourceId;
2797
+ const pinned = metadataIsPinned(link.metadata);
2745
2798
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2746
2799
  "li",
2747
2800
  {
@@ -2761,12 +2814,35 @@ function AttachedItemsBody({
2761
2814
  className: "text-foreground"
2762
2815
  }
2763
2816
  ) }),
2817
+ onSetPinned ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2818
+ "button",
2819
+ {
2820
+ type: "button",
2821
+ disabled: busy,
2822
+ onClick: () => void setPinned(link, !pinned),
2823
+ title: pinned ? "Unpin (stops staying in agent context)" : "Pin \u2014 always in agent context",
2824
+ "aria-label": pinned ? "Unpin" : "Pin",
2825
+ className: (0, import_design_system5.cn)(
2826
+ "flex h-6 w-6 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground disabled:opacity-50",
2827
+ pinned ? "text-primary" : "text-muted-foreground"
2828
+ ),
2829
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2830
+ PinIcon,
2831
+ {
2832
+ className: (0, import_design_system5.cn)(
2833
+ "h-3.5 w-3.5",
2834
+ pinned && "fill-primary/20"
2835
+ )
2836
+ }
2837
+ )
2838
+ }
2839
+ ) : null,
2764
2840
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2765
2841
  "button",
2766
2842
  {
2767
2843
  type: "button",
2768
2844
  disabled: busy,
2769
- onClick: () => void detach(link.resourceId),
2845
+ onClick: () => void detach(link),
2770
2846
  title: "Detach",
2771
2847
  className: (0, import_design_system5.cn)(
2772
2848
  "flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors",
@@ -2862,7 +2938,15 @@ function AssociationCard({
2862
2938
  const role = getContentRoleMeta(info.contentRole);
2863
2939
  const Icon = info.Icon ?? DefaultEntityIcon;
2864
2940
  const [listOpen, setListOpen] = (0, import_react16.useState)(false);
2865
- const { status, countFor, attachedIdsFor, linksFor, attach, detach } = useContainerLinks({
2941
+ const {
2942
+ status,
2943
+ countFor,
2944
+ attachedIdsFor,
2945
+ linksFor,
2946
+ attach,
2947
+ detach,
2948
+ setPinned
2949
+ } = useContainerLinks({
2866
2950
  containerType: container?.type ?? "organization",
2867
2951
  containerId: container?.id ?? null,
2868
2952
  orgId: container?.orgId ?? null
@@ -2959,7 +3043,8 @@ function AssociationCard({
2959
3043
  setOpen(true);
2960
3044
  }
2961
3045
  } : {},
2962
- onDetach: (resourceId) => detach(token, resourceId)
3046
+ onDetach: (resourceId, role2) => detach(token, resourceId, role2 ?? void 0),
3047
+ onSetPinned: (resourceId, pinned, role2) => setPinned(token, resourceId, pinned, role2 ?? void 0)
2963
3048
  }
2964
3049
  ),
2965
3050
  canAttach && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
@@ -3018,6 +3103,9 @@ function AssociationCardGrid({
3018
3103
  var import_react17 = require("react");
3019
3104
  var import_design_system8 = require("@ai-matrx/design-system");
3020
3105
  var import_jsx_runtime12 = require("react/jsx-runtime");
3106
+ function metadataIsPinned2(metadata) {
3107
+ return metadata !== null && typeof metadata === "object" && !Array.isArray(metadata) && Reflect.get(metadata, "pinned") === true;
3108
+ }
3021
3109
  function useContainerLinksAdapter(container, tokens) {
3022
3110
  const store = useAssociationsStore();
3023
3111
  const links = useContainerLinks({
@@ -3032,6 +3120,7 @@ function useContainerLinksAdapter(container, tokens) {
3032
3120
  resourceId: l.resourceId,
3033
3121
  label: l.label,
3034
3122
  role: l.role,
3123
+ pinned: metadataIsPinned2(l.metadata),
3035
3124
  removable: true
3036
3125
  }))
3037
3126
  ) : [];
@@ -3041,11 +3130,13 @@ function useContainerLinksAdapter(container, tokens) {
3041
3130
  reload: links.reload,
3042
3131
  rows,
3043
3132
  attach: async (token, id, title) => links.attach(token, id, title),
3044
- detach: async (token, id, role) => links.detach(token, id, role)
3133
+ detach: async (token, id, role) => links.detach(token, id, role),
3134
+ setPinned: async (token, id, pinned, role) => links.setPinned(token, id, pinned, role)
3045
3135
  };
3046
3136
  }
3047
3137
  function AssociationList(props) {
3048
3138
  const store = useAssociationsStore();
3139
+ const notifier = useNotifier();
3049
3140
  const ctxEntity = usePrimaryEntity();
3050
3141
  const container = props.container ?? ctxEntity ?? null;
3051
3142
  const defaultAdapter = useContainerLinksAdapter(
@@ -3058,6 +3149,7 @@ function AssociationList(props) {
3058
3149
  const [pickerToken, setPickerToken] = (0, import_react17.useState)(null);
3059
3150
  const [showUniversal, setShowUniversal] = (0, import_react17.useState)(false);
3060
3151
  const [removingKeys, setRemovingKeys] = (0, import_react17.useState)(/* @__PURE__ */ new Set());
3152
+ const [pinningKeys, setPinningKeys] = (0, import_react17.useState)(/* @__PURE__ */ new Set());
3061
3153
  const rows = tokenFilter ? adapter.rows.filter((r) => tokenFilter.includes(r.token)) : adapter.rows;
3062
3154
  const visibleRows = rows.filter((r) => !removingKeys.has(r.key));
3063
3155
  const { titleFor } = useEntityTitles(
@@ -3081,6 +3173,31 @@ function AssociationList(props) {
3081
3173
  });
3082
3174
  return res;
3083
3175
  };
3176
+ const handlePin = async (row, pinned) => {
3177
+ if (!adapter.setPinned) return;
3178
+ const info = store.registry.tryGetEntityInfo(row.token);
3179
+ if (!info) return;
3180
+ setPinningKeys((prev) => new Set(prev).add(row.key));
3181
+ try {
3182
+ const res = await adapter.setPinned(
3183
+ info.token,
3184
+ row.resourceId,
3185
+ pinned,
3186
+ row.role ?? void 0
3187
+ );
3188
+ if (!res.ok) {
3189
+ notifier.error(
3190
+ `Couldn't ${pinned ? "pin" : "unpin"} resource${res.error ? `: ${res.error}` : ""}`
3191
+ );
3192
+ }
3193
+ } finally {
3194
+ setPinningKeys((prev) => {
3195
+ const next = new Set(prev);
3196
+ next.delete(row.key);
3197
+ return next;
3198
+ });
3199
+ }
3200
+ };
3084
3201
  const openRow = (row) => {
3085
3202
  if (props.openEntity) {
3086
3203
  props.openEntity(row.token, row.resourceId);
@@ -3094,7 +3211,7 @@ function AssociationList(props) {
3094
3211
  };
3095
3212
  const grouped = groupRows(store, visibleRows);
3096
3213
  const isLoading = adapter.status === "loading" || adapter.status === "idle";
3097
- const attachableTokens = tokenFilter ?? store.registry.curatedTokens();
3214
+ const attachableTokens = tokenFilter ?? store.registry.listableTokens();
3098
3215
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: (0, import_design_system8.cn)("flex flex-col gap-2 text-foreground", props.className), children: [
3099
3216
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center justify-between gap-2 px-1", children: [
3100
3217
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("h3", { className: "flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: [
@@ -3271,18 +3388,15 @@ function AssociationList(props) {
3271
3388
  "button",
3272
3389
  {
3273
3390
  type: "button",
3274
- onClick: () => void adapter.setPinned?.(
3275
- info.token,
3276
- row.resourceId,
3277
- !(row.pinned ?? false)
3278
- ),
3391
+ disabled: pinningKeys.has(row.key),
3392
+ onClick: () => void handlePin(row, !(row.pinned ?? false)),
3279
3393
  title: row.pinned ? "Unpin (stops staying in agent context)" : "Pin \u2014 always in agent context",
3280
3394
  "aria-label": row.pinned ? "Unpin" : "Pin",
3281
3395
  className: (0, import_design_system8.cn)(
3282
3396
  "shrink-0 rounded p-0.5 transition-colors hover:!text-foreground",
3283
3397
  row.pinned ? "text-primary" : "text-muted-foreground/0 group-hover:text-muted-foreground"
3284
3398
  ),
3285
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3399
+ children: pinningKeys.has(row.key) ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SpinnerIcon, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3286
3400
  PinIcon,
3287
3401
  {
3288
3402
  className: (0, import_design_system8.cn)(