@andespindola/brainlink 0.1.0-beta.9 → 0.1.0-beta.90
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.
- package/AGENTS.md +8 -5
- package/CHANGELOG.md +26 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +146 -17
- package/SECURITY.md +1 -1
- package/dist/application/analyze-vault.js +7 -7
- package/dist/application/build-context.js +56 -1
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +154 -102
- package/dist/application/frontend/client-html.js +49 -40
- package/dist/application/frontend/client-js.js +3130 -166
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +18 -6
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +252 -19
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +25 -10
- package/dist/application/server/routes.js +102 -1
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +20 -3
- package/dist/cli/commands/write-commands.js +818 -8
- package/dist/domain/context.js +53 -11
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +67 -16
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +38 -0
- package/dist/infrastructure/file-index.js +358 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +56 -0
- package/dist/infrastructure/private-pack-codec.js +134 -0
- package/dist/infrastructure/search-packs.js +452 -0
- package/dist/infrastructure/session-state.js +57 -2
- package/dist/mcp/server.js +11 -1
- package/dist/mcp/tools.js +215 -3
- package/docs/AGENT_USAGE.md +103 -16
- package/docs/ARCHITECTURE.md +25 -26
- package/docs/QUICKSTART.md +9 -1
- package/package.json +6 -4
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -120
- package/dist/infrastructure/sqlite/schema.js +0 -111
- package/dist/infrastructure/sqlite/search-reader.js +0 -156
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -25
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createEmbeddingBuckets, createLocalEmbedding, cosineSimilarity } from '../domain/embeddings.js';
|
|
3
|
+
import { parseMarkdownDocument } from '../domain/markdown.js';
|
|
4
|
+
import { writeMarkdownFile, ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
|
|
5
|
+
import { indexVault } from './index-vault.js';
|
|
6
|
+
const tokenPattern = /[\p{L}\p{N}_-]+/gu;
|
|
7
|
+
const frontmatterPattern = /^---\n[\s\S]*?\n---\n?/m;
|
|
8
|
+
const rootHeadingPattern = /^#\s+.+\n+/m;
|
|
9
|
+
const maxCandidatesPerBucket = 240;
|
|
10
|
+
const normalizePath = (path) => path.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
11
|
+
const toComparableBody = (content) => content
|
|
12
|
+
.replace(frontmatterPattern, '')
|
|
13
|
+
.replace(rootHeadingPattern, '')
|
|
14
|
+
.replaceAll('\r\n', '\n')
|
|
15
|
+
.trim();
|
|
16
|
+
const normalizeStrictContent = (content) => toComparableBody(content);
|
|
17
|
+
const normalizeSemanticContent = (content) => toComparableBody(content)
|
|
18
|
+
.replace(/\s+/g, ' ')
|
|
19
|
+
.trim();
|
|
20
|
+
const toHash = (value) => createHash('sha256').update(value, 'utf8').digest('hex');
|
|
21
|
+
const toCandidateId = (leftPath, rightPath) => [normalizePath(leftPath), normalizePath(rightPath)].sort((left, right) => left.localeCompare(right)).join('|');
|
|
22
|
+
const hasSharedTokens = (left, right) => {
|
|
23
|
+
const leftTokens = new Set((left.match(tokenPattern) ?? []).map((token) => token.toLowerCase()).filter((token) => token.length > 2));
|
|
24
|
+
const rightTokens = new Set((right.match(tokenPattern) ?? []).map((token) => token.toLowerCase()).filter((token) => token.length > 2));
|
|
25
|
+
if (leftTokens.size === 0 || rightTokens.size === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
for (const token of leftTokens) {
|
|
29
|
+
if (rightTokens.has(token)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
const relatedMarker = (targetTitle) => `Related: [[${targetTitle}]] priority: low #related-to`;
|
|
36
|
+
const ensureRelatedEdgeLine = (content, targetTitle) => {
|
|
37
|
+
const linkPattern = new RegExp(`\\[\\[\\s*${targetTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*(?:[\\]|#])?`, 'i');
|
|
38
|
+
if (linkPattern.test(content)) {
|
|
39
|
+
return content;
|
|
40
|
+
}
|
|
41
|
+
const trimmed = content.trimEnd();
|
|
42
|
+
return `${trimmed}\n\n${relatedMarker(targetTitle)}\n`;
|
|
43
|
+
};
|
|
44
|
+
const ensureMergedMarker = (content, targetTitle) => {
|
|
45
|
+
const marker = `Merged into [[${targetTitle}]]`;
|
|
46
|
+
if (content.includes(marker)) {
|
|
47
|
+
return content;
|
|
48
|
+
}
|
|
49
|
+
return `${content.trimEnd()}\n\n${marker} priority: low #related-to\n`;
|
|
50
|
+
};
|
|
51
|
+
const appendMergedContent = (baseContent, mergedTitle, mergedContent) => {
|
|
52
|
+
const marker = `## Merged Memory From [[${mergedTitle}]]`;
|
|
53
|
+
if (baseContent.includes(marker)) {
|
|
54
|
+
return baseContent;
|
|
55
|
+
}
|
|
56
|
+
const mergedBody = normalizeSemanticContent(mergedContent);
|
|
57
|
+
return `${baseContent.trimEnd()}\n\n${marker}\n\n${mergedBody}\n`;
|
|
58
|
+
};
|
|
59
|
+
const loadNoteRecords = async (vaultPath, agentId) => {
|
|
60
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
61
|
+
const files = await readMarkdownFiles(vaultPath);
|
|
62
|
+
return files
|
|
63
|
+
.map((file) => {
|
|
64
|
+
const parsed = parseMarkdownDocument({
|
|
65
|
+
absolutePath: file.absolutePath,
|
|
66
|
+
vaultPath: absoluteVaultPath,
|
|
67
|
+
content: file.content,
|
|
68
|
+
createdAt: file.createdAt,
|
|
69
|
+
updatedAt: file.updatedAt
|
|
70
|
+
});
|
|
71
|
+
const strict = normalizeStrictContent(parsed.content);
|
|
72
|
+
const semantic = normalizeSemanticContent(parsed.content);
|
|
73
|
+
const embedding = createLocalEmbedding(`${parsed.title}\n${semantic}`);
|
|
74
|
+
return {
|
|
75
|
+
title: parsed.title,
|
|
76
|
+
path: normalizePath(parsed.path),
|
|
77
|
+
agentId: parsed.agentId,
|
|
78
|
+
content: parsed.content,
|
|
79
|
+
normalizedStrictContent: strict,
|
|
80
|
+
semanticContent: semantic,
|
|
81
|
+
embedding,
|
|
82
|
+
buckets: createEmbeddingBuckets(embedding, 20)
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
.filter((record) => (agentId ? record.agentId === agentId : true));
|
|
86
|
+
};
|
|
87
|
+
const pairToCandidate = (left, right, kind, score, reason) => ({
|
|
88
|
+
id: toCandidateId(left.path, right.path),
|
|
89
|
+
possibleDuplicate: true,
|
|
90
|
+
kind,
|
|
91
|
+
score: Number(score.toFixed(4)),
|
|
92
|
+
left: {
|
|
93
|
+
title: left.title,
|
|
94
|
+
path: left.path,
|
|
95
|
+
agentId: left.agentId
|
|
96
|
+
},
|
|
97
|
+
right: {
|
|
98
|
+
title: right.title,
|
|
99
|
+
path: right.path,
|
|
100
|
+
agentId: right.agentId
|
|
101
|
+
},
|
|
102
|
+
reason
|
|
103
|
+
});
|
|
104
|
+
const indexCandidatePairs = (notes) => {
|
|
105
|
+
const bucketMap = new Map();
|
|
106
|
+
notes.forEach((note, index) => {
|
|
107
|
+
note.buckets.forEach((bucket) => {
|
|
108
|
+
const current = bucketMap.get(bucket) ?? [];
|
|
109
|
+
if (current.length < maxCandidatesPerBucket) {
|
|
110
|
+
current.push(index);
|
|
111
|
+
bucketMap.set(bucket, current);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const pairKeys = new Set();
|
|
116
|
+
const pairs = [];
|
|
117
|
+
bucketMap.forEach((indexes) => {
|
|
118
|
+
for (let leftIndex = 0; leftIndex < indexes.length; leftIndex += 1) {
|
|
119
|
+
for (let rightIndex = leftIndex + 1; rightIndex < indexes.length; rightIndex += 1) {
|
|
120
|
+
const left = Math.min(indexes[leftIndex] ?? 0, indexes[rightIndex] ?? 0);
|
|
121
|
+
const right = Math.max(indexes[leftIndex] ?? 0, indexes[rightIndex] ?? 0);
|
|
122
|
+
const key = `${left}|${right}`;
|
|
123
|
+
if (!pairKeys.has(key)) {
|
|
124
|
+
pairKeys.add(key);
|
|
125
|
+
pairs.push([left, right]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return pairs;
|
|
131
|
+
};
|
|
132
|
+
export const scanDuplicateNotes = async (vaultPath, options = {}) => {
|
|
133
|
+
const notes = await loadNoteRecords(vaultPath, options.agentId);
|
|
134
|
+
if (notes.length < 2) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const minSemanticScore = options.minSemanticScore ?? 0.92;
|
|
138
|
+
const includeSemantic = options.includeSemantic !== false;
|
|
139
|
+
const seen = new Map();
|
|
140
|
+
const byHash = notes.reduce((state, note) => {
|
|
141
|
+
const key = toHash(note.normalizedStrictContent);
|
|
142
|
+
const current = state.get(key) ?? [];
|
|
143
|
+
current.push(note);
|
|
144
|
+
state.set(key, current);
|
|
145
|
+
return state;
|
|
146
|
+
}, new Map());
|
|
147
|
+
byHash.forEach((group) => {
|
|
148
|
+
if (group.length < 2) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const [base, ...rest] = group.sort((left, right) => left.path.localeCompare(right.path));
|
|
152
|
+
rest.forEach((note) => {
|
|
153
|
+
const candidate = pairToCandidate(base, note, 'exact', 1, 'Exact content hash match');
|
|
154
|
+
seen.set(candidate.id, candidate);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
if (includeSemantic) {
|
|
158
|
+
const pairs = indexCandidatePairs(notes);
|
|
159
|
+
pairs.forEach(([leftIndex, rightIndex]) => {
|
|
160
|
+
const left = notes[leftIndex];
|
|
161
|
+
const right = notes[rightIndex];
|
|
162
|
+
if (!left || !right || left.path === right.path) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const id = toCandidateId(left.path, right.path);
|
|
166
|
+
if (seen.has(id)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const score = cosineSimilarity(left.embedding, right.embedding);
|
|
170
|
+
const titleShared = hasSharedTokens(left.title, right.title);
|
|
171
|
+
const contentShared = hasSharedTokens(left.semanticContent, right.semanticContent);
|
|
172
|
+
if (score >= minSemanticScore && (titleShared || contentShared || score >= 0.975)) {
|
|
173
|
+
const candidate = pairToCandidate(left, right, 'semantic', score, 'High semantic similarity');
|
|
174
|
+
seen.set(id, candidate);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const focusPath = options.focusPath ? normalizePath(options.focusPath) : undefined;
|
|
179
|
+
const limited = Array.from(seen.values())
|
|
180
|
+
.filter((item) => (focusPath ? item.left.path === focusPath || item.right.path === focusPath : true))
|
|
181
|
+
.sort((left, right) => right.score - left.score || left.left.path.localeCompare(right.left.path))
|
|
182
|
+
.slice(0, Math.max(1, options.limit ?? 25));
|
|
183
|
+
return limited;
|
|
184
|
+
};
|
|
185
|
+
export const resolveDuplicateNotes = async (vaultPath, options) => {
|
|
186
|
+
const leftPath = normalizePath(options.leftPath);
|
|
187
|
+
const rightPath = normalizePath(options.rightPath);
|
|
188
|
+
if (leftPath === rightPath) {
|
|
189
|
+
throw new Error('leftPath and rightPath must be different notes.');
|
|
190
|
+
}
|
|
191
|
+
const notes = await loadNoteRecords(vaultPath);
|
|
192
|
+
const byPath = new Map(notes.map((note) => [note.path, note]));
|
|
193
|
+
const left = byPath.get(leftPath);
|
|
194
|
+
const right = byPath.get(rightPath);
|
|
195
|
+
if (!left || !right) {
|
|
196
|
+
throw new Error(`Duplicate resolution paths were not found in vault index source: ${leftPath}, ${rightPath}`);
|
|
197
|
+
}
|
|
198
|
+
const updates = new Map();
|
|
199
|
+
const leftRelated = ensureRelatedEdgeLine(left.content, right.title);
|
|
200
|
+
const rightRelated = ensureRelatedEdgeLine(right.content, left.title);
|
|
201
|
+
if (options.action === 'link') {
|
|
202
|
+
updates.set(left.path, leftRelated);
|
|
203
|
+
updates.set(right.path, rightRelated);
|
|
204
|
+
}
|
|
205
|
+
else if (options.action === 'ignore') {
|
|
206
|
+
updates.set(left.path, leftRelated);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const mergedLeft = appendMergedContent(leftRelated, right.title, right.content);
|
|
210
|
+
const mergedRight = ensureMergedMarker(rightRelated, left.title);
|
|
211
|
+
updates.set(left.path, mergedLeft);
|
|
212
|
+
updates.set(right.path, mergedRight);
|
|
213
|
+
}
|
|
214
|
+
for (const [path, content] of updates) {
|
|
215
|
+
await writeMarkdownFile(vaultPath, path, content);
|
|
216
|
+
}
|
|
217
|
+
const shouldIndex = options.autoIndex !== false;
|
|
218
|
+
const index = shouldIndex ? await indexVault(vaultPath) : undefined;
|
|
219
|
+
return {
|
|
220
|
+
action: options.action,
|
|
221
|
+
leftPath,
|
|
222
|
+
rightPath,
|
|
223
|
+
updatedPaths: Array.from(updates.keys()).sort((leftValue, rightValue) => leftValue.localeCompare(rightValue)),
|
|
224
|
+
...(index ? { index } : {})
|
|
225
|
+
};
|
|
226
|
+
};
|
|
@@ -25,6 +25,13 @@ body {
|
|
|
25
25
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
body {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
min-height: 100dvh;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
button,
|
|
29
36
|
input,
|
|
30
37
|
select {
|
|
@@ -32,70 +39,94 @@ select {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
.shell {
|
|
35
|
-
|
|
36
|
-
grid-template-columns: minmax(0, 1fr) 360px;
|
|
42
|
+
flex: 1 1 auto;
|
|
37
43
|
width: 100%;
|
|
38
|
-
height:
|
|
44
|
+
min-height: 0;
|
|
39
45
|
overflow: hidden;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
.workspace {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
43
51
|
position: relative;
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 100%;
|
|
44
54
|
min-width: 0;
|
|
45
55
|
min-height: 0;
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
.graph-header {
|
|
59
|
+
z-index: 5;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
min-height: 72px;
|
|
64
|
+
padding: 10px 16px;
|
|
65
|
+
border-bottom: 1px solid var(--line);
|
|
66
|
+
background: linear-gradient(180deg, rgba(17, 21, 27, 0.96) 0%, rgba(17, 21, 27, 0.86) 100%);
|
|
67
|
+
backdrop-filter: blur(8px);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.brand-block {
|
|
71
|
+
display: grid;
|
|
72
|
+
gap: 2px;
|
|
73
|
+
min-width: max-content;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.brand-block strong {
|
|
77
|
+
font-size: 18px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.graph-stage {
|
|
81
|
+
position: relative;
|
|
50
82
|
width: 100%;
|
|
51
83
|
height: 100%;
|
|
52
84
|
background:
|
|
53
85
|
radial-gradient(circle at 18% 20%, rgba(53, 208, 162, 0.12), transparent 28rem),
|
|
54
86
|
linear-gradient(135deg, #0d0f12 0%, #12161c 55%, #0a0d10 100%);
|
|
55
|
-
|
|
87
|
+
overflow: hidden;
|
|
56
88
|
}
|
|
57
89
|
|
|
58
|
-
#graph
|
|
59
|
-
|
|
90
|
+
#graph,
|
|
91
|
+
#graphGl {
|
|
92
|
+
display: block;
|
|
93
|
+
position: absolute;
|
|
94
|
+
inset: 0;
|
|
95
|
+
width: 100%;
|
|
96
|
+
height: 100%;
|
|
60
97
|
}
|
|
61
98
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
top: 18px;
|
|
65
|
-
left: 18px;
|
|
66
|
-
right: 18px;
|
|
67
|
-
display: flex;
|
|
68
|
-
align-items: center;
|
|
69
|
-
justify-content: space-between;
|
|
70
|
-
gap: 18px;
|
|
71
|
-
pointer-events: none;
|
|
99
|
+
#graph {
|
|
100
|
+
cursor: grab;
|
|
72
101
|
}
|
|
73
102
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
align-items: baseline;
|
|
77
|
-
gap: 12px;
|
|
103
|
+
#graphGl {
|
|
104
|
+
pointer-events: none;
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
#graph:active {
|
|
108
|
+
cursor: grabbing;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
.
|
|
85
|
-
.eyebrow,
|
|
86
|
-
.inspector small {
|
|
111
|
+
.eyebrow {
|
|
87
112
|
color: var(--muted);
|
|
88
113
|
font-size: 12px;
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
.search {
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
flex: 1 1 320px;
|
|
118
|
+
min-width: 220px;
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
.agent-filter {
|
|
97
122
|
width: min(220px, 28vw);
|
|
98
|
-
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.header-actions {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 10px;
|
|
129
|
+
margin-left: auto;
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
.search input,
|
|
@@ -116,9 +147,6 @@ select {
|
|
|
116
147
|
}
|
|
117
148
|
|
|
118
149
|
.toolbar {
|
|
119
|
-
position: absolute;
|
|
120
|
-
left: 18px;
|
|
121
|
-
bottom: 18px;
|
|
122
150
|
display: flex;
|
|
123
151
|
gap: 8px;
|
|
124
152
|
}
|
|
@@ -138,70 +166,34 @@ select {
|
|
|
138
166
|
color: var(--accent);
|
|
139
167
|
}
|
|
140
168
|
|
|
141
|
-
.
|
|
142
|
-
display:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
min-width: 0;
|
|
146
|
-
height: 100%;
|
|
147
|
-
padding: 24px;
|
|
148
|
-
border-left: 1px solid var(--line);
|
|
149
|
-
background: var(--panel);
|
|
150
|
-
overflow: auto;
|
|
169
|
+
.floating-metrics {
|
|
170
|
+
display: flex;
|
|
171
|
+
gap: 10px;
|
|
172
|
+
flex-wrap: wrap;
|
|
151
173
|
}
|
|
152
174
|
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
.metric-chip {
|
|
176
|
+
min-width: 94px;
|
|
177
|
+
padding: 10px 12px;
|
|
178
|
+
border: 1px solid var(--line);
|
|
179
|
+
border-radius: 10px;
|
|
180
|
+
background: rgba(21, 25, 31, 0.88);
|
|
181
|
+
display: grid;
|
|
182
|
+
gap: 3px;
|
|
157
183
|
}
|
|
158
184
|
|
|
159
|
-
.
|
|
160
|
-
margin-top: 6px;
|
|
185
|
+
.metric-chip strong {
|
|
161
186
|
font-size: 26px;
|
|
162
|
-
line-height: 1
|
|
163
|
-
overflow-wrap: anywhere;
|
|
187
|
+
line-height: 1;
|
|
164
188
|
}
|
|
165
189
|
|
|
166
|
-
.
|
|
167
|
-
margin-bottom: 10px;
|
|
190
|
+
.metric-chip small {
|
|
168
191
|
color: var(--muted);
|
|
169
|
-
font-size:
|
|
170
|
-
|
|
192
|
+
font-size: 11px;
|
|
193
|
+
letter-spacing: 0.03em;
|
|
171
194
|
text-transform: uppercase;
|
|
172
195
|
}
|
|
173
196
|
|
|
174
|
-
#path {
|
|
175
|
-
margin-top: 10px;
|
|
176
|
-
color: var(--muted);
|
|
177
|
-
line-height: 1.45;
|
|
178
|
-
overflow-wrap: anywhere;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.metrics {
|
|
182
|
-
display: grid;
|
|
183
|
-
grid-template-columns: repeat(3, 1fr);
|
|
184
|
-
border: 1px solid var(--line);
|
|
185
|
-
border-radius: 8px;
|
|
186
|
-
overflow: hidden;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.metrics div {
|
|
190
|
-
display: grid;
|
|
191
|
-
gap: 4px;
|
|
192
|
-
padding: 14px;
|
|
193
|
-
background: var(--panel-strong);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.metrics div + div {
|
|
197
|
-
border-left: 1px solid var(--line);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.metrics span {
|
|
201
|
-
font-size: 22px;
|
|
202
|
-
font-weight: 700;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
197
|
.tags {
|
|
206
198
|
display: flex;
|
|
207
199
|
flex-wrap: wrap;
|
|
@@ -215,6 +207,7 @@ select {
|
|
|
215
207
|
background: var(--accent-weak);
|
|
216
208
|
color: var(--accent);
|
|
217
209
|
font-size: 12px;
|
|
210
|
+
word-break: break-word;
|
|
218
211
|
overflow-wrap: anywhere;
|
|
219
212
|
}
|
|
220
213
|
|
|
@@ -230,6 +223,7 @@ li {
|
|
|
230
223
|
padding: 10px 0;
|
|
231
224
|
border-bottom: 1px solid var(--line);
|
|
232
225
|
color: var(--text);
|
|
226
|
+
word-break: break-word;
|
|
233
227
|
overflow-wrap: anywhere;
|
|
234
228
|
}
|
|
235
229
|
|
|
@@ -268,8 +262,8 @@ li small {
|
|
|
268
262
|
}
|
|
269
263
|
|
|
270
264
|
.content-dialog {
|
|
271
|
-
width: min(
|
|
272
|
-
max-height: calc(100svh -
|
|
265
|
+
width: min(1240px, calc(100vw - 24px));
|
|
266
|
+
max-height: calc(100svh - 20px);
|
|
273
267
|
padding: 0;
|
|
274
268
|
border: 1px solid var(--line);
|
|
275
269
|
border-radius: 8px;
|
|
@@ -285,8 +279,8 @@ li small {
|
|
|
285
279
|
|
|
286
280
|
.content-dialog article {
|
|
287
281
|
display: grid;
|
|
288
|
-
grid-template-rows: auto minmax(0, 1fr);
|
|
289
|
-
max-height: calc(100svh -
|
|
282
|
+
grid-template-rows: auto auto minmax(0, 1fr);
|
|
283
|
+
max-height: calc(100svh - 22px);
|
|
290
284
|
}
|
|
291
285
|
|
|
292
286
|
.content-dialog header {
|
|
@@ -333,6 +327,41 @@ li small {
|
|
|
333
327
|
color: var(--accent);
|
|
334
328
|
}
|
|
335
329
|
|
|
330
|
+
.content-meta {
|
|
331
|
+
display: grid;
|
|
332
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
333
|
+
gap: 10px;
|
|
334
|
+
padding: 14px 22px;
|
|
335
|
+
border-bottom: 1px solid var(--line);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.content-meta-section {
|
|
339
|
+
min-height: 0;
|
|
340
|
+
padding: 10px;
|
|
341
|
+
border: 1px solid var(--line);
|
|
342
|
+
border-radius: 8px;
|
|
343
|
+
background: var(--panel-strong);
|
|
344
|
+
display: grid;
|
|
345
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
346
|
+
gap: 8px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.content-meta-section h3 {
|
|
350
|
+
margin: 0;
|
|
351
|
+
color: var(--muted);
|
|
352
|
+
font-size: 11px;
|
|
353
|
+
font-weight: 700;
|
|
354
|
+
text-transform: uppercase;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.content-meta-section ul,
|
|
358
|
+
.content-meta-section .tags {
|
|
359
|
+
max-height: 220px;
|
|
360
|
+
overflow: auto;
|
|
361
|
+
align-content: flex-start;
|
|
362
|
+
padding-right: 4px;
|
|
363
|
+
}
|
|
364
|
+
|
|
336
365
|
.content-dialog .note-content {
|
|
337
366
|
max-height: none;
|
|
338
367
|
min-height: 0;
|
|
@@ -341,33 +370,56 @@ li small {
|
|
|
341
370
|
padding: 22px;
|
|
342
371
|
}
|
|
343
372
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
373
|
+
.app-footer {
|
|
374
|
+
flex: 0 0 28px;
|
|
375
|
+
height: 28px;
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
justify-content: center;
|
|
379
|
+
background: transparent;
|
|
380
|
+
}
|
|
349
381
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
382
|
+
.app-footer small {
|
|
383
|
+
color: var(--muted);
|
|
384
|
+
font-size: 11px;
|
|
385
|
+
letter-spacing: 0.02em;
|
|
386
|
+
}
|
|
355
387
|
|
|
356
|
-
|
|
388
|
+
@media (max-width: 860px) {
|
|
389
|
+
.graph-header {
|
|
357
390
|
align-items: stretch;
|
|
358
|
-
flex-
|
|
391
|
+
flex-wrap: wrap;
|
|
392
|
+
padding: 10px 12px;
|
|
393
|
+
min-height: 0;
|
|
359
394
|
}
|
|
360
395
|
|
|
361
396
|
.search {
|
|
362
397
|
width: 100%;
|
|
398
|
+
flex-basis: 100%;
|
|
399
|
+
order: 3;
|
|
363
400
|
}
|
|
364
401
|
|
|
365
402
|
.agent-filter {
|
|
366
403
|
width: 100%;
|
|
367
404
|
}
|
|
368
405
|
|
|
406
|
+
.header-actions {
|
|
407
|
+
width: 100%;
|
|
408
|
+
margin-left: 0;
|
|
409
|
+
justify-content: space-between;
|
|
410
|
+
order: 4;
|
|
411
|
+
}
|
|
412
|
+
|
|
369
413
|
.content-dialog header {
|
|
370
414
|
align-items: stretch;
|
|
371
415
|
flex-direction: column;
|
|
372
416
|
}
|
|
417
|
+
|
|
418
|
+
.metric-chip {
|
|
419
|
+
min-width: 82px;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.content-meta {
|
|
423
|
+
grid-template-columns: 1fr;
|
|
424
|
+
}
|
|
373
425
|
}`;
|