@jungjaehoon/mama-os 0.63.0 → 0.65.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 +61 -0
- package/README.md +13 -12
- 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/help-actions.d.ts +2 -0
- package/dist/api/help-actions.d.ts.map +1 -1
- package/dist/api/help-actions.js +4 -1
- package/dist/api/help-actions.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/api/work-actions.d.ts.map +1 -1
- package/dist/api/work-actions.js +9 -2
- package/dist/api/work-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 +6 -1
- package/dist/runtime/action-surface.d.ts.map +1 -1
- package/dist/runtime/action-surface.js +17 -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 +14 -1
- package/dist/runtime/owner-runtime.d.ts.map +1 -1
- package/dist/runtime/owner-runtime.js +29 -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 +15 -9
- 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.d.ts +3 -1
- package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
- package/dist/runtime/stimulus-delivery.js +14 -6
- package/dist/runtime/stimulus-delivery.js.map +1 -1
- package/dist/runtime/timezone.d.ts +6 -0
- package/dist/runtime/timezone.d.ts.map +1 -1
- package/dist/runtime/timezone.js +11 -0
- package/dist/runtime/timezone.js.map +1 -1
- package/dist/runtime/turn-orders.d.ts +23 -0
- package/dist/runtime/turn-orders.d.ts.map +1 -1
- package/dist/runtime/turn-orders.js +34 -11
- 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
|
|
|
10
11
|
/* eslint-env browser */
|
|
@@ -12,20 +13,9 @@
|
|
|
12
13
|
|
|
13
14
|
import { escapeHtml, getElementByIdOrNull } from '../utils/dom.js';
|
|
14
15
|
import { DebugLogger } from '../utils/debug-logger.js';
|
|
15
|
-
import { API, type
|
|
16
|
+
import { API, type MemoryLinks, type MemoryTimelineResponse } from '../utils/api.js';
|
|
16
17
|
import { renderSafeMarkdown } from '../utils/markdown.js';
|
|
17
|
-
|
|
18
|
-
type GraphNodeRecord = GraphNode & {
|
|
19
|
-
topic?: string;
|
|
20
|
-
decision?: string;
|
|
21
|
-
reasoning?: string;
|
|
22
|
-
created_at?: string | number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type GraphEdgeRecord = GraphEdge & {
|
|
26
|
-
from: GraphNodeRecord['id'];
|
|
27
|
-
to: GraphNodeRecord['id'];
|
|
28
|
-
};
|
|
18
|
+
import { groupInfo } from './memory-timeline.js';
|
|
29
19
|
|
|
30
20
|
type EdgeStyle = {
|
|
31
21
|
color: string;
|
|
@@ -33,15 +23,9 @@ type EdgeStyle = {
|
|
|
33
23
|
width: number;
|
|
34
24
|
};
|
|
35
25
|
|
|
36
|
-
type GraphInput = {
|
|
37
|
-
nodes: GraphNodeRecord[];
|
|
38
|
-
edges: GraphEdgeRecord[];
|
|
39
|
-
meta?: Record<string, unknown>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
26
|
const logger = new DebugLogger('Graph');
|
|
43
27
|
|
|
44
|
-
/** What
|
|
28
|
+
/** What a record's stored kind is called in the detail panel. */
|
|
45
29
|
export const KIND_LABELS: Record<string, string> = {
|
|
46
30
|
decision: 'Decision',
|
|
47
31
|
preference: 'Preference',
|
|
@@ -72,6 +56,7 @@ export const RELATION_LABELS: Record<string, string> = {
|
|
|
72
56
|
blocks: 'blocks',
|
|
73
57
|
};
|
|
74
58
|
|
|
59
|
+
const OUTSIDE_COLOR = '#D1D5DB';
|
|
75
60
|
const LABEL_LENGTH = 40;
|
|
76
61
|
|
|
77
62
|
function shortLabel(text: string): string {
|
|
@@ -79,28 +64,121 @@ function shortLabel(text: string): string {
|
|
|
79
64
|
return flat.length > LABEL_LENGTH ? `${flat.slice(0, LABEL_LENGTH - 1)}…` : flat;
|
|
80
65
|
}
|
|
81
66
|
|
|
67
|
+
export interface FilterGraphNode {
|
|
68
|
+
id: string;
|
|
69
|
+
/** The timeline group of a selected dot, or `outside` for a linked record the filter left out. */
|
|
70
|
+
group: string;
|
|
71
|
+
kind: string | null;
|
|
72
|
+
label: string;
|
|
73
|
+
/** The record whose detail a click opens: the record itself, or a work item's latest update. */
|
|
74
|
+
detailId: string;
|
|
75
|
+
/** Updates of a work item inside the period. */
|
|
76
|
+
updates: number;
|
|
77
|
+
outside: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FilterGraph {
|
|
81
|
+
nodes: FilterGraphNode[];
|
|
82
|
+
edges: Array<{ from: string; to: string; relation: string }>;
|
|
83
|
+
/** Which dot each listed record is drawn as: a work update is drawn as its item. */
|
|
84
|
+
recordNode: Record<string, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The graph of what the filters selected. A work item is one dot for all its updates; a link
|
|
89
|
+
* between two updates of one item is not drawn, and a link that touches nothing selected is left
|
|
90
|
+
* out. Days arrive newest first, so an item's first update seen is its latest.
|
|
91
|
+
*/
|
|
92
|
+
export function composeFilterGraph(
|
|
93
|
+
timeline: MemoryTimelineResponse,
|
|
94
|
+
links: MemoryLinks
|
|
95
|
+
): FilterGraph {
|
|
96
|
+
const nodes = new Map<string, FilterGraphNode>();
|
|
97
|
+
const recordNode: Record<string, string> = {};
|
|
98
|
+
for (const day of timeline.days) {
|
|
99
|
+
for (const group of day.groups) {
|
|
100
|
+
for (const record of group.records ?? []) {
|
|
101
|
+
nodes.set(record.id, {
|
|
102
|
+
id: record.id,
|
|
103
|
+
group: group.group,
|
|
104
|
+
kind: record.kind,
|
|
105
|
+
label: record.topic,
|
|
106
|
+
detailId: record.id,
|
|
107
|
+
updates: 0,
|
|
108
|
+
outside: false,
|
|
109
|
+
});
|
|
110
|
+
recordNode[record.id] = record.id;
|
|
111
|
+
}
|
|
112
|
+
for (const item of group.items ?? []) {
|
|
113
|
+
const id = `item:${item.commitmentId}`;
|
|
114
|
+
const node = nodes.get(id) ?? {
|
|
115
|
+
id,
|
|
116
|
+
group: 'work',
|
|
117
|
+
kind: 'commitment',
|
|
118
|
+
label: item.title ?? item.topic,
|
|
119
|
+
detailId: item.revisions[0].id,
|
|
120
|
+
updates: 0,
|
|
121
|
+
outside: false,
|
|
122
|
+
};
|
|
123
|
+
node.updates += item.revisions.length;
|
|
124
|
+
nodes.set(id, node);
|
|
125
|
+
for (const revision of item.revisions) recordNode[revision.id] = id;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Links name both ends with a node; a work update outside the filter is drawn as its item.
|
|
130
|
+
const dotFor = (ref: string): string => {
|
|
131
|
+
const listed = recordNode[ref];
|
|
132
|
+
if (listed !== undefined) return listed;
|
|
133
|
+
const commitmentId = links.nodes[ref].commitmentId;
|
|
134
|
+
return commitmentId === null ? ref : `item:${commitmentId}`;
|
|
135
|
+
};
|
|
136
|
+
const edges: FilterGraph['edges'] = [];
|
|
137
|
+
const seen = new Set<string>();
|
|
138
|
+
const outside: FilterGraphNode[] = [];
|
|
139
|
+
for (const link of links.edges) {
|
|
140
|
+
const from = dotFor(link.from);
|
|
141
|
+
const to = dotFor(link.to);
|
|
142
|
+
const key = `${from}\0${to}\0${link.relation}`;
|
|
143
|
+
if (from === to || seen.has(key) || (!nodes.has(from) && !nodes.has(to))) continue;
|
|
144
|
+
seen.add(key);
|
|
145
|
+
edges.push({ from, to, relation: link.relation });
|
|
146
|
+
for (const [dot, ref] of [
|
|
147
|
+
[from, link.from],
|
|
148
|
+
[to, link.to],
|
|
149
|
+
] as const) {
|
|
150
|
+
if (nodes.has(dot)) continue;
|
|
151
|
+
const node: FilterGraphNode = {
|
|
152
|
+
id: dot,
|
|
153
|
+
group: 'outside',
|
|
154
|
+
kind: links.nodes[ref].kind,
|
|
155
|
+
label: links.nodes[ref].label,
|
|
156
|
+
detailId: ref,
|
|
157
|
+
updates: 0,
|
|
158
|
+
outside: true,
|
|
159
|
+
};
|
|
160
|
+
nodes.set(dot, node);
|
|
161
|
+
outside.push(node);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
nodes: [...nodes.values()].filter((node) => !node.outside).concat(outside),
|
|
166
|
+
edges,
|
|
167
|
+
recordNode,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
82
171
|
/**
|
|
83
172
|
* Graph Module Class
|
|
84
173
|
*/
|
|
85
174
|
export class GraphModule {
|
|
86
175
|
network: VisNetwork | null = null;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
176
|
+
graph: FilterGraph = { nodes: [], edges: [], recordNode: {} };
|
|
177
|
+
// The links between records do not depend on the filter: read once per page load.
|
|
178
|
+
private links: Promise<MemoryLinks> | null = null;
|
|
179
|
+
// A slower earlier read must not draw or describe what the owner has moved away from.
|
|
180
|
+
private drawRequest = 0;
|
|
91
181
|
private detailRequest = 0;
|
|
92
|
-
nodeColors: Record<string, string> = {
|
|
93
|
-
decision: '#60A5FA',
|
|
94
|
-
preference: '#C084FC',
|
|
95
|
-
constraint: '#FB7185',
|
|
96
|
-
lesson: '#FBBF24',
|
|
97
|
-
workflow: '#F472B6',
|
|
98
|
-
fact: '#2DD4BF',
|
|
99
|
-
commitment: '#FB923C',
|
|
100
|
-
observation: '#94A3B8',
|
|
101
|
-
memory: '#CBD5E1', // Legacy memories may have no stored classification.
|
|
102
|
-
registry: '#A3E635',
|
|
103
|
-
};
|
|
104
182
|
edgeStyles: Record<string, EdgeStyle> = {
|
|
105
183
|
supersedes: { color: '#666666', dashes: false, width: 2 },
|
|
106
184
|
mentions: { color: '#6B4C9A', dashes: false, width: 2 },
|
|
@@ -114,72 +192,77 @@ export class GraphModule {
|
|
|
114
192
|
};
|
|
115
193
|
|
|
116
194
|
/**
|
|
117
|
-
* Draw
|
|
195
|
+
* Draw what the filters selected, from the same timeline answer the list shows.
|
|
118
196
|
*/
|
|
119
|
-
async
|
|
120
|
-
const request = ++this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.init(data);
|
|
130
|
-
const center = data.nodes.find((node) => String(node.id) === id);
|
|
131
|
-
if (center) void this.showDetail(center);
|
|
132
|
-
} finally {
|
|
133
|
-
if (loading && request === this.recordRequest) loading.style.display = 'none';
|
|
134
|
-
}
|
|
197
|
+
async showSelection(timeline: MemoryTimelineResponse): Promise<void> {
|
|
198
|
+
const request = ++this.drawRequest;
|
|
199
|
+
// A failed read is not kept: the next filter change reads the links again.
|
|
200
|
+
this.links ??= API.getMemoryLinks().catch((error: unknown) => {
|
|
201
|
+
this.links = null;
|
|
202
|
+
throw error;
|
|
203
|
+
});
|
|
204
|
+
const links = await this.links;
|
|
205
|
+
if (request !== this.drawRequest) return;
|
|
206
|
+
this.init(composeFilterGraph(timeline, links));
|
|
135
207
|
}
|
|
136
208
|
|
|
137
209
|
/**
|
|
138
|
-
*
|
|
210
|
+
* A listed record was picked: select its dot and open the record's own detail.
|
|
139
211
|
*/
|
|
140
|
-
|
|
212
|
+
focusRecord(recordId: string): void {
|
|
213
|
+
const dot = this.graph.recordNode[recordId];
|
|
214
|
+
const node = this.graph.nodes.find((candidate) => candidate.id === dot);
|
|
215
|
+
if (this.network && dot !== undefined) {
|
|
216
|
+
this.network.selectNodes([dot]);
|
|
217
|
+
this.network.focus(dot, { scale: 1.2, animation: { duration: 400 } });
|
|
218
|
+
}
|
|
219
|
+
void this.showDetail({
|
|
220
|
+
detailId: recordId,
|
|
221
|
+
label: node?.label ?? recordId,
|
|
222
|
+
kind: node?.kind ?? null,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
init(graph: FilterGraph): void {
|
|
141
227
|
const container = getElementByIdOrNull<HTMLDivElement>('graph-canvas');
|
|
142
228
|
if (!container) {
|
|
143
229
|
logger.error('graph-canvas element not found');
|
|
144
230
|
return;
|
|
145
231
|
}
|
|
146
|
-
this.
|
|
232
|
+
this.graph = graph;
|
|
147
233
|
this.network?.destroy?.();
|
|
148
234
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
235
|
+
const empty = getElementByIdOrNull<HTMLElement>('graph-empty');
|
|
236
|
+
if (empty) {
|
|
237
|
+
empty.style.display = graph.nodes.length === 0 ? '' : 'none';
|
|
238
|
+
empty.textContent = 'Nothing saved for this filter.';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const nodes = graph.nodes.map((node) => {
|
|
242
|
+
const color = node.outside ? OUTSIDE_COLOR : groupInfo(node.group).color;
|
|
153
243
|
return {
|
|
154
|
-
id:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
kind === 'observation' && !center
|
|
158
|
-
? undefined
|
|
159
|
-
: shortLabel(String(n.decision_preview ?? n.decision ?? n.topic ?? n.id)),
|
|
160
|
-
title: this.createNodeTooltip(n),
|
|
244
|
+
id: node.id,
|
|
245
|
+
label: shortLabel(node.label),
|
|
246
|
+
title: this.createNodeTooltip(node),
|
|
161
247
|
color: {
|
|
162
248
|
background: color,
|
|
163
|
-
border:
|
|
249
|
+
border: color,
|
|
164
250
|
highlight: { background: color, border: '#131313' },
|
|
165
251
|
},
|
|
166
|
-
size:
|
|
167
|
-
|
|
168
|
-
font: { color: '#131313', size: center ? 15 : 13 },
|
|
169
|
-
data: n,
|
|
252
|
+
size: node.outside ? 7 : node.updates > 0 ? 10 + Math.min(node.updates, 20) / 2 : 11,
|
|
253
|
+
font: { color: node.outside ? '#9CA3AF' : '#131313', size: 12 },
|
|
170
254
|
};
|
|
171
255
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const style = this.getEdgeStyle(e.relationship);
|
|
256
|
+
const edges = graph.edges.map((edge) => {
|
|
257
|
+
const style = this.getEdgeStyle(edge.relation);
|
|
175
258
|
return {
|
|
176
|
-
from:
|
|
177
|
-
to:
|
|
259
|
+
from: edge.from,
|
|
260
|
+
to: edge.to,
|
|
178
261
|
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
179
262
|
color: style.color,
|
|
180
263
|
dashes: style.dashes,
|
|
181
264
|
width: style.width,
|
|
182
|
-
title: this.relationLabel(
|
|
265
|
+
title: this.relationLabel(edge.relation),
|
|
183
266
|
};
|
|
184
267
|
});
|
|
185
268
|
|
|
@@ -192,43 +275,36 @@ export class GraphModule {
|
|
|
192
275
|
physics: {
|
|
193
276
|
enabled: true,
|
|
194
277
|
barnesHut: {
|
|
195
|
-
gravitationalConstant: -
|
|
196
|
-
centralGravity: 0.
|
|
197
|
-
springLength:
|
|
278
|
+
gravitationalConstant: -4000,
|
|
279
|
+
centralGravity: 0.5,
|
|
280
|
+
springLength: 120,
|
|
198
281
|
springConstant: 0.04,
|
|
199
282
|
damping: 0.09,
|
|
200
|
-
avoidOverlap:
|
|
283
|
+
avoidOverlap: 0.6,
|
|
201
284
|
},
|
|
202
285
|
stabilization: { enabled: true, iterations: 200, updateInterval: 25 },
|
|
203
286
|
},
|
|
204
287
|
interaction: { hover: true, tooltipDelay: 100, zoomView: true, dragView: true },
|
|
205
288
|
}
|
|
206
289
|
);
|
|
207
|
-
|
|
208
290
|
this.network.on('click', (params: { nodes: Array<string | number> }) => {
|
|
209
|
-
const
|
|
210
|
-
if (targetId === undefined) return;
|
|
211
|
-
const node = data.nodes.find((n) => String(n.id) === String(targetId));
|
|
291
|
+
const node = graph.nodes.find((candidate) => candidate.id === String(params.nodes[0]));
|
|
212
292
|
if (node) void this.showDetail(node);
|
|
213
293
|
});
|
|
214
|
-
// Stepping to a linked record redraws the graph around it.
|
|
215
|
-
this.network.on('doubleClick', (params: { nodes: Array<string | number> }) => {
|
|
216
|
-
const targetId = params.nodes[0];
|
|
217
|
-
if (targetId !== undefined && String(targetId) !== this.centerId) {
|
|
218
|
-
void this.showRecord(String(targetId));
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
294
|
this.network.on('stabilizationIterationsDone', () => {
|
|
222
295
|
this.network?.setOptions?.({ physics: { enabled: false } });
|
|
223
296
|
});
|
|
224
297
|
|
|
225
298
|
this.renderLegend();
|
|
226
|
-
|
|
227
|
-
legend?.classList.add('visible');
|
|
299
|
+
getElementByIdOrNull<HTMLElement>('legend-panel')?.classList.add('visible');
|
|
228
300
|
const stats = getElementByIdOrNull<HTMLElement>('graph-stats');
|
|
229
301
|
if (stats) {
|
|
302
|
+
const selected = graph.nodes.filter((node) => !node.outside).length;
|
|
303
|
+
const outside = graph.nodes.length - selected;
|
|
230
304
|
stats.style.display = '';
|
|
231
|
-
stats.textContent = `${
|
|
305
|
+
stats.textContent = `${selected} records and work items · ${graph.edges.length} links${
|
|
306
|
+
outside === 0 ? '' : ` · ${outside} linked records outside this filter, faded`
|
|
307
|
+
} · click a dot for its detail`;
|
|
232
308
|
}
|
|
233
309
|
}
|
|
234
310
|
|
|
@@ -236,45 +312,37 @@ export class GraphModule {
|
|
|
236
312
|
// Styling and legend
|
|
237
313
|
// =============================================
|
|
238
314
|
|
|
239
|
-
getNodeColor(kind = 'memory'): string {
|
|
240
|
-
return this.nodeColors[kind] ?? '#CBD5E1';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
315
|
getEdgeStyle(relationship?: string): EdgeStyle {
|
|
244
316
|
return (
|
|
245
317
|
this.edgeStyles[relationship ?? 'default'] ?? { color: '#4a4a6a', dashes: false, width: 2 }
|
|
246
318
|
);
|
|
247
319
|
}
|
|
248
320
|
|
|
249
|
-
kindLabel(kind: string): string {
|
|
250
|
-
return KIND_LABELS[kind] ?? kind;
|
|
321
|
+
kindLabel(kind: string | null): string {
|
|
322
|
+
return kind === null ? KIND_LABELS.memory : (KIND_LABELS[kind] ?? kind);
|
|
251
323
|
}
|
|
252
324
|
|
|
253
|
-
relationLabel(relationship
|
|
254
|
-
return
|
|
325
|
+
relationLabel(relationship: string): string {
|
|
326
|
+
return RELATION_LABELS[relationship] ?? relationship;
|
|
255
327
|
}
|
|
256
328
|
|
|
257
|
-
/** Count what is drawn, by
|
|
329
|
+
/** Count what is drawn, by group and by relationship, with the words the legend shows. */
|
|
258
330
|
getLegendEntries() {
|
|
259
|
-
const
|
|
331
|
+
const groups = new Map<string, number>();
|
|
260
332
|
const relations = new Map<string, number>();
|
|
261
|
-
for (const node of this.
|
|
262
|
-
|
|
263
|
-
kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
|
|
333
|
+
for (const node of this.graph.nodes) {
|
|
334
|
+
groups.set(node.group, (groups.get(node.group) ?? 0) + 1);
|
|
264
335
|
}
|
|
265
|
-
for (const edge of this.
|
|
266
|
-
|
|
267
|
-
relations.set(relation, (relations.get(relation) ?? 0) + 1);
|
|
336
|
+
for (const edge of this.graph.edges) {
|
|
337
|
+
relations.set(edge.relation, (relations.get(edge.relation) ?? 0) + 1);
|
|
268
338
|
}
|
|
269
339
|
return {
|
|
270
|
-
nodes: [...
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
color: this.getNodeColor(kind),
|
|
277
|
-
})),
|
|
340
|
+
nodes: [...groups].map(([group, count]) => ({
|
|
341
|
+
group,
|
|
342
|
+
label: group === 'outside' ? 'Linked, outside this filter' : groupInfo(group).label,
|
|
343
|
+
count,
|
|
344
|
+
color: group === 'outside' ? OUTSIDE_COLOR : groupInfo(group).color,
|
|
345
|
+
})),
|
|
278
346
|
edges: [...relations]
|
|
279
347
|
.sort(([, a], [, b]) => b - a)
|
|
280
348
|
.map(([relationship, count]) => ({
|
|
@@ -298,7 +366,7 @@ export class GraphModule {
|
|
|
298
366
|
`<div class="legend-row">${sample}<span class="legend-label">${escapeHtml(label)}</span><span class="legend-count">${count}</span></div>`;
|
|
299
367
|
container.innerHTML =
|
|
300
368
|
section(
|
|
301
|
-
'Dots',
|
|
369
|
+
'Dots (a work item is one dot for its updates)',
|
|
302
370
|
entries.nodes.map(({ label, count, color }) =>
|
|
303
371
|
row(`<span class="legend-dot" style="background:${color}"></span>`, label, count)
|
|
304
372
|
)
|
|
@@ -317,17 +385,17 @@ export class GraphModule {
|
|
|
317
385
|
if (entries.nodes.length === 0) container.textContent = 'Nothing drawn';
|
|
318
386
|
}
|
|
319
387
|
|
|
320
|
-
createNodeTooltip(node:
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
return `${
|
|
388
|
+
createNodeTooltip(node: FilterGraphNode): string {
|
|
389
|
+
const group = node.outside ? 'Linked, outside this filter' : groupInfo(node.group).label;
|
|
390
|
+
const updates = node.updates > 0 ? `\n${node.updates} updates in this period` : '';
|
|
391
|
+
return `${group} · ${this.kindLabel(node.kind)}\n${node.label.slice(0, 160)}${updates}`;
|
|
324
392
|
}
|
|
325
393
|
|
|
326
394
|
// =============================================
|
|
327
395
|
// Detail Panel
|
|
328
396
|
// =============================================
|
|
329
397
|
|
|
330
|
-
async showDetail(
|
|
398
|
+
async showDetail(target: Pick<FilterGraphNode, 'detailId' | 'label' | 'kind'>): Promise<void> {
|
|
331
399
|
const panel = getElementByIdOrNull<HTMLDivElement>('decision-detail-modal');
|
|
332
400
|
const topicEl = getElementByIdOrNull<HTMLElement>('detail-topic');
|
|
333
401
|
const kindEl = getElementByIdOrNull<HTMLElement>('detail-kind');
|
|
@@ -338,20 +406,21 @@ export class GraphModule {
|
|
|
338
406
|
logger.error('Detail panel elements missing');
|
|
339
407
|
return;
|
|
340
408
|
}
|
|
341
|
-
if (topicEl) topicEl.textContent =
|
|
342
|
-
if (kindEl) kindEl.textContent = this.kindLabel(
|
|
343
|
-
if (createdEl)
|
|
344
|
-
createdEl.textContent = node.created_at ? new Date(node.created_at).toLocaleString() : '-';
|
|
345
|
-
}
|
|
409
|
+
if (topicEl) topicEl.textContent = target.label;
|
|
410
|
+
if (kindEl) kindEl.textContent = this.kindLabel(target.kind);
|
|
411
|
+
if (createdEl) createdEl.textContent = '-';
|
|
346
412
|
decisionEl.innerHTML = '<span class="loading-similar">Loading details...</span>';
|
|
347
413
|
reasoningEl.innerHTML = '';
|
|
348
414
|
panel.classList.add('visible');
|
|
349
415
|
|
|
350
416
|
const request = ++this.detailRequest;
|
|
351
417
|
try {
|
|
352
|
-
const detail = await API.getGraphDetail(
|
|
418
|
+
const detail = await API.getGraphDetail(target.detailId);
|
|
353
419
|
if (request !== this.detailRequest) return;
|
|
354
420
|
const detailNode = detail.node;
|
|
421
|
+
if (createdEl && detailNode.created_at) {
|
|
422
|
+
createdEl.textContent = new Date(detailNode.created_at).toLocaleString();
|
|
423
|
+
}
|
|
355
424
|
decisionEl.innerHTML = renderSafeMarkdown(
|
|
356
425
|
String(detailNode.decision || detailNode.decision_preview || '-')
|
|
357
426
|
);
|
|
@@ -47,7 +47,7 @@ const OPERATION_LABELS: Record<string, string> = {
|
|
|
47
47
|
withdraw: 'withdrawn',
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
function groupInfo(group: string): { label: string; hint: string; color: string } {
|
|
50
|
+
export function groupInfo(group: string): { label: string; hint: string; color: string } {
|
|
51
51
|
return GROUPS[group] ?? { label: group, hint: `Records of kind ${group}`, color: '#CBD5E1' };
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -71,7 +71,14 @@ export class MemoryTimelineModule {
|
|
|
71
71
|
private bound = false;
|
|
72
72
|
private debouncedLoad = debounce(() => void this.load(), 300);
|
|
73
73
|
|
|
74
|
-
constructor(
|
|
74
|
+
constructor(
|
|
75
|
+
private readonly hooks: {
|
|
76
|
+
/** A listed record was picked. */
|
|
77
|
+
onSelect: (id: string) => void;
|
|
78
|
+
/** The list now shows this answer; the graph draws the same records. */
|
|
79
|
+
onRender: (data: MemoryTimelineResponse) => void;
|
|
80
|
+
}
|
|
81
|
+
) {}
|
|
75
82
|
|
|
76
83
|
/** Wire the controls once, then read the current period. */
|
|
77
84
|
async init(): Promise<void> {
|
|
@@ -127,7 +134,7 @@ export class MemoryTimelineModule {
|
|
|
127
134
|
document
|
|
128
135
|
.querySelector(`#memory-timeline [data-record-id="${CSS.escape(id)}"]`)
|
|
129
136
|
?.classList.add('selected');
|
|
130
|
-
this.onSelect(id);
|
|
137
|
+
this.hooks.onSelect(id);
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
private params(): Record<string, string> {
|
|
@@ -148,6 +155,7 @@ export class MemoryTimelineModule {
|
|
|
148
155
|
// A slower earlier read must not overwrite the answer to the latest filter.
|
|
149
156
|
if (request !== this.request) return;
|
|
150
157
|
this.render(data);
|
|
158
|
+
this.hooks.onRender(data);
|
|
151
159
|
} catch (error) {
|
|
152
160
|
if (request !== this.request) return;
|
|
153
161
|
list.innerHTML = `<div class="mt-empty mt-error">Could not read what was saved: ${escapeHtml(
|
|
@@ -180,11 +180,11 @@ export class WikiModule {
|
|
|
180
180
|
|
|
181
181
|
document.getElementById('wiki-new-btn')?.addEventListener('click', () => this.promptNewPage());
|
|
182
182
|
|
|
183
|
-
// Auto-open
|
|
183
|
+
// Auto-open the agent's table of contents only on initial load (no page selected yet)
|
|
184
184
|
if (!this.currentPath) {
|
|
185
|
-
const
|
|
186
|
-
if (!mobile &&
|
|
187
|
-
this.openPage(
|
|
185
|
+
const homeNode = tree.find((n) => n.name === 'Home.md');
|
|
186
|
+
if (!mobile && homeNode) {
|
|
187
|
+
this.openPage(homeNode.path);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
}
|
|
@@ -79,7 +79,7 @@ declare global {
|
|
|
79
79
|
|
|
80
80
|
interface VisNetwork {
|
|
81
81
|
on(
|
|
82
|
-
event: 'click' | '
|
|
82
|
+
event: 'click' | 'stabilized' | 'stabilizationIterationsDone',
|
|
83
83
|
handler: (params: { nodes: Array<string | number> }) => void
|
|
84
84
|
): void;
|
|
85
85
|
on(event: 'hoverNode' | 'blurNode', handler: (params: { node: string | number }) => void): void;
|
|
@@ -81,6 +81,11 @@ export interface TimelineGroup {
|
|
|
81
81
|
items?: TimelineItem[];
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export interface MemoryLinks {
|
|
85
|
+
edges: Array<{ from: string; to: string; relation: string }>;
|
|
86
|
+
nodes: Record<string, { kind: string; label: string; commitmentId: string | null }>;
|
|
87
|
+
}
|
|
88
|
+
|
|
84
89
|
export interface MemoryTimelineResponse {
|
|
85
90
|
from: string;
|
|
86
91
|
to: string;
|
|
@@ -683,9 +688,9 @@ export class API {
|
|
|
683
688
|
// Graph API
|
|
684
689
|
// =============================================
|
|
685
690
|
|
|
686
|
-
/**
|
|
687
|
-
static async
|
|
688
|
-
return this.get<
|
|
691
|
+
/** Every link between records, with each end's kind, label and work item. */
|
|
692
|
+
static async getMemoryLinks(): Promise<MemoryLinks> {
|
|
693
|
+
return this.get<MemoryLinks>('/api/memory/links');
|
|
689
694
|
}
|
|
690
695
|
|
|
691
696
|
/** What was saved in a period, by day, kind and item. */
|
package/public/viewer/viewer.css
CHANGED
|
@@ -476,8 +476,8 @@ button:not([class*="p-"]):not([class*="px-"]):not([class*="py-"]) { padding: 6px
|
|
|
476
476
|
flex-shrink: 0;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
|
|
480
|
-
|
|
479
|
+
#graph-empty {
|
|
480
|
+
padding: 0 24px;
|
|
481
481
|
text-align: center;
|
|
482
482
|
font-size: 14px;
|
|
483
483
|
color: #8a847c;
|
|
@@ -520,9 +520,7 @@
|
|
|
520
520
|
></div>
|
|
521
521
|
|
|
522
522
|
<div class="absolute inset-0 flex items-center justify-center" id="graph-empty">
|
|
523
|
-
|
|
524
|
-
>Pick a saved record on the left to see what it rests on and links to.</span
|
|
525
|
-
>
|
|
523
|
+
Reading what was saved...
|
|
526
524
|
</div>
|
|
527
525
|
|
|
528
526
|
<div
|
|
@@ -1069,15 +1067,17 @@ export MAMA_AUTH_TOKEN="your-secure-token-here"</pre
|
|
|
1069
1067
|
|
|
1070
1068
|
const memory = new MemoryModule();
|
|
1071
1069
|
const graph = new GraphModule();
|
|
1072
|
-
const timeline = new MemoryTimelineModule(
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
empty
|
|
1078
|
-
empty
|
|
1079
|
-
|
|
1080
|
-
|
|
1070
|
+
const timeline = new MemoryTimelineModule({
|
|
1071
|
+
onSelect: (id) => graph.focusRecord(id),
|
|
1072
|
+
onRender: (data) =>
|
|
1073
|
+
graph.showSelection(data).catch((error) => {
|
|
1074
|
+
console.error('[MAMA] Failed to draw the memory graph:', error);
|
|
1075
|
+
const empty = document.getElementById('graph-empty');
|
|
1076
|
+
if (empty) {
|
|
1077
|
+
empty.style.display = '';
|
|
1078
|
+
empty.textContent = `Could not read the links between records: ${error.message}`;
|
|
1079
|
+
}
|
|
1080
|
+
}),
|
|
1081
1081
|
});
|
|
1082
1082
|
const wiki = new WikiModule();
|
|
1083
1083
|
|