Andrew commited on
Commit
ddc185d
·
1 Parent(s): 5523d0b

feat(utils): add tree branching logic for finding message paths

Browse files
Files changed (1) hide show
  1. src/lib/utils/tree/branching.ts +176 -0
src/lib/utils/tree/branching.ts ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from "$lib/types/Message";
2
+
3
+ export interface BranchState {
4
+ messageId: string;
5
+ personaId: string;
6
+ personaName: string;
7
+ }
8
+
9
+ /**
10
+ * Creates a linear path of messages from the tree, starting from the root
11
+ * and following the path to the target message (msgId).
12
+ */
13
+ export function createMessagesPath<
14
+ T extends { id: string; ancestors?: string[]; children?: string[] },
15
+ >(messages: T[], msgId?: string): T[] {
16
+ const msg = messages.find((msg) => msg.id === msgId) ?? messages.at(-1);
17
+ if (!msg) return [];
18
+
19
+ const path: T[] = [];
20
+
21
+ // Ancestor path
22
+ if (msg.ancestors?.length) {
23
+ for (const ancestorId of msg.ancestors) {
24
+ const ancestor = messages.find((m) => m.id === ancestorId);
25
+ if (ancestor) {
26
+ path.push(ancestor);
27
+ }
28
+ }
29
+ }
30
+
31
+ // The node itself
32
+ path.push(msg);
33
+
34
+ // Children path (follow last child)
35
+ let childrenIds = msg.children;
36
+ while (childrenIds?.length) {
37
+ const lastChildId = childrenIds.at(-1);
38
+ const lastChild = messages.find((m) => m.id === lastChildId);
39
+ if (lastChild) {
40
+ path.push(lastChild);
41
+ childrenIds = lastChild.children;
42
+ } else {
43
+ break;
44
+ }
45
+ }
46
+
47
+ return path;
48
+ }
49
+
50
+ /**
51
+ * Identifies alternative branches for message alternatives toggle.
52
+ * Filters out branches that are explicit multi-persona branches (handled via branch icons).
53
+ */
54
+ export function createMessagesAlternatives(messages: Message[]): string[][] {
55
+ const alternatives: string[][] = [];
56
+
57
+ for (const message of messages) {
58
+ if (message.children?.length) {
59
+ const branchGroupCounts = new Map<string, number>();
60
+
61
+ // Precompute how many children share the same branchedFrom metadata
62
+ for (const childId of message.children) {
63
+ const child = messages.find((m) => m.id === childId);
64
+ if (child?.branchedFrom) {
65
+ const key = `${child.branchedFrom.messageId}:${child.branchedFrom.personaId}`;
66
+ branchGroupCounts.set(key, (branchGroupCounts.get(key) ?? 0) + 1);
67
+ }
68
+ }
69
+
70
+ // Filter out children that are branches (have branchedFrom metadata)
71
+ // Branches should only be navigated via branch icons, not alternatives toggle
72
+ const nonBranchChildren = message.children.filter((childId) => {
73
+ const child = messages.find((m) => m.id === childId);
74
+ if (!child?.branchedFrom) {
75
+ return true;
76
+ }
77
+
78
+ const branchKey = `${child.branchedFrom.messageId}:${child.branchedFrom.personaId}`;
79
+ const sharedCount = branchGroupCounts.get(branchKey) ?? 0;
80
+
81
+ // If multiple messages share the same branch origin (e.g. retries), show them as alternatives
82
+ if (sharedCount > 1) {
83
+ return true;
84
+ }
85
+
86
+ return false;
87
+ });
88
+
89
+ // Only add to alternatives if there are multiple non-branch children
90
+ if (nonBranchChildren.length > 1) {
91
+ alternatives.push(nonBranchChildren);
92
+ }
93
+ }
94
+ }
95
+ return alternatives;
96
+ }
97
+
98
+ /**
99
+ * Detects the current active branch based on the message history path.
100
+ */
101
+ export function detectCurrentBranch(
102
+ msgs: Message[],
103
+ branchState?: BranchState | null,
104
+ personas?: { id: string; name: string }[]
105
+ ): BranchState | null {
106
+ if (branchState) {
107
+ return branchState;
108
+ }
109
+
110
+ // Look at the last message to see if we're in a branch
111
+ const lastMsg = msgs[msgs.length - 1];
112
+ if (lastMsg?.branchedFrom) {
113
+ // We're in a branch - find the persona name
114
+ const persona = personas?.find((p) => p.id === lastMsg.branchedFrom?.personaId);
115
+ return {
116
+ messageId: lastMsg.branchedFrom.messageId,
117
+ personaId: lastMsg.branchedFrom.personaId,
118
+ personaName: persona?.name || lastMsg.branchedFrom.personaId,
119
+ };
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Filters the full list of messages to only show those relevant to the current branch context.
127
+ * If no branch is active, returns all messages.
128
+ */
129
+ export function filterMessagesByBranch(
130
+ msgs: Message[],
131
+ currentBranch: BranchState | null
132
+ ): Message[] {
133
+ if (!currentBranch) {
134
+ // No active branch - show all messages
135
+ return msgs;
136
+ }
137
+
138
+ // Find all messages that are part of this branch (have matching branchedFrom)
139
+ const branchMessages = msgs.filter(
140
+ (m) =>
141
+ m.branchedFrom?.messageId === currentBranch.messageId &&
142
+ m.branchedFrom?.personaId === currentBranch.personaId
143
+ );
144
+
145
+ if (branchMessages.length === 0) {
146
+ // New branch - no messages yet
147
+ // Show all messages up to and including the branch point
148
+
149
+ // Find the branch point message
150
+ const branchPoint = msgs.find((m) => m.id === currentBranch.messageId);
151
+
152
+ if (!branchPoint) {
153
+ console.warn("⚠️ Branch point not found - showing all messages");
154
+ return msgs;
155
+ }
156
+
157
+ // Get ancestors of branch point (messages before it)
158
+ const ancestorIds = branchPoint.ancestors || [];
159
+ const ancestors = msgs.filter((m) => ancestorIds.includes(m.id));
160
+
161
+ // Show ancestors + branch point
162
+ const filtered = [...ancestors, branchPoint];
163
+ return filtered;
164
+ }
165
+
166
+ // Existing branch - get ancestor IDs from the first branch message
167
+ // These include ALL messages in the history up to the branch point (including nested branches)
168
+ const firstBranchMsg = branchMessages[0];
169
+ const ancestorIds = firstBranchMsg.ancestors || [];
170
+
171
+ // Filter to get actual ancestor messages
172
+ const ancestors = msgs.filter((m) => ancestorIds.includes(m.id));
173
+
174
+ // Combine ancestors + branch messages for the complete view
175
+ return [...ancestors, ...branchMessages];
176
+ }