@ai-matrx/associations 0.10.7 → 0.11.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.
@@ -46,6 +46,7 @@ __export(react_exports, {
46
46
  UnresolvedRef: () => UnresolvedRef,
47
47
  attachedKey: () => attachedKey,
48
48
  buildCommentTree: () => buildCommentTree,
49
+ containerLinksArgs: () => containerLinksArgs,
49
50
  getContentRoleMeta: () => getContentRoleMeta,
50
51
  lazyPickerOverride: () => lazyPickerOverride,
51
52
  lazyWindowShell: () => lazyWindowShell,
@@ -1254,6 +1255,77 @@ var useEntityRelationships = useAssociations;
1254
1255
 
1255
1256
  // src/react/hooks/useContainerLinks.ts
1256
1257
  var import_react3 = require("react");
1258
+
1259
+ // src/edges.ts
1260
+ var ASSOCIATION_TARGET_TYPES = [
1261
+ "organization",
1262
+ // an org — the top-level container resources attach into
1263
+ "scope",
1264
+ "scope_type",
1265
+ "project",
1266
+ "task",
1267
+ "context_item",
1268
+ "thread",
1269
+ "war_room",
1270
+ "category",
1271
+ "conversation",
1272
+ // a chat conversation a working_document is attached to
1273
+ "fc_set",
1274
+ // a flashcard set (card→set membership)
1275
+ "fc_card",
1276
+ // a flashcard (card→card hierarchy, quiz→card)
1277
+ "study_media",
1278
+ // a study-media artifact (summary/mind_map) — converter lineage target
1279
+ "assessment",
1280
+ // a quiz / practice test — a converter lineage target + origin (assessment→artifact 'source')
1281
+ "note",
1282
+ // a note — the origin a converted study artifact links back to (artifact→note 'source' lineage, P4)
1283
+ "file",
1284
+ // a file (card→file media + source lineage)
1285
+ "quiz_session",
1286
+ // a quiz a card is used in
1287
+ "agent",
1288
+ // an orchestrator agent — the container an Orchestra's member agents attach into (role 'member'); an 'orchestra' self-edge marks the orchestrator
1289
+ "agent_term_list",
1290
+ // a term list — durable file attachments target its vocabulary resource
1291
+ "research_tag",
1292
+ // a research tag a source is filed under (rs_source_tag M2M collapsed into associations)
1293
+ "skill",
1294
+ // a skill — projects link to it (skill→project, role 'member'); code_files attach as resources (code_file→skill, role 'resource')
1295
+ "surface",
1296
+ // a UI surface — agents bind here (agent→surface); context menu reads via agent.menu_surface
1297
+ "plan_entity",
1298
+ // a Content Planning source/media citation — plan_node role edges (about/cites/embeds) target it
1299
+ "plan_node",
1300
+ // a planned page — research topics/tags attach here and follow realization into web_page
1301
+ "web_site",
1302
+ // a web.site — parties attach onto a site's roster (party→web_site role 'writes_for')
1303
+ "seo_topic",
1304
+ // a taxonomy topic — plan_node/plan_entity tag into it (role 'topic')
1305
+ "seo_keyword",
1306
+ // a keyword — plan_node attaches secondaries (role 'secondary_keyword'; the primary is an FK, never an edge)
1307
+ "web_page",
1308
+ // a canonical page — the page workspace attaches notes/tasks/files/keywords onto it (keyword edges: role 'primary'|'supporting')
1309
+ "web_screenshot",
1310
+ // a page capture — per-image attachments on the page workspace
1311
+ "party",
1312
+ // a CRM person/company (crm.party) — the record page attaches tasks/files/notes onto it
1313
+ "crm_outreach_list",
1314
+ // a CRM outreach list (a worked send/dial queue) — parties and resources attach into it
1315
+ "crm_deal",
1316
+ // a CRM deal (crm.deal) — people on it (role 'deal_contact'), tasks/files attach onto it
1317
+ "rulebook",
1318
+ // a Masterwork Rulebook — its Scout interviews attach here (conversation→rulebook, role 'interview')
1319
+ "research_topic",
1320
+ // a research topic — Sources are filed under it (processed_document→research_topic, the landing door's edge)
1321
+ "media_source_library"
1322
+ // a media-catalog Library — Sources are catalogued into it (processed_document→media_source_library)
1323
+ ];
1324
+ function isAssociationTargetType(value) {
1325
+ return typeof value === "string" && ASSOCIATION_TARGET_TYPES.includes(value);
1326
+ }
1327
+
1328
+ // src/react/hooks/useContainerLinks.ts
1257
1329
  function metadataRecord(metadata) {
1258
1330
  if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
1259
1331
  return metadata == null ? {} : null;
@@ -1262,6 +1334,7 @@ function metadataRecord(metadata) {
1262
1334
  }
1263
1335
  function useContainerLinks(args) {
1264
1336
  const { containerType, containerId, orgId } = args;
1337
+ const outgoing = args.direction === "outgoing";
1265
1338
  const store = useAssociationsStore();
1266
1339
  const {
1267
1340
  edges,
@@ -1272,7 +1345,7 @@ function useContainerLinks(args) {
1272
1345
  type: containerType,
1273
1346
  id: containerId
1274
1347
  });
1275
- const conversationKey = containerType === "conversation" && containerId ? `conversation:${containerId}` : null;
1348
+ const conversationKey = !outgoing && containerType === "conversation" && containerId ? `conversation:${containerId}` : null;
1276
1349
  const activeConversationKey = (0, import_react3.useRef)(conversationKey);
1277
1350
  const conversationGeneration = (0, import_react3.useRef)(0);
1278
1351
  const [conversationResult, setConversationResult] = (0, import_react3.useState)(null);
@@ -1328,9 +1401,9 @@ function useContainerLinks(args) {
1328
1401
  }, [loadConversationFiles]);
1329
1402
  const conversationFiles = conversationResult?.key === conversationKey ? conversationResult.files : [];
1330
1403
  const conversationFileError = conversationResult?.key === conversationKey ? conversationResult.error : null;
1331
- const conversationFileStatus = containerType !== "conversation" ? "idle" : conversationResult?.key !== conversationKey ? "loading" : conversationResult.loading ? "loading" : conversationFileError ? "error" : "ready";
1404
+ const conversationFileStatus = outgoing || containerType !== "conversation" ? "idle" : conversationResult?.key !== conversationKey ? "loading" : conversationResult.loading ? "loading" : conversationFileError ? "error" : "ready";
1332
1405
  const incomingGeneric = edges.filter(
1333
- (edge) => edge.direction === "incoming" && !(containerType === "conversation" && edge.otherType === "file")
1406
+ (edge) => outgoing ? edge.direction === "outgoing" : edge.direction === "incoming" && !(containerType === "conversation" && edge.otherType === "file")
1334
1407
  ).map((edge) => ({
1335
1408
  edgeId: edge.id,
1336
1409
  resourceId: edge.otherId,
@@ -1339,7 +1412,29 @@ function useContainerLinks(args) {
1339
1412
  label: edge.label ?? null,
1340
1413
  metadata: edge.metadata ?? {}
1341
1414
  }));
1342
- const incoming = containerType === "conversation" ? [...incomingGeneric, ...conversationFiles] : incomingGeneric;
1415
+ const incoming = !outgoing && containerType === "conversation" ? [...incomingGeneric, ...conversationFiles] : incomingGeneric;
1416
+ const endpoints = (token, resourceId) => {
1417
+ if (!containerId) return { error: "Missing container id" };
1418
+ if (outgoing) {
1419
+ if (!isAssociationTargetType(token)) {
1420
+ return {
1421
+ error: `A ${token.replace(/_/g, " ")} cannot be filed under: it is not a container type.`
1422
+ };
1423
+ }
1424
+ return {
1425
+ sourceType: containerType,
1426
+ sourceId: containerId,
1427
+ targetType: token,
1428
+ targetId: resourceId
1429
+ };
1430
+ }
1431
+ return {
1432
+ sourceType: token,
1433
+ sourceId: resourceId,
1434
+ targetType: containerType,
1435
+ targetId: containerId
1436
+ };
1437
+ };
1343
1438
  const linksFor = (token) => incoming.filter((link) => link.token === token);
1344
1439
  const countFor = (token) => incoming.reduce((n, link) => link.token === token ? n + 1 : n, 0);
1345
1440
  const attachedIdsFor = (token) => {
@@ -1350,39 +1445,36 @@ function useContainerLinks(args) {
1350
1445
  return set;
1351
1446
  };
1352
1447
  const attach = async (token, resourceId, label, metadata, options) => {
1353
- if (!containerId) return { ok: false, error: "Missing container id" };
1448
+ const ends = endpoints(token, resourceId);
1449
+ if ("error" in ends) return { ok: false, error: ends.error };
1354
1450
  const result = await store.add({
1355
- sourceType: token,
1356
- sourceId: resourceId,
1357
- targetType: containerType,
1358
- targetId: containerId,
1451
+ ...ends,
1359
1452
  ...orgId != null ? { orgId } : {},
1360
1453
  ...label !== void 0 ? { label } : {},
1361
1454
  ...metadata !== void 0 ? { metadata } : {},
1362
1455
  ...options?.role !== void 0 ? { role: options.role } : {},
1363
1456
  ...options?.replaceMetadata !== void 0 ? { replaceMetadata: options.replaceMetadata } : {}
1364
1457
  });
1365
- if (result.ok && containerType === "conversation" && token === "file") {
1458
+ if (result.ok && !outgoing && containerType === "conversation" && token === "file") {
1366
1459
  await loadConversationFiles();
1367
1460
  }
1368
1461
  return result;
1369
1462
  };
1370
1463
  const detach = async (token, resourceId, role) => {
1371
- if (!containerId) return { ok: false, error: "Missing container id" };
1464
+ const ends = endpoints(token, resourceId);
1465
+ if ("error" in ends) return { ok: false, error: ends.error };
1372
1466
  const result = await store.remove({
1373
- sourceType: token,
1374
- sourceId: resourceId,
1375
- targetType: containerType,
1376
- targetId: containerId,
1467
+ ...ends,
1377
1468
  ...role !== void 0 ? { role } : {}
1378
1469
  });
1379
- if (result.ok && containerType === "conversation" && token === "file") {
1470
+ if (result.ok && !outgoing && containerType === "conversation" && token === "file") {
1380
1471
  await loadConversationFiles();
1381
1472
  }
1382
1473
  return result;
1383
1474
  };
1384
1475
  const setPinned = async (token, resourceId, pinned, role) => {
1385
- if (!containerId) return { ok: false, error: "Missing container id" };
1476
+ const ends = endpoints(token, resourceId);
1477
+ if ("error" in ends) return { ok: false, error: ends.error };
1386
1478
  const exactRole = role ?? null;
1387
1479
  const link = incoming.find(
1388
1480
  (candidate) => candidate.token === token && candidate.resourceId === resourceId && candidate.role === exactRole
@@ -1396,10 +1488,7 @@ function useContainerLinks(args) {
1396
1488
  };
1397
1489
  }
1398
1490
  return store.add({
1399
- sourceType: token,
1400
- sourceId: resourceId,
1401
- targetType: containerType,
1402
- targetId: containerId,
1491
+ ...ends,
1403
1492
  ...orgId != null ? { orgId } : {},
1404
1493
  ...link.label != null ? { label: link.label } : {},
1405
1494
  ...role !== void 0 ? { role } : {},
@@ -1409,16 +1498,17 @@ function useContainerLinks(args) {
1409
1498
  const reload = async () => {
1410
1499
  await Promise.all([
1411
1500
  reloadGeneric(),
1412
- containerType === "conversation" ? loadConversationFiles() : Promise.resolve()
1501
+ !outgoing && containerType === "conversation" ? loadConversationFiles() : Promise.resolve()
1413
1502
  ]);
1414
1503
  };
1415
- const status = !containerId ? genericStatus : conversationFileStatus === "error" ? "error" : containerType === "conversation" && (conversationFileStatus === "idle" || conversationFileStatus === "loading") ? "loading" : genericStatus;
1504
+ const status = !containerId ? genericStatus : conversationFileStatus === "error" ? "error" : !outgoing && containerType === "conversation" && (conversationFileStatus === "idle" || conversationFileStatus === "loading") ? "loading" : genericStatus;
1416
1505
  const error = conversationFileError ?? genericError;
1417
1506
  return {
1418
1507
  status,
1419
1508
  error,
1420
1509
  reload,
1421
1510
  totalCount: incoming.length,
1511
+ presentTokens: [...new Set(incoming.map((link) => link.token))],
1422
1512
  countFor,
1423
1513
  attachedIdsFor,
1424
1514
  linksFor,
@@ -1427,6 +1517,21 @@ function useContainerLinks(args) {
1427
1517
  setPinned
1428
1518
  };
1429
1519
  }
1520
+ function containerLinksArgs(container) {
1521
+ if (container?.direction === "outgoing") {
1522
+ return {
1523
+ containerType: container.type,
1524
+ containerId: container.id,
1525
+ orgId: container.orgId ?? null,
1526
+ direction: "outgoing"
1527
+ };
1528
+ }
1529
+ return {
1530
+ containerType: container?.type ?? "organization",
1531
+ containerId: container?.id ?? null,
1532
+ orgId: container?.orgId ?? null
1533
+ };
1534
+ }
1430
1535
 
1431
1536
  // src/react/hooks/useEntityTitles.ts
1432
1537
  var import_react4 = require("react");
@@ -2261,7 +2366,7 @@ function AssociationPicker(props) {
2261
2366
  const Override = pickerOverrides?.[props.token];
2262
2367
  if (Override) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Override, { ...props });
2263
2368
  const title = `Add ${info.labelPlural}`;
2264
- const subtitle = props.containerLabel ? `Attach to ${props.containerLabel}` : "Click an item to attach or detach it";
2369
+ const subtitle = props.filing ? `File ${props.containerLabel ?? "this"} under a ${info.label.toLowerCase()}` : props.containerLabel ? `Attach to ${props.containerLabel}` : "Click an item to attach or detach it";
2265
2370
  if (!props.open) return null;
2266
2371
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2267
2372
  AssociationWindow,
@@ -2878,8 +2983,9 @@ function AttachedItemsSheet(props) {
2878
2983
  const Icon = info.Icon ?? DefaultEntityIcon;
2879
2984
  const title = info.labelPlural;
2880
2985
  const containerName = props.container?.label ?? props.containerLabel;
2986
+ const filing = props.container?.direction === "outgoing";
2881
2987
  const subtitle = props.container ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "inline-flex min-w-0 max-w-full items-center gap-1", children: [
2882
- "Attached to",
2988
+ filing ? "What this is filed under:" : "Attached to",
2883
2989
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2884
2990
  DoorRef,
2885
2991
  {
@@ -3155,11 +3261,7 @@ function AssociationCard({
3155
3261
  attach,
3156
3262
  detach,
3157
3263
  setPinned
3158
- } = useContainerLinks({
3159
- containerType: container?.type ?? "organization",
3160
- containerId: container?.id ?? null,
3161
- orgId: container?.orgId ?? null
3162
- });
3264
+ } = useContainerLinks(containerLinksArgs(container));
3163
3265
  if (!container) {
3164
3266
  store.errorSink({
3165
3267
  code: "association_card_no_container",
@@ -3170,7 +3272,8 @@ function AssociationCard({
3170
3272
  }
3171
3273
  const count = countFor(token);
3172
3274
  const loading = status === "loading" || status === "idle";
3173
- const canAttach = info.canListCandidates;
3275
+ const filing = container.direction === "outgoing";
3276
+ const canAttach = info.canListCandidates && (!filing || isAssociationTargetType(token));
3174
3277
  const canDrillIn = count > 0;
3175
3278
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3176
3279
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
@@ -3218,7 +3321,7 @@ function AssociationCard({
3218
3321
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "block text-[11px] text-muted-foreground tabular-nums", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
3219
3322
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SpinnerIcon, { className: "h-3 w-3 animate-spin" }),
3220
3323
  "Loading\u2026"
3221
- ] }) : `${count} attached` })
3324
+ ] }) : `${count} ${container.direction === "outgoing" ? "filed under" : "attached"}` })
3222
3325
  ] })
3223
3326
  ]
3224
3327
  }
@@ -3264,6 +3367,7 @@ function AssociationCard({
3264
3367
  token,
3265
3368
  ...container.label !== void 0 ? { containerLabel: container.label } : {},
3266
3369
  orgId: container.orgId ?? null,
3370
+ ...filing ? { filing: true } : {},
3267
3371
  attachedIds: attachedIdsFor(token),
3268
3372
  onAttach: (resourceId, title) => attach(token, resourceId, title),
3269
3373
  onDetach: (resourceId) => detach(token, resourceId)
@@ -3281,31 +3385,55 @@ function AssociationCardGrid({
3281
3385
  className
3282
3386
  }) {
3283
3387
  const store = useAssociationsStore();
3388
+ const primary = usePrimaryEntity();
3284
3389
  const excluded = new Set(excludeTokens ?? []);
3285
- const list = (tokens ?? store.registry.curatedTokens()).filter(
3286
- (token) => !excluded.has(token)
3390
+ const filing = primary?.direction === "outgoing";
3391
+ const links = useContainerLinks(containerLinksArgs(primary));
3392
+ const base2 = tokens ?? store.registry.curatedTokens();
3393
+ const withPresent = filing ? [
3394
+ ...base2,
3395
+ ...links.presentTokens.filter(
3396
+ (t) => isEntityTypeToken(t) && !base2.includes(t)
3397
+ )
3398
+ ] : base2;
3399
+ const list = withPresent.filter(
3400
+ (token) => !excluded.has(token) && (!filing || isAssociationTargetType(token))
3287
3401
  );
3288
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: (0, import_design_system7.cn)("space-y-5", className), children: CONTENT_ROLES.map((role) => {
3289
- const inRole = list.filter(
3290
- (token) => store.registry.getEntityInfo(token).contentRole === role.id
3291
- );
3292
- if (inRole.length === 0) return null;
3293
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
3294
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "mb-2.5 flex items-baseline gap-3 pl-1.5", children: [
3295
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center gap-2", children: [
3296
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3297
- "span",
3298
- {
3299
- className: (0, import_design_system7.cn)("h-2.5 w-2.5 rounded-full", role.accentBar)
3300
- }
3301
- ),
3302
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-sm font-semibold text-foreground", children: role.title })
3402
+ const roleIds = new Set(CONTENT_ROLES.map((r) => r.id));
3403
+ const roleless = filing ? list.filter((token) => {
3404
+ const role = store.registry.getEntityInfo(token).contentRole;
3405
+ return !role || !roleIds.has(role);
3406
+ }) : [];
3407
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: (0, import_design_system7.cn)("space-y-5", className), children: [
3408
+ CONTENT_ROLES.map((role) => {
3409
+ const inRole = list.filter(
3410
+ (token) => store.registry.getEntityInfo(token).contentRole === role.id
3411
+ );
3412
+ if (inRole.length === 0) return null;
3413
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
3414
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "mb-2.5 flex items-baseline gap-3 pl-1.5", children: [
3415
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center gap-2", children: [
3416
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3417
+ "span",
3418
+ {
3419
+ className: (0, import_design_system7.cn)("h-2.5 w-2.5 rounded-full", role.accentBar)
3420
+ }
3421
+ ),
3422
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-sm font-semibold text-foreground", children: role.title })
3423
+ ] }),
3424
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "hidden text-xs text-muted-foreground sm:block", children: role.tagline })
3303
3425
  ] }),
3304
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "hidden text-xs text-muted-foreground sm:block", children: role.tagline })
3426
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3", children: inRole.map((token) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AssociationCard, { token }, token)) })
3427
+ ] }, role.id);
3428
+ }),
3429
+ roleless.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
3430
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "mb-2.5 flex items-baseline gap-3 pl-1.5", children: [
3431
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-sm font-semibold text-foreground", children: "Filed under" }),
3432
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "hidden text-xs text-muted-foreground sm:block", children: "Libraries, topics and other places this is kept." })
3305
3433
  ] }),
3306
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3", children: inRole.map((token) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AssociationCard, { token }, token)) })
3307
- ] }, role.id);
3308
- }) });
3434
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3", children: roleless.map((token) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AssociationCard, { token }, token)) })
3435
+ ] })
3436
+ ] });
3309
3437
  }
3310
3438
 
3311
3439
  // src/react/components/AssociationList.tsx
@@ -3317,11 +3445,7 @@ function metadataIsPinned2(metadata) {
3317
3445
  }
3318
3446
  function useContainerLinksAdapter(container, tokens) {
3319
3447
  const store = useAssociationsStore();
3320
- const links = useContainerLinks({
3321
- containerType: container?.type ?? "organization",
3322
- containerId: container?.id ?? null,
3323
- orgId: container?.orgId ?? null
3324
- });
3448
+ const links = useContainerLinks(containerLinksArgs(container));
3325
3449
  const rows = container ? (tokens ?? store.registry.curatedTokens()).flatMap(
3326
3450
  (token) => links.linksFor(token).map((l) => ({
3327
3451
  key: l.edgeId,