Skip to content

Commit 45e62ac

Browse files
authored
Extract backlinks-index recipe CT-970 (commontoolsinc#1857)
* Extract `backlinks-index` recipe * All wired up and almost working * Use one index for all mentionables [CT-969] * Compute global mentionable list and use it * Format pass
1 parent 20383d2 commit 45e62ac

File tree

5 files changed

+193
-50
lines changed

5 files changed

+193
-50
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/// <cts-enable />
2+
import {
3+
Cell,
4+
cell,
5+
Default,
6+
derive,
7+
lift,
8+
NAME,
9+
OpaqueRef,
10+
recipe,
11+
UI,
12+
} from "commontools";
13+
14+
export type MentionableCharm = {
15+
[NAME]: string;
16+
content?: string;
17+
mentioned?: MentionableCharm[];
18+
};
19+
20+
type Input = {
21+
allCharms: MentionableCharm[];
22+
};
23+
24+
export type BacklinksMap = { [charmId: string]: MentionableCharm[] };
25+
26+
type Output = {
27+
backlinks: BacklinksMap;
28+
mentionable: MentionableCharm[];
29+
};
30+
31+
/**
32+
* BacklinksIndex builds a map of backlinks across all charms and exposes a
33+
* unified mentionable list for consumers like editors.
34+
*
35+
* Behavior:
36+
* - Backlinks are computed by scanning each charm's `mentioned` list and
37+
* mapping mention target -> list of source charms.
38+
* - Mentionable list is a union of:
39+
* - every charm in `allCharms`
40+
* - any items a charm exports via a `mentionable` property
41+
* (either an array of charms or a Cell of such an array)
42+
*
43+
* The backlinks map is keyed by a charm's `content` value (falling back to
44+
* its `[NAME]`). This mirrors how existing note patterns identify notes when
45+
* computing backlinks locally.
46+
*/
47+
const BacklinksIndex = recipe<Input, Output>(
48+
"BacklinksIndex",
49+
({ allCharms }) => {
50+
const computeIndex = lift<
51+
{ allCharms: MentionableCharm[] },
52+
BacklinksMap
53+
>(
54+
({ allCharms }) => {
55+
const cs = allCharms ?? [];
56+
const index: BacklinksMap = {};
57+
58+
for (const c of cs) {
59+
const mentions = c.mentioned ?? [];
60+
for (const m of mentions) {
61+
const key = m?.content || m?.[NAME];
62+
if (!key) continue;
63+
if (!index[key]) index[key] = [];
64+
index[key].push(c);
65+
}
66+
}
67+
68+
return index;
69+
},
70+
);
71+
72+
const backlinks: OpaqueRef<BacklinksMap> = computeIndex({ allCharms });
73+
74+
// Compute mentionable list from allCharms via lift, then mirror that into
75+
// a real Cell for downstream consumers that expect a Cell.
76+
const computeMentionable = lift<
77+
{ allCharms: any[] },
78+
MentionableCharm[]
79+
>(({ allCharms }) => {
80+
const cs = allCharms ?? [];
81+
const out: MentionableCharm[] = [];
82+
for (const c of cs) {
83+
out.push(c);
84+
const exported = (c as unknown as {
85+
mentionable?: MentionableCharm[] | { get?: () => MentionableCharm[] };
86+
}).mentionable;
87+
if (Array.isArray(exported)) {
88+
for (const m of exported) if (m) out.push(m);
89+
} else if (exported && typeof (exported as any).get === "function") {
90+
const arr = (exported as { get: () => MentionableCharm[] }).get() ??
91+
[];
92+
for (const m of arr) if (m) out.push(m);
93+
}
94+
}
95+
return out;
96+
});
97+
98+
return {
99+
[NAME]: "BacklinksIndex",
100+
[UI]: undefined,
101+
backlinks,
102+
mentionable: computeMentionable({ allCharms }),
103+
};
104+
},
105+
);
106+
107+
export default BacklinksIndex;

packages/patterns/chatbot-list-view.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from "commontools";
1919

2020
import Chat from "./chatbot-note-composed.tsx";
21+
import BacklinksIndex, { BacklinksMap } from "./backlinks-index.tsx";
2122
import { ListItem } from "./common-tools.tsx";
2223

2324
export type MentionableCharm = {
@@ -36,6 +37,7 @@ type Input = {
3637
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
3738
charmsList: Default<CharmEntry[], []>;
3839
allCharms: Cell<any[]>;
40+
index: BacklinksMap;
3941
theme?: {
4042
accentColor: Default<string, "#3b82f6">;
4143
fontFace: Default<string, "system-ui, -apple-system, sans-serif">;
@@ -45,6 +47,10 @@ type Input = {
4547

4648
type Output = {
4749
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
50+
// Expose a mentionable list aggregated from local chat entries
51+
// Returned as an opaque ref to an array (not a Cell), suitable for
52+
// upstream aggregators that read exported mentionables.
53+
mentionable?: MentionableCharm[];
4854
};
4955

5056
const removeChat = handler<
@@ -89,7 +95,7 @@ const storeCharm = lift(
8995
charm: any;
9096
selectedCharm: Cell<Default<{ charm: any }, { charm: undefined }>>;
9197
charmsList: Cell<CharmEntry[]>;
92-
allCharms: Cell<any[]>;
98+
allCharms: Cell<MentionableCharm[]>;
9399
theme?: {
94100
accentColor: Default<string, "#3b82f6">;
95101
fontFace: Default<string, "system-ui, -apple-system, sans-serif">;
@@ -124,10 +130,11 @@ const populateChatList = lift(
124130
charmsList: CharmEntry[];
125131
allCharms: Cell<any[]>;
126132
selectedCharm: Cell<{ charm: any }>;
133+
index: any;
127134
}>(),
128135
undefined,
129136
(
130-
{ charmsList, allCharms, selectedCharm },
137+
{ charmsList, allCharms, selectedCharm, index },
131138
) => {
132139
if (charmsList.length === 0) {
133140
const isInitialized = cell(false);
@@ -137,6 +144,7 @@ const populateChatList = lift(
137144
messages: [],
138145
content: "",
139146
allCharms,
147+
index,
140148
}),
141149
selectedCharm,
142150
charmsList,
@@ -155,16 +163,18 @@ const createChatRecipe = handler<
155163
selectedCharm: Cell<{ charm: any }>;
156164
charmsList: Cell<CharmEntry[]>;
157165
allCharms: Cell<any[]>;
166+
index: any;
158167
}
159168
>(
160-
(_, { selectedCharm, charmsList, allCharms }) => {
169+
(_, { selectedCharm, charmsList, allCharms, index }) => {
161170
const isInitialized = cell(false);
162171

163172
const charm = Chat({
164173
title: "New Chat",
165174
messages: [],
166175
content: "",
167176
allCharms,
177+
index,
168178
});
169179
// store the charm ref in a cell (pass isInitialized to prevent recursive calls)
170180
return storeCharm({
@@ -234,24 +244,40 @@ const getCharmName = lift(({ charm }: { charm: any }) => {
234244
// create the named cell inside the recipe body, so we do it just once
235245
export default recipe<Input, Output>(
236246
"Launcher",
237-
({ selectedCharm, charmsList, allCharms, theme }) => {
247+
({ selectedCharm, charmsList, allCharms, index, theme }) => {
238248
logCharmsList({ charmsList: charmsList as unknown as Cell<CharmEntry[]> });
239249

250+
const combined = combineLists({
251+
allCharms: allCharms as unknown as MentionableCharm[],
252+
charmsList,
253+
});
254+
240255
populateChatList({
241256
selectedCharm: selectedCharm as unknown as Cell<
242257
Pick<CharmEntry, "charm">
243258
>,
244259
charmsList,
245260
allCharms,
246-
});
247-
248-
const combined = combineLists({
249-
allCharms: allCharms as unknown as any[],
250-
charmsList,
261+
index,
251262
});
252263

253264
const selected = getSelectedCharm({ entry: selectedCharm });
254265

266+
// Aggregate mentionables from the local charms list so that this
267+
// container exposes its child chat/note charms as mention targets.
268+
const localMentionable = lift<
269+
{ list: CharmEntry[] },
270+
MentionableCharm[]
271+
>(({ list }) => {
272+
const out: MentionableCharm[] = [];
273+
for (const entry of list) {
274+
const c = entry.charm;
275+
out.push(c.note);
276+
out.push(c.chat);
277+
}
278+
return out;
279+
})({ list: charmsList });
280+
255281
const localTheme = theme ?? {
256282
accentColor: cell("#3b82f6"),
257283
fontFace: cell("system-ui, -apple-system, sans-serif"),
@@ -272,6 +298,7 @@ export default recipe<Input, Output>(
272298
selectedCharm,
273299
charmsList,
274300
allCharms: combined as unknown as any,
301+
index,
275302
})}
276303
>
277304
Create New Chat
@@ -289,6 +316,7 @@ export default recipe<Input, Output>(
289316
selectedCharm,
290317
charmsList,
291318
allCharms: combined as unknown as any,
319+
index,
292320
})}
293321
/>
294322
</div>
@@ -459,6 +487,8 @@ export default recipe<Input, Output>(
459487
),
460488
selectedCharm,
461489
charmsList,
490+
// Expose the aggregated mentionables for parent-level indexing.
491+
mentionable: localMentionable,
462492
};
463493
},
464494
);

packages/patterns/chatbot-note-composed.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727

2828
import Chat from "./chatbot.tsx";
2929
import Note from "./note.tsx";
30+
import { type BacklinksMap } from "./backlinks-index.tsx";
3031
import Tools, {
3132
addListItem,
3233
calculator,
@@ -69,6 +70,7 @@ type ChatbotNoteInput = {
6970
messages: Default<Array<BuiltInLLMMessage>, []>;
7071
content: Default<string, "">;
7172
allCharms: Cell<MentionableCharm[]>;
73+
index: { backlinks: BacklinksMap; mentionable: Cell<MentionableCharm[]> };
7274
};
7375

7476
type ChatbotNoteResult = {
@@ -79,6 +81,8 @@ type ChatbotNoteResult = {
7981
note: any;
8082
chat: any;
8183
list: Default<ListItem[], []>;
84+
// Optional: expose sub-charms as mentionable targets
85+
mentionable?: MentionableCharm[];
8286
};
8387

8488
const newNote = handler<
@@ -89,14 +93,14 @@ const newNote = handler<
8993
/** A cell to store the result message indicating success or error */
9094
result: Cell<string>;
9195
},
92-
{ allCharms: Cell<MentionableCharm[]> }
96+
{ allCharms: Cell<MentionableCharm[]>; index: any }
9397
>(
9498
(args, state) => {
9599
try {
96100
const n = Note({
97101
title: args.title,
98102
content: args.content || "",
99-
allCharms: state.allCharms,
103+
index: state.index,
100104
});
101105

102106
args.result.set(
@@ -241,7 +245,7 @@ const navigateToNote = handler<
241245

242246
export default recipe<ChatbotNoteInput, ChatbotNoteResult>(
243247
"Chatbot + Note",
244-
({ title, messages, content, allCharms }) => {
248+
({ title, messages, content, allCharms, index }) => {
245249
const list = cell<ListItem[]>([]);
246250

247251
const tools = {
@@ -299,12 +303,13 @@ export default recipe<ChatbotNoteInput, ChatbotNoteResult>(
299303
description: "Create a new note instance",
300304
handler: newNote({
301305
allCharms: allCharms as unknown as OpaqueRef<MentionableCharm[]>,
306+
index: index as unknown as OpaqueRef<any>,
302307
}),
303308
},
304309
};
305310

306311
const chat = Chat({ messages, tools, mentionable: allCharms });
307-
const note = Note({ title, content, allCharms });
312+
const note = Note({ title, content, index });
308313

309314
return {
310315
[NAME]: title,
@@ -315,6 +320,11 @@ export default recipe<ChatbotNoteInput, ChatbotNoteResult>(
315320
mentioned: note.mentioned,
316321
backlinks: note.backlinks,
317322
list,
323+
// Expose both child charms for mention systems that scan charm exports.
324+
mentionable: [
325+
chat as unknown as MentionableCharm,
326+
note as unknown as MentionableCharm,
327+
],
318328
};
319329
},
320330
);

0 commit comments

Comments
 (0)