@jungjaehoon/mama-os 0.63.0 → 0.64.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.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/dist/api/attachment-actions.d.ts.map +1 -1
- package/dist/api/attachment-actions.js +6 -2
- package/dist/api/attachment-actions.js.map +1 -1
- package/dist/api/drive-actions.d.ts +2 -1
- package/dist/api/drive-actions.d.ts.map +1 -1
- package/dist/api/drive-actions.js +1 -0
- package/dist/api/drive-actions.js.map +1 -1
- package/dist/api/drive-delivery.d.ts +16 -0
- package/dist/api/drive-delivery.d.ts.map +1 -0
- package/dist/api/drive-delivery.js +190 -0
- package/dist/api/drive-delivery.js.map +1 -0
- package/dist/api/file-delivery.d.ts +2 -2
- package/dist/api/file-delivery.d.ts.map +1 -1
- package/dist/api/file-delivery.js +5 -5
- package/dist/api/file-delivery.js.map +1 -1
- package/dist/api/security-events.d.ts +5 -4
- package/dist/api/security-events.d.ts.map +1 -1
- package/dist/api/security-events.js +25 -3
- package/dist/api/security-events.js.map +1 -1
- package/dist/api/viewer-data.d.ts +5 -1
- package/dist/api/viewer-data.d.ts.map +1 -1
- package/dist/api/viewer-server.d.ts +1 -1
- package/dist/api/viewer-server.d.ts.map +1 -1
- package/dist/api/viewer-server.js +82 -17
- package/dist/api/viewer-server.js.map +1 -1
- package/dist/api/wiki-actions.d.ts.map +1 -1
- package/dist/api/wiki-actions.js +44 -0
- package/dist/api/wiki-actions.js.map +1 -1
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +14 -7
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/gateways/discord.d.ts.map +1 -1
- package/dist/gateways/discord.js +1 -1
- package/dist/gateways/discord.js.map +1 -1
- package/dist/gateways/slack.d.ts.map +1 -1
- package/dist/gateways/slack.js +1 -1
- package/dist/gateways/slack.js.map +1 -1
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +1 -1
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/replay/replay-source-catalog.js +1 -1
- package/dist/replay/replay-source-catalog.js.map +1 -1
- package/dist/runtime/action-surface.d.ts +4 -1
- package/dist/runtime/action-surface.d.ts.map +1 -1
- package/dist/runtime/action-surface.js +16 -1
- package/dist/runtime/action-surface.js.map +1 -1
- package/dist/runtime/config.d.ts +20 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +48 -3
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/outbound-attempts.d.ts +4 -2
- package/dist/runtime/outbound-attempts.d.ts.map +1 -1
- package/dist/runtime/outbound-attempts.js +17 -1
- package/dist/runtime/outbound-attempts.js.map +1 -1
- package/dist/runtime/owner-runtime.d.ts +6 -1
- package/dist/runtime/owner-runtime.d.ts.map +1 -1
- package/dist/runtime/owner-runtime.js +4 -1
- package/dist/runtime/owner-runtime.js.map +1 -1
- package/dist/runtime/owner-system-prompt.d.ts +1 -1
- package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
- package/dist/runtime/owner-system-prompt.js +10 -4
- package/dist/runtime/owner-system-prompt.js.map +1 -1
- package/dist/runtime/report-scheduler.d.ts +2 -1
- package/dist/runtime/report-scheduler.d.ts.map +1 -1
- package/dist/runtime/report-scheduler.js +1 -1
- package/dist/runtime/report-scheduler.js.map +1 -1
- package/dist/runtime/stimulus-delivery.js +1 -1
- package/dist/runtime/stimulus-delivery.js.map +1 -1
- package/dist/runtime/turn-orders.js +1 -1
- package/dist/runtime/turn-orders.js.map +1 -1
- package/dist/wiki/obsidian-writer.d.ts +0 -3
- package/dist/wiki/obsidian-writer.d.ts.map +1 -1
- package/dist/wiki/obsidian-writer.js +8 -108
- package/dist/wiki/obsidian-writer.js.map +1 -1
- package/dist/wiki/path-safety.d.ts +1 -1
- package/dist/wiki/path-safety.d.ts.map +1 -1
- package/dist/wiki/path-safety.js +2 -2
- package/dist/wiki/path-safety.js.map +1 -1
- package/dist/wiki/wiki-move.d.ts +12 -0
- package/dist/wiki/wiki-move.d.ts.map +1 -0
- package/dist/wiki/wiki-move.js +95 -0
- package/dist/wiki/wiki-move.js.map +1 -0
- package/dist/wiki/wiki-read.d.ts +7 -0
- package/dist/wiki/wiki-read.d.ts.map +1 -1
- package/dist/wiki/wiki-read.js +38 -17
- package/dist/wiki/wiki-read.js.map +1 -1
- package/package.json +2 -2
- package/public/viewer/js/modules/graph.js +177 -115
- package/public/viewer/js/modules/memory-timeline.js +6 -5
- package/public/viewer/js/modules/wiki.js +4 -4
- package/public/viewer/js/utils/api.js +3 -3
- package/public/viewer/src/modules/graph.ts +205 -136
- package/public/viewer/src/modules/memory-timeline.ts +11 -3
- package/public/viewer/src/modules/wiki.ts +4 -4
- package/public/viewer/src/types/global.d.ts +1 -1
- package/public/viewer/src/utils/api.ts +8 -3
- package/public/viewer/viewer.css +2 -2
- package/public/viewer/viewer.html +12 -12
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Graph Module -
|
|
2
|
+
* Graph Module - the filtered memory as a graph
|
|
3
3
|
* @module modules/graph
|
|
4
4
|
*
|
|
5
|
-
* The memory view
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The memory view's list and graph show the same records: whatever the period, kind and search
|
|
6
|
+
* select. One dot per record, one per work item for its updates in the period, and faded dots for
|
|
7
|
+
* the records they link to outside the filter. Lines are the links between records; the source
|
|
8
|
+
* messages a record rests on are listed in its detail.
|
|
8
9
|
*/
|
|
9
10
|
/* eslint-env browser */
|
|
10
11
|
/* global vis */
|
|
@@ -12,8 +13,9 @@ import { escapeHtml, getElementByIdOrNull } from '../utils/dom.js';
|
|
|
12
13
|
import { DebugLogger } from '../utils/debug-logger.js';
|
|
13
14
|
import { API } from '../utils/api.js';
|
|
14
15
|
import { renderSafeMarkdown } from '../utils/markdown.js';
|
|
16
|
+
import { groupInfo } from './memory-timeline.js';
|
|
15
17
|
const logger = new DebugLogger('Graph');
|
|
16
|
-
/** What
|
|
18
|
+
/** What a record's stored kind is called in the detail panel. */
|
|
17
19
|
export const KIND_LABELS = {
|
|
18
20
|
decision: 'Decision',
|
|
19
21
|
preference: 'Preference',
|
|
@@ -42,33 +44,107 @@ export const RELATION_LABELS = {
|
|
|
42
44
|
next_action_for: 'next step for',
|
|
43
45
|
blocks: 'blocks',
|
|
44
46
|
};
|
|
47
|
+
const OUTSIDE_COLOR = '#D1D5DB';
|
|
45
48
|
const LABEL_LENGTH = 40;
|
|
46
49
|
function shortLabel(text) {
|
|
47
50
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
48
51
|
return flat.length > LABEL_LENGTH ? `${flat.slice(0, LABEL_LENGTH - 1)}…` : flat;
|
|
49
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The graph of what the filters selected. A work item is one dot for all its updates; a link
|
|
55
|
+
* between two updates of one item is not drawn, and a link that touches nothing selected is left
|
|
56
|
+
* out. Days arrive newest first, so an item's first update seen is its latest.
|
|
57
|
+
*/
|
|
58
|
+
export function composeFilterGraph(timeline, links) {
|
|
59
|
+
const nodes = new Map();
|
|
60
|
+
const recordNode = {};
|
|
61
|
+
for (const day of timeline.days) {
|
|
62
|
+
for (const group of day.groups) {
|
|
63
|
+
for (const record of group.records ?? []) {
|
|
64
|
+
nodes.set(record.id, {
|
|
65
|
+
id: record.id,
|
|
66
|
+
group: group.group,
|
|
67
|
+
kind: record.kind,
|
|
68
|
+
label: record.topic,
|
|
69
|
+
detailId: record.id,
|
|
70
|
+
updates: 0,
|
|
71
|
+
outside: false,
|
|
72
|
+
});
|
|
73
|
+
recordNode[record.id] = record.id;
|
|
74
|
+
}
|
|
75
|
+
for (const item of group.items ?? []) {
|
|
76
|
+
const id = `item:${item.commitmentId}`;
|
|
77
|
+
const node = nodes.get(id) ?? {
|
|
78
|
+
id,
|
|
79
|
+
group: 'work',
|
|
80
|
+
kind: 'commitment',
|
|
81
|
+
label: item.title ?? item.topic,
|
|
82
|
+
detailId: item.revisions[0].id,
|
|
83
|
+
updates: 0,
|
|
84
|
+
outside: false,
|
|
85
|
+
};
|
|
86
|
+
node.updates += item.revisions.length;
|
|
87
|
+
nodes.set(id, node);
|
|
88
|
+
for (const revision of item.revisions)
|
|
89
|
+
recordNode[revision.id] = id;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Links name both ends with a node; a work update outside the filter is drawn as its item.
|
|
94
|
+
const dotFor = (ref) => {
|
|
95
|
+
const listed = recordNode[ref];
|
|
96
|
+
if (listed !== undefined)
|
|
97
|
+
return listed;
|
|
98
|
+
const commitmentId = links.nodes[ref].commitmentId;
|
|
99
|
+
return commitmentId === null ? ref : `item:${commitmentId}`;
|
|
100
|
+
};
|
|
101
|
+
const edges = [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const outside = [];
|
|
104
|
+
for (const link of links.edges) {
|
|
105
|
+
const from = dotFor(link.from);
|
|
106
|
+
const to = dotFor(link.to);
|
|
107
|
+
const key = `${from}\0${to}\0${link.relation}`;
|
|
108
|
+
if (from === to || seen.has(key) || (!nodes.has(from) && !nodes.has(to)))
|
|
109
|
+
continue;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
edges.push({ from, to, relation: link.relation });
|
|
112
|
+
for (const [dot, ref] of [
|
|
113
|
+
[from, link.from],
|
|
114
|
+
[to, link.to],
|
|
115
|
+
]) {
|
|
116
|
+
if (nodes.has(dot))
|
|
117
|
+
continue;
|
|
118
|
+
const node = {
|
|
119
|
+
id: dot,
|
|
120
|
+
group: 'outside',
|
|
121
|
+
kind: links.nodes[ref].kind,
|
|
122
|
+
label: links.nodes[ref].label,
|
|
123
|
+
detailId: ref,
|
|
124
|
+
updates: 0,
|
|
125
|
+
outside: true,
|
|
126
|
+
};
|
|
127
|
+
nodes.set(dot, node);
|
|
128
|
+
outside.push(node);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
nodes: [...nodes.values()].filter((node) => !node.outside).concat(outside),
|
|
133
|
+
edges,
|
|
134
|
+
recordNode,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
50
137
|
/**
|
|
51
138
|
* Graph Module Class
|
|
52
139
|
*/
|
|
53
140
|
export class GraphModule {
|
|
54
141
|
network = null;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
142
|
+
graph = { nodes: [], edges: [], recordNode: {} };
|
|
143
|
+
// The links between records do not depend on the filter: read once per page load.
|
|
144
|
+
links = null;
|
|
145
|
+
// A slower earlier read must not draw or describe what the owner has moved away from.
|
|
146
|
+
drawRequest = 0;
|
|
59
147
|
detailRequest = 0;
|
|
60
|
-
nodeColors = {
|
|
61
|
-
decision: '#60A5FA',
|
|
62
|
-
preference: '#C084FC',
|
|
63
|
-
constraint: '#FB7185',
|
|
64
|
-
lesson: '#FBBF24',
|
|
65
|
-
workflow: '#F472B6',
|
|
66
|
-
fact: '#2DD4BF',
|
|
67
|
-
commitment: '#FB923C',
|
|
68
|
-
observation: '#94A3B8',
|
|
69
|
-
memory: '#CBD5E1', // Legacy memories may have no stored classification.
|
|
70
|
-
registry: '#A3E635',
|
|
71
|
-
};
|
|
72
148
|
edgeStyles = {
|
|
73
149
|
supersedes: { color: '#666666', dashes: false, width: 2 },
|
|
74
150
|
mentions: { color: '#6B4C9A', dashes: false, width: 2 },
|
|
@@ -81,74 +157,74 @@ export class GraphModule {
|
|
|
81
157
|
synthesizes: { color: '#6B4C9A', width: 3, dashes: false },
|
|
82
158
|
};
|
|
83
159
|
/**
|
|
84
|
-
* Draw
|
|
160
|
+
* Draw what the filters selected, from the same timeline answer the list shows.
|
|
85
161
|
*/
|
|
86
|
-
async
|
|
87
|
-
const request = ++this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return;
|
|
98
|
-
this.centerId = id;
|
|
99
|
-
this.init(data);
|
|
100
|
-
const center = data.nodes.find((node) => String(node.id) === id);
|
|
101
|
-
if (center)
|
|
102
|
-
void this.showDetail(center);
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
if (loading && request === this.recordRequest)
|
|
106
|
-
loading.style.display = 'none';
|
|
107
|
-
}
|
|
162
|
+
async showSelection(timeline) {
|
|
163
|
+
const request = ++this.drawRequest;
|
|
164
|
+
// A failed read is not kept: the next filter change reads the links again.
|
|
165
|
+
this.links ??= API.getMemoryLinks().catch((error) => {
|
|
166
|
+
this.links = null;
|
|
167
|
+
throw error;
|
|
168
|
+
});
|
|
169
|
+
const links = await this.links;
|
|
170
|
+
if (request !== this.drawRequest)
|
|
171
|
+
return;
|
|
172
|
+
this.init(composeFilterGraph(timeline, links));
|
|
108
173
|
}
|
|
109
174
|
/**
|
|
110
|
-
*
|
|
175
|
+
* A listed record was picked: select its dot and open the record's own detail.
|
|
111
176
|
*/
|
|
112
|
-
|
|
177
|
+
focusRecord(recordId) {
|
|
178
|
+
const dot = this.graph.recordNode[recordId];
|
|
179
|
+
const node = this.graph.nodes.find((candidate) => candidate.id === dot);
|
|
180
|
+
if (this.network && dot !== undefined) {
|
|
181
|
+
this.network.selectNodes([dot]);
|
|
182
|
+
this.network.focus(dot, { scale: 1.2, animation: { duration: 400 } });
|
|
183
|
+
}
|
|
184
|
+
void this.showDetail({
|
|
185
|
+
detailId: recordId,
|
|
186
|
+
label: node?.label ?? recordId,
|
|
187
|
+
kind: node?.kind ?? null,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
init(graph) {
|
|
113
191
|
const container = getElementByIdOrNull('graph-canvas');
|
|
114
192
|
if (!container) {
|
|
115
193
|
logger.error('graph-canvas element not found');
|
|
116
194
|
return;
|
|
117
195
|
}
|
|
118
|
-
this.
|
|
196
|
+
this.graph = graph;
|
|
119
197
|
this.network?.destroy?.();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
198
|
+
const empty = getElementByIdOrNull('graph-empty');
|
|
199
|
+
if (empty) {
|
|
200
|
+
empty.style.display = graph.nodes.length === 0 ? '' : 'none';
|
|
201
|
+
empty.textContent = 'Nothing saved for this filter.';
|
|
202
|
+
}
|
|
203
|
+
const nodes = graph.nodes.map((node) => {
|
|
204
|
+
const color = node.outside ? OUTSIDE_COLOR : groupInfo(node.group).color;
|
|
124
205
|
return {
|
|
125
|
-
id:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
? undefined
|
|
129
|
-
: shortLabel(String(n.decision_preview ?? n.decision ?? n.topic ?? n.id)),
|
|
130
|
-
title: this.createNodeTooltip(n),
|
|
206
|
+
id: node.id,
|
|
207
|
+
label: shortLabel(node.label),
|
|
208
|
+
title: this.createNodeTooltip(node),
|
|
131
209
|
color: {
|
|
132
210
|
background: color,
|
|
133
|
-
border:
|
|
211
|
+
border: color,
|
|
134
212
|
highlight: { background: color, border: '#131313' },
|
|
135
213
|
},
|
|
136
|
-
size:
|
|
137
|
-
|
|
138
|
-
font: { color: '#131313', size: center ? 15 : 13 },
|
|
139
|
-
data: n,
|
|
214
|
+
size: node.outside ? 7 : node.updates > 0 ? 10 + Math.min(node.updates, 20) / 2 : 11,
|
|
215
|
+
font: { color: node.outside ? '#9CA3AF' : '#131313', size: 12 },
|
|
140
216
|
};
|
|
141
217
|
});
|
|
142
|
-
const edges =
|
|
143
|
-
const style = this.getEdgeStyle(
|
|
218
|
+
const edges = graph.edges.map((edge) => {
|
|
219
|
+
const style = this.getEdgeStyle(edge.relation);
|
|
144
220
|
return {
|
|
145
|
-
from:
|
|
146
|
-
to:
|
|
221
|
+
from: edge.from,
|
|
222
|
+
to: edge.to,
|
|
147
223
|
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
148
224
|
color: style.color,
|
|
149
225
|
dashes: style.dashes,
|
|
150
226
|
width: style.width,
|
|
151
|
-
title: this.relationLabel(
|
|
227
|
+
title: this.relationLabel(edge.relation),
|
|
152
228
|
};
|
|
153
229
|
});
|
|
154
230
|
this.network = new vis.Network(container, { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) }, {
|
|
@@ -157,79 +233,63 @@ export class GraphModule {
|
|
|
157
233
|
physics: {
|
|
158
234
|
enabled: true,
|
|
159
235
|
barnesHut: {
|
|
160
|
-
gravitationalConstant: -
|
|
161
|
-
centralGravity: 0.
|
|
162
|
-
springLength:
|
|
236
|
+
gravitationalConstant: -4000,
|
|
237
|
+
centralGravity: 0.5,
|
|
238
|
+
springLength: 120,
|
|
163
239
|
springConstant: 0.04,
|
|
164
240
|
damping: 0.09,
|
|
165
|
-
avoidOverlap:
|
|
241
|
+
avoidOverlap: 0.6,
|
|
166
242
|
},
|
|
167
243
|
stabilization: { enabled: true, iterations: 200, updateInterval: 25 },
|
|
168
244
|
},
|
|
169
245
|
interaction: { hover: true, tooltipDelay: 100, zoomView: true, dragView: true },
|
|
170
246
|
});
|
|
171
247
|
this.network.on('click', (params) => {
|
|
172
|
-
const
|
|
173
|
-
if (targetId === undefined)
|
|
174
|
-
return;
|
|
175
|
-
const node = data.nodes.find((n) => String(n.id) === String(targetId));
|
|
248
|
+
const node = graph.nodes.find((candidate) => candidate.id === String(params.nodes[0]));
|
|
176
249
|
if (node)
|
|
177
250
|
void this.showDetail(node);
|
|
178
251
|
});
|
|
179
|
-
// Stepping to a linked record redraws the graph around it.
|
|
180
|
-
this.network.on('doubleClick', (params) => {
|
|
181
|
-
const targetId = params.nodes[0];
|
|
182
|
-
if (targetId !== undefined && String(targetId) !== this.centerId) {
|
|
183
|
-
void this.showRecord(String(targetId));
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
252
|
this.network.on('stabilizationIterationsDone', () => {
|
|
187
253
|
this.network?.setOptions?.({ physics: { enabled: false } });
|
|
188
254
|
});
|
|
189
255
|
this.renderLegend();
|
|
190
|
-
|
|
191
|
-
legend?.classList.add('visible');
|
|
256
|
+
getElementByIdOrNull('legend-panel')?.classList.add('visible');
|
|
192
257
|
const stats = getElementByIdOrNull('graph-stats');
|
|
193
258
|
if (stats) {
|
|
259
|
+
const selected = graph.nodes.filter((node) => !node.outside).length;
|
|
260
|
+
const outside = graph.nodes.length - selected;
|
|
194
261
|
stats.style.display = '';
|
|
195
|
-
stats.textContent = `${
|
|
262
|
+
stats.textContent = `${selected} records and work items · ${graph.edges.length} links${outside === 0 ? '' : ` · ${outside} linked records outside this filter, faded`} · click a dot for its detail`;
|
|
196
263
|
}
|
|
197
264
|
}
|
|
198
265
|
// =============================================
|
|
199
266
|
// Styling and legend
|
|
200
267
|
// =============================================
|
|
201
|
-
getNodeColor(kind = 'memory') {
|
|
202
|
-
return this.nodeColors[kind] ?? '#CBD5E1';
|
|
203
|
-
}
|
|
204
268
|
getEdgeStyle(relationship) {
|
|
205
269
|
return (this.edgeStyles[relationship ?? 'default'] ?? { color: '#4a4a6a', dashes: false, width: 2 });
|
|
206
270
|
}
|
|
207
271
|
kindLabel(kind) {
|
|
208
|
-
return KIND_LABELS[kind] ?? kind;
|
|
272
|
+
return kind === null ? KIND_LABELS.memory : (KIND_LABELS[kind] ?? kind);
|
|
209
273
|
}
|
|
210
274
|
relationLabel(relationship) {
|
|
211
|
-
return
|
|
275
|
+
return RELATION_LABELS[relationship] ?? relationship;
|
|
212
276
|
}
|
|
213
|
-
/** Count what is drawn, by
|
|
277
|
+
/** Count what is drawn, by group and by relationship, with the words the legend shows. */
|
|
214
278
|
getLegendEntries() {
|
|
215
|
-
const
|
|
279
|
+
const groups = new Map();
|
|
216
280
|
const relations = new Map();
|
|
217
|
-
for (const node of this.
|
|
218
|
-
|
|
219
|
-
kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
|
|
281
|
+
for (const node of this.graph.nodes) {
|
|
282
|
+
groups.set(node.group, (groups.get(node.group) ?? 0) + 1);
|
|
220
283
|
}
|
|
221
|
-
for (const edge of this.
|
|
222
|
-
|
|
223
|
-
relations.set(relation, (relations.get(relation) ?? 0) + 1);
|
|
284
|
+
for (const edge of this.graph.edges) {
|
|
285
|
+
relations.set(edge.relation, (relations.get(edge.relation) ?? 0) + 1);
|
|
224
286
|
}
|
|
225
287
|
return {
|
|
226
|
-
nodes: [...
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
kind,
|
|
230
|
-
label: this.kindLabel(kind),
|
|
288
|
+
nodes: [...groups].map(([group, count]) => ({
|
|
289
|
+
group,
|
|
290
|
+
label: group === 'outside' ? 'Linked, outside this filter' : groupInfo(group).label,
|
|
231
291
|
count,
|
|
232
|
-
color:
|
|
292
|
+
color: group === 'outside' ? OUTSIDE_COLOR : groupInfo(group).color,
|
|
233
293
|
})),
|
|
234
294
|
edges: [...relations]
|
|
235
295
|
.sort(([, a], [, b]) => b - a)
|
|
@@ -251,7 +311,7 @@ export class GraphModule {
|
|
|
251
311
|
: `<div><div class="legend-heading">${title}</div>${rows.join('')}</div>`;
|
|
252
312
|
const row = (sample, label, count) => `<div class="legend-row">${sample}<span class="legend-label">${escapeHtml(label)}</span><span class="legend-count">${count}</span></div>`;
|
|
253
313
|
container.innerHTML =
|
|
254
|
-
section('Dots', entries.nodes.map(({ label, count, color }) => row(`<span class="legend-dot" style="background:${color}"></span>`, label, count))) +
|
|
314
|
+
section('Dots (a work item is one dot for its updates)', entries.nodes.map(({ label, count, color }) => row(`<span class="legend-dot" style="background:${color}"></span>`, label, count))) +
|
|
255
315
|
section('Lines (arrow points at what the record links to)', entries.edges.map(({ label, count, color, dashes, width }) => {
|
|
256
316
|
const dash = Array.isArray(dashes) ? dashes.join(' ') : dashes ? '5 5' : 'none';
|
|
257
317
|
return row(`<svg width="28" height="10" class="shrink-0" aria-hidden="true"><line x1="0" y1="5" x2="28" y2="5" stroke="${color}" stroke-width="${width}" stroke-dasharray="${dash}" /></svg>`, label, count);
|
|
@@ -260,14 +320,14 @@ export class GraphModule {
|
|
|
260
320
|
container.textContent = 'Nothing drawn';
|
|
261
321
|
}
|
|
262
322
|
createNodeTooltip(node) {
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
return `${
|
|
323
|
+
const group = node.outside ? 'Linked, outside this filter' : groupInfo(node.group).label;
|
|
324
|
+
const updates = node.updates > 0 ? `\n${node.updates} updates in this period` : '';
|
|
325
|
+
return `${group} · ${this.kindLabel(node.kind)}\n${node.label.slice(0, 160)}${updates}`;
|
|
266
326
|
}
|
|
267
327
|
// =============================================
|
|
268
328
|
// Detail Panel
|
|
269
329
|
// =============================================
|
|
270
|
-
async showDetail(
|
|
330
|
+
async showDetail(target) {
|
|
271
331
|
const panel = getElementByIdOrNull('decision-detail-modal');
|
|
272
332
|
const topicEl = getElementByIdOrNull('detail-topic');
|
|
273
333
|
const kindEl = getElementByIdOrNull('detail-kind');
|
|
@@ -279,21 +339,23 @@ export class GraphModule {
|
|
|
279
339
|
return;
|
|
280
340
|
}
|
|
281
341
|
if (topicEl)
|
|
282
|
-
topicEl.textContent =
|
|
342
|
+
topicEl.textContent = target.label;
|
|
283
343
|
if (kindEl)
|
|
284
|
-
kindEl.textContent = this.kindLabel(
|
|
285
|
-
if (createdEl)
|
|
286
|
-
createdEl.textContent =
|
|
287
|
-
}
|
|
344
|
+
kindEl.textContent = this.kindLabel(target.kind);
|
|
345
|
+
if (createdEl)
|
|
346
|
+
createdEl.textContent = '-';
|
|
288
347
|
decisionEl.innerHTML = '<span class="loading-similar">Loading details...</span>';
|
|
289
348
|
reasoningEl.innerHTML = '';
|
|
290
349
|
panel.classList.add('visible');
|
|
291
350
|
const request = ++this.detailRequest;
|
|
292
351
|
try {
|
|
293
|
-
const detail = await API.getGraphDetail(
|
|
352
|
+
const detail = await API.getGraphDetail(target.detailId);
|
|
294
353
|
if (request !== this.detailRequest)
|
|
295
354
|
return;
|
|
296
355
|
const detailNode = detail.node;
|
|
356
|
+
if (createdEl && detailNode.created_at) {
|
|
357
|
+
createdEl.textContent = new Date(detailNode.created_at).toLocaleString();
|
|
358
|
+
}
|
|
297
359
|
decisionEl.innerHTML = renderSafeMarkdown(String(detailNode.decision || detailNode.decision_preview || '-'));
|
|
298
360
|
reasoningEl.innerHTML = renderSafeMarkdown(String(detailNode.reasoning || '-'));
|
|
299
361
|
}
|
|
@@ -35,7 +35,7 @@ const OPERATION_LABELS = {
|
|
|
35
35
|
revise: 'updated',
|
|
36
36
|
withdraw: 'withdrawn',
|
|
37
37
|
};
|
|
38
|
-
function groupInfo(group) {
|
|
38
|
+
export function groupInfo(group) {
|
|
39
39
|
return GROUPS[group] ?? { label: group, hint: `Records of kind ${group}`, color: '#CBD5E1' };
|
|
40
40
|
}
|
|
41
41
|
function dayLabel(day) {
|
|
@@ -48,7 +48,7 @@ function dayLabel(day) {
|
|
|
48
48
|
}).format(new Date(`${day}T00:00:00Z`));
|
|
49
49
|
}
|
|
50
50
|
export class MemoryTimelineModule {
|
|
51
|
-
|
|
51
|
+
hooks;
|
|
52
52
|
period = '7d';
|
|
53
53
|
day = null;
|
|
54
54
|
query = '';
|
|
@@ -57,8 +57,8 @@ export class MemoryTimelineModule {
|
|
|
57
57
|
request = 0;
|
|
58
58
|
bound = false;
|
|
59
59
|
debouncedLoad = debounce(() => void this.load(), 300);
|
|
60
|
-
constructor(
|
|
61
|
-
this.
|
|
60
|
+
constructor(hooks) {
|
|
61
|
+
this.hooks = hooks;
|
|
62
62
|
}
|
|
63
63
|
/** Wire the controls once, then read the current period. */
|
|
64
64
|
async init() {
|
|
@@ -114,7 +114,7 @@ export class MemoryTimelineModule {
|
|
|
114
114
|
document
|
|
115
115
|
.querySelector(`#memory-timeline [data-record-id="${CSS.escape(id)}"]`)
|
|
116
116
|
?.classList.add('selected');
|
|
117
|
-
this.onSelect(id);
|
|
117
|
+
this.hooks.onSelect(id);
|
|
118
118
|
}
|
|
119
119
|
params() {
|
|
120
120
|
return {
|
|
@@ -135,6 +135,7 @@ export class MemoryTimelineModule {
|
|
|
135
135
|
if (request !== this.request)
|
|
136
136
|
return;
|
|
137
137
|
this.render(data);
|
|
138
|
+
this.hooks.onRender(data);
|
|
138
139
|
}
|
|
139
140
|
catch (error) {
|
|
140
141
|
if (request !== this.request)
|
|
@@ -154,11 +154,11 @@ export class WikiModule {
|
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
document.getElementById('wiki-new-btn')?.addEventListener('click', () => this.promptNewPage());
|
|
157
|
-
// Auto-open
|
|
157
|
+
// Auto-open the agent's table of contents only on initial load (no page selected yet)
|
|
158
158
|
if (!this.currentPath) {
|
|
159
|
-
const
|
|
160
|
-
if (!mobile &&
|
|
161
|
-
this.openPage(
|
|
159
|
+
const homeNode = tree.find((n) => n.name === 'Home.md');
|
|
160
|
+
if (!mobile && homeNode) {
|
|
161
|
+
this.openPage(homeNode.path);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
}
|
|
@@ -124,9 +124,9 @@ export class API {
|
|
|
124
124
|
// =============================================
|
|
125
125
|
// Graph API
|
|
126
126
|
// =============================================
|
|
127
|
-
/**
|
|
128
|
-
static async
|
|
129
|
-
return this.get('/api/
|
|
127
|
+
/** Every link between records, with each end's kind, label and work item. */
|
|
128
|
+
static async getMemoryLinks() {
|
|
129
|
+
return this.get('/api/memory/links');
|
|
130
130
|
}
|
|
131
131
|
/** What was saved in a period, by day, kind and item. */
|
|
132
132
|
static async getMemoryTimeline(params) {
|