@schlessera/brain-ui-react 0.16.0 → 0.17.0

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.
@@ -0,0 +1,140 @@
1
+ // Standalone text-diff engine behind the Edit tool's diff view: line-level
2
+ // LCS with word-level refinement for single-line change pairs. Pure data —
3
+ // no React; rendering stays in components/chat/tool-views.tsx.
4
+
5
+ // ------------------------------------------------------------
6
+ // Diff helpers — line-level LCS with word-level refinement
7
+ // ------------------------------------------------------------
8
+
9
+ type DiffOp<T> = { kind: "same" | "del" | "ins"; value: T };
10
+
11
+ /**
12
+ * Hand-rolled LCS diff (standard DP + forward walk) so we avoid pulling in the
13
+ * `diff` package. Returns ops in source order: `same` where the sequences
14
+ * agree, `del` for items only in `a`, `ins` for items only in `b`.
15
+ */
16
+ function diffSeq<T>(
17
+ a: T[],
18
+ b: T[],
19
+ eq: (x: T, y: T) => boolean = (x, y) => x === y
20
+ ): DiffOp<T>[] {
21
+ const m = a.length;
22
+ const n = b.length;
23
+ // dp[i][j] = length of the LCS of a[i:] and b[j:]
24
+ const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
25
+ for (let i = m - 1; i >= 0; i--) {
26
+ for (let j = n - 1; j >= 0; j--) {
27
+ dp[i][j] = eq(a[i], b[j])
28
+ ? dp[i + 1][j + 1] + 1
29
+ : Math.max(dp[i + 1][j], dp[i][j + 1]);
30
+ }
31
+ }
32
+ const ops: DiffOp<T>[] = [];
33
+ let i = 0;
34
+ let j = 0;
35
+ while (i < m && j < n) {
36
+ if (eq(a[i], b[j])) {
37
+ ops.push({ kind: "same", value: a[i] });
38
+ i++;
39
+ j++;
40
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
41
+ ops.push({ kind: "del", value: a[i] });
42
+ i++;
43
+ } else {
44
+ ops.push({ kind: "ins", value: b[j] });
45
+ j++;
46
+ }
47
+ }
48
+ while (i < m) ops.push({ kind: "del", value: a[i++] });
49
+ while (j < n) ops.push({ kind: "ins", value: b[j++] });
50
+ return ops;
51
+ }
52
+
53
+ /** A word token, flagged when it differs from its counterpart line. */
54
+ export type WordToken = { text: string; changed: boolean };
55
+
56
+ /**
57
+ * Token-level LCS for a single deleted/inserted line pair. Splitting on
58
+ * `/(\s+)/` keeps the whitespace as its own tokens so we can reassemble the
59
+ * line exactly. Unchanged tokens are shared between both sides.
60
+ */
61
+ function wordDiff(oldLine: string, newLine: string): { del: WordToken[]; ins: WordToken[] } {
62
+ const a = oldLine.split(/(\s+)/);
63
+ const b = newLine.split(/(\s+)/);
64
+ // Token-level LCS is O(words²); a pathological minified line would be slow.
65
+ // Above the cap, skip word refinement and flag the whole line as changed.
66
+ if (a.length * b.length > 10_000) {
67
+ return {
68
+ del: [{ text: oldLine, changed: true }],
69
+ ins: [{ text: newLine, changed: true }],
70
+ };
71
+ }
72
+ const ops = diffSeq(a, b);
73
+ const del: WordToken[] = [];
74
+ const ins: WordToken[] = [];
75
+ for (const op of ops) {
76
+ if (op.kind === "same") {
77
+ del.push({ text: op.value, changed: false });
78
+ ins.push({ text: op.value, changed: false });
79
+ } else if (op.kind === "del") {
80
+ del.push({ text: op.value, changed: true });
81
+ } else {
82
+ ins.push({ text: op.value, changed: true });
83
+ }
84
+ }
85
+ return { del, ins };
86
+ }
87
+
88
+ /** A rendered diff row. `tokens` is set only for word-refined single-line edits. */
89
+ export type DiffRow = {
90
+ kind: "same" | "del" | "ins";
91
+ line: string;
92
+ tokens: WordToken[] | null;
93
+ };
94
+
95
+ /**
96
+ * Merge `old_string`/`new_string` into a single diff: unchanged lines appear
97
+ * once as neutral, deletions/insertions keep the red/green treatment. A change
98
+ * block that is exactly one deleted line against one inserted line gets
99
+ * word-level highlighting.
100
+ */
101
+ export function computeDiffRows(oldStr: string, newStr: string): DiffRow[] {
102
+ const oldLines = oldStr.split("\n");
103
+ const newLines = newStr.split("\n");
104
+ // Line-level LCS is O(m×n) in time and memory; a huge Edit (thousands of
105
+ // lines) would build a massive DP matrix and hang the UI. Above the cap, fall
106
+ // back to a naive "all old removed, all new added" diff.
107
+ if (oldLines.length * newLines.length > 250_000) {
108
+ return [
109
+ ...oldLines.map((line): DiffRow => ({ kind: "del", line, tokens: null })),
110
+ ...newLines.map((line): DiffRow => ({ kind: "ins", line, tokens: null })),
111
+ ];
112
+ }
113
+ const ops = diffSeq(oldLines, newLines);
114
+ const rows: DiffRow[] = [];
115
+ let idx = 0;
116
+ while (idx < ops.length) {
117
+ if (ops[idx].kind === "same") {
118
+ rows.push({ kind: "same", line: ops[idx].value, tokens: null });
119
+ idx++;
120
+ continue;
121
+ }
122
+ // Collect a maximal run of changes, grouping deletes before inserts.
123
+ const dels: string[] = [];
124
+ const inss: string[] = [];
125
+ while (idx < ops.length && ops[idx].kind !== "same") {
126
+ if (ops[idx].kind === "del") dels.push(ops[idx].value);
127
+ else inss.push(ops[idx].value);
128
+ idx++;
129
+ }
130
+ if (dels.length === 1 && inss.length === 1) {
131
+ const { del, ins } = wordDiff(dels[0], inss[0]);
132
+ rows.push({ kind: "del", line: dels[0], tokens: del });
133
+ rows.push({ kind: "ins", line: inss[0], tokens: ins });
134
+ } else {
135
+ for (const d of dels) rows.push({ kind: "del", line: d, tokens: null });
136
+ for (const s of inss) rows.push({ kind: "ins", line: s, tokens: null });
137
+ }
138
+ }
139
+ return rows;
140
+ }
@@ -456,7 +456,7 @@ export const useChatStore = create<ChatState>((set, get) => {
456
456
  ),
457
457
  })),
458
458
 
459
- requestToolApproval: (key, toolUseId, toolName, input, description) =>
459
+ requestToolApproval: (key, toolUseId, toolName, input, _description) =>
460
460
  mutateLastAssistant(key, (last) => {
461
461
  // Check if tool call already exists (from streaming)
462
462
  const existingIdx = last.toolCalls.findIndex(
@@ -204,7 +204,20 @@ function sceneRequest(state: GraphState): { endpoint: string; query: string } |
204
204
  }
205
205
 
206
206
  export const useGraphStore = create<GraphState>((set, get) => ({
207
- mode: "local",
207
+ /**
208
+ * Clusters, not Local.
209
+ *
210
+ * Local is centred on ONE node and has no center until the user picks one,
211
+ * so opening the graph landed on an empty canvas with a picker — which reads
212
+ * as "the graph is broken", not as "choose a starting point". Clusters is
213
+ * the only mode that answers the question someone opening a graph view is
214
+ * actually asking: what is in here, and what clumps together.
215
+ *
216
+ * Local stays one click away, and clicking any node still switches to it
217
+ * (node-popover.tsx) — that is the natural way in, rather than the landing
218
+ * state.
219
+ */
220
+ mode: "clusters",
208
221
  meta: null,
209
222
  metaState: "idle",
210
223