@jungjaehoon/mama-os 0.62.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +1 -1
  3. package/dist/api/attachment-actions.d.ts.map +1 -1
  4. package/dist/api/attachment-actions.js +6 -2
  5. package/dist/api/attachment-actions.js.map +1 -1
  6. package/dist/api/drive-actions.d.ts +2 -1
  7. package/dist/api/drive-actions.d.ts.map +1 -1
  8. package/dist/api/drive-actions.js +1 -0
  9. package/dist/api/drive-actions.js.map +1 -1
  10. package/dist/api/drive-delivery.d.ts +16 -0
  11. package/dist/api/drive-delivery.d.ts.map +1 -0
  12. package/dist/api/drive-delivery.js +190 -0
  13. package/dist/api/drive-delivery.js.map +1 -0
  14. package/dist/api/file-delivery.d.ts +2 -2
  15. package/dist/api/file-delivery.d.ts.map +1 -1
  16. package/dist/api/file-delivery.js +5 -5
  17. package/dist/api/file-delivery.js.map +1 -1
  18. package/dist/api/security-events.d.ts +5 -4
  19. package/dist/api/security-events.d.ts.map +1 -1
  20. package/dist/api/security-events.js +25 -3
  21. package/dist/api/security-events.js.map +1 -1
  22. package/dist/api/viewer-data.d.ts +82 -1
  23. package/dist/api/viewer-data.d.ts.map +1 -1
  24. package/dist/api/viewer-data.js +116 -0
  25. package/dist/api/viewer-data.js.map +1 -1
  26. package/dist/api/viewer-server.d.ts +2 -2
  27. package/dist/api/viewer-server.d.ts.map +1 -1
  28. package/dist/api/viewer-server.js +154 -5
  29. package/dist/api/viewer-server.js.map +1 -1
  30. package/dist/api/wiki-actions.d.ts.map +1 -1
  31. package/dist/api/wiki-actions.js +44 -0
  32. package/dist/api/wiki-actions.js.map +1 -1
  33. package/dist/api/work-actions.d.ts +2 -0
  34. package/dist/api/work-actions.d.ts.map +1 -1
  35. package/dist/api/work-actions.js +4 -5
  36. package/dist/api/work-actions.js.map +1 -1
  37. package/dist/cli/commands/daemon.d.ts.map +1 -1
  38. package/dist/cli/commands/daemon.js +14 -7
  39. package/dist/cli/commands/daemon.js.map +1 -1
  40. package/dist/gateways/discord.d.ts.map +1 -1
  41. package/dist/gateways/discord.js +1 -1
  42. package/dist/gateways/discord.js.map +1 -1
  43. package/dist/gateways/slack.d.ts.map +1 -1
  44. package/dist/gateways/slack.js +1 -1
  45. package/dist/gateways/slack.js.map +1 -1
  46. package/dist/gateways/telegram.d.ts.map +1 -1
  47. package/dist/gateways/telegram.js +1 -1
  48. package/dist/gateways/telegram.js.map +1 -1
  49. package/dist/replay/replay-source-catalog.js +1 -1
  50. package/dist/replay/replay-source-catalog.js.map +1 -1
  51. package/dist/runtime/action-surface.d.ts +4 -1
  52. package/dist/runtime/action-surface.d.ts.map +1 -1
  53. package/dist/runtime/action-surface.js +19 -1
  54. package/dist/runtime/action-surface.js.map +1 -1
  55. package/dist/runtime/config.d.ts +20 -0
  56. package/dist/runtime/config.d.ts.map +1 -1
  57. package/dist/runtime/config.js +48 -3
  58. package/dist/runtime/config.js.map +1 -1
  59. package/dist/runtime/outbound-attempts.d.ts +4 -2
  60. package/dist/runtime/outbound-attempts.d.ts.map +1 -1
  61. package/dist/runtime/outbound-attempts.js +17 -1
  62. package/dist/runtime/outbound-attempts.js.map +1 -1
  63. package/dist/runtime/owner-runtime.d.ts +6 -1
  64. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  65. package/dist/runtime/owner-runtime.js +4 -1
  66. package/dist/runtime/owner-runtime.js.map +1 -1
  67. package/dist/runtime/owner-system-prompt.d.ts +1 -1
  68. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  69. package/dist/runtime/owner-system-prompt.js +10 -4
  70. package/dist/runtime/owner-system-prompt.js.map +1 -1
  71. package/dist/runtime/report-scheduler.d.ts +2 -1
  72. package/dist/runtime/report-scheduler.d.ts.map +1 -1
  73. package/dist/runtime/report-scheduler.js +1 -1
  74. package/dist/runtime/report-scheduler.js.map +1 -1
  75. package/dist/runtime/stimulus-delivery.js +1 -1
  76. package/dist/runtime/stimulus-delivery.js.map +1 -1
  77. package/dist/runtime/turn-orders.d.ts.map +1 -1
  78. package/dist/runtime/turn-orders.js +7 -3
  79. package/dist/runtime/turn-orders.js.map +1 -1
  80. package/dist/wiki/obsidian-writer.d.ts +0 -3
  81. package/dist/wiki/obsidian-writer.d.ts.map +1 -1
  82. package/dist/wiki/obsidian-writer.js +8 -108
  83. package/dist/wiki/obsidian-writer.js.map +1 -1
  84. package/dist/wiki/path-safety.d.ts +1 -1
  85. package/dist/wiki/path-safety.d.ts.map +1 -1
  86. package/dist/wiki/path-safety.js +2 -2
  87. package/dist/wiki/path-safety.js.map +1 -1
  88. package/dist/wiki/wiki-move.d.ts +12 -0
  89. package/dist/wiki/wiki-move.d.ts.map +1 -0
  90. package/dist/wiki/wiki-move.js +95 -0
  91. package/dist/wiki/wiki-move.js.map +1 -0
  92. package/dist/wiki/wiki-read.d.ts +7 -0
  93. package/dist/wiki/wiki-read.d.ts.map +1 -1
  94. package/dist/wiki/wiki-read.js +38 -17
  95. package/dist/wiki/wiki-read.js.map +1 -1
  96. package/package.json +2 -2
  97. package/public/viewer/js/modules/graph.js +266 -992
  98. package/public/viewer/js/modules/memory-timeline.js +216 -0
  99. package/public/viewer/js/modules/wiki.js +4 -4
  100. package/public/viewer/js/utils/api.js +7 -26
  101. package/public/viewer/src/modules/graph.ts +307 -1185
  102. package/public/viewer/src/modules/memory-timeline.ts +280 -0
  103. package/public/viewer/src/modules/wiki.ts +4 -4
  104. package/public/viewer/src/utils/api.ts +48 -47
  105. package/public/viewer/viewer.css +257 -7
  106. package/public/viewer/viewer.html +68 -292
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Memory Timeline Module - what was saved, and when
3
+ * @module modules/memory-timeline
4
+ *
5
+ * The memory view's list: a period (today, 7 days, 30 days or one day), then by day, then by
6
+ * kind, then by work item and its updates. Every filter is a server read, so the list and the
7
+ * counts on the kind chips always describe the same records.
8
+ */
9
+
10
+ /* eslint-env browser */
11
+
12
+ import { escapeAttr, escapeHtml, debounce, getElementByIdOrNull } from '../utils/dom.js';
13
+ import {
14
+ API,
15
+ type MemoryTimelineResponse,
16
+ type TimelineGroup,
17
+ type TimelineItem,
18
+ type TimelineRecord,
19
+ } from '../utils/api.js';
20
+
21
+ /** The groups the server names first, in its order, with the words and colours the list shows. */
22
+ const GROUPS: Record<string, { label: string; hint: string; color: string }> = {
23
+ owner_rule: {
24
+ label: 'Owner rules',
25
+ hint: 'Rules the owner set in their own chat',
26
+ color: '#FB7185',
27
+ },
28
+ learned: {
29
+ label: 'Learned rules',
30
+ hint: 'Lessons, preferences, constraints and workflows learned in other turns',
31
+ color: '#FBBF24',
32
+ },
33
+ decision: { label: 'Decisions', hint: 'Decisions recorded', color: '#60A5FA' },
34
+ fact: { label: 'Facts', hint: 'Facts recorded', color: '#2DD4BF' },
35
+ work: { label: 'Work updates', hint: 'Work items opened or updated', color: '#FB923C' },
36
+ };
37
+
38
+ const VIA_LABELS: Record<string, string> = {
39
+ owner_chat: 'owner chat',
40
+ source_delta: 'source update',
41
+ report: 'report',
42
+ };
43
+
44
+ const OPERATION_LABELS: Record<string, string> = {
45
+ create: 'opened',
46
+ revise: 'updated',
47
+ withdraw: 'withdrawn',
48
+ };
49
+
50
+ export function groupInfo(group: string): { label: string; hint: string; color: string } {
51
+ return GROUPS[group] ?? { label: group, hint: `Records of kind ${group}`, color: '#CBD5E1' };
52
+ }
53
+
54
+ function dayLabel(day: string): string {
55
+ return new Intl.DateTimeFormat(undefined, {
56
+ weekday: 'short',
57
+ year: 'numeric',
58
+ month: 'short',
59
+ day: 'numeric',
60
+ timeZone: 'UTC',
61
+ }).format(new Date(`${day}T00:00:00Z`));
62
+ }
63
+
64
+ export class MemoryTimelineModule {
65
+ period = '7d';
66
+ day: string | null = null;
67
+ query = '';
68
+ groups = new Set<string>();
69
+ selectedId: string | null = null;
70
+ private request = 0;
71
+ private bound = false;
72
+ private debouncedLoad = debounce(() => void this.load(), 300);
73
+
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
+ ) {}
82
+
83
+ /** Wire the controls once, then read the current period. */
84
+ async init(): Promise<void> {
85
+ if (!this.bound) {
86
+ this.bind();
87
+ this.bound = true;
88
+ }
89
+ await this.load();
90
+ }
91
+
92
+ private bind(): void {
93
+ document.querySelectorAll<HTMLButtonElement>('[data-memory-period]').forEach((button) => {
94
+ button.addEventListener('click', () => {
95
+ this.period = button.dataset.memoryPeriod ?? '7d';
96
+ this.day = null;
97
+ const dayInput = getElementByIdOrNull<HTMLInputElement>('memory-day');
98
+ if (dayInput) dayInput.value = '';
99
+ void this.load();
100
+ });
101
+ });
102
+ getElementByIdOrNull<HTMLInputElement>('memory-day')?.addEventListener('change', (event) => {
103
+ const value = (event.target as HTMLInputElement).value;
104
+ this.day = value === '' ? null : value;
105
+ void this.load();
106
+ });
107
+ getElementByIdOrNull<HTMLInputElement>('memory-timeline-search')?.addEventListener(
108
+ 'input',
109
+ (event) => {
110
+ this.query = (event.target as HTMLInputElement).value.trim();
111
+ this.debouncedLoad();
112
+ }
113
+ );
114
+ getElementByIdOrNull<HTMLElement>('memory-group-chips')?.addEventListener('click', (event) => {
115
+ const chip = (event.target as HTMLElement).closest<HTMLButtonElement>('[data-group]');
116
+ if (!chip?.dataset.group) return;
117
+ const group = chip.dataset.group;
118
+ if (this.groups.has(group)) this.groups.delete(group);
119
+ else this.groups.add(group);
120
+ void this.load();
121
+ });
122
+ getElementByIdOrNull<HTMLElement>('memory-timeline')?.addEventListener('click', (event) => {
123
+ const row = (event.target as HTMLElement).closest<HTMLElement>('[data-record-id]');
124
+ if (!row?.dataset.recordId) return;
125
+ this.select(row.dataset.recordId);
126
+ });
127
+ }
128
+
129
+ private select(id: string): void {
130
+ this.selectedId = id;
131
+ document.querySelectorAll('#memory-timeline .mt-row.selected').forEach((row) => {
132
+ row.classList.remove('selected');
133
+ });
134
+ document
135
+ .querySelector(`#memory-timeline [data-record-id="${CSS.escape(id)}"]`)
136
+ ?.classList.add('selected');
137
+ this.hooks.onSelect(id);
138
+ }
139
+
140
+ private params(): Record<string, string> {
141
+ return {
142
+ ...(this.day === null ? { period: this.period } : { from: this.day, to: this.day }),
143
+ ...(this.query === '' ? {} : { q: this.query }),
144
+ ...(this.groups.size === 0 ? {} : { groups: [...this.groups].join(',') }),
145
+ };
146
+ }
147
+
148
+ async load(): Promise<void> {
149
+ const list = getElementByIdOrNull<HTMLElement>('memory-timeline');
150
+ if (!list) return;
151
+ const request = ++this.request;
152
+ list.setAttribute('aria-busy', 'true');
153
+ try {
154
+ const data = await API.getMemoryTimeline(this.params());
155
+ // A slower earlier read must not overwrite the answer to the latest filter.
156
+ if (request !== this.request) return;
157
+ this.render(data);
158
+ this.hooks.onRender(data);
159
+ } catch (error) {
160
+ if (request !== this.request) return;
161
+ list.innerHTML = `<div class="mt-empty mt-error">Could not read what was saved: ${escapeHtml(
162
+ error instanceof Error ? error.message : String(error)
163
+ )}</div>`;
164
+ } finally {
165
+ if (request === this.request) list.removeAttribute('aria-busy');
166
+ }
167
+ }
168
+
169
+ render(data: MemoryTimelineResponse): void {
170
+ document.querySelectorAll<HTMLButtonElement>('[data-memory-period]').forEach((button) => {
171
+ button.setAttribute(
172
+ 'aria-pressed',
173
+ String(this.day === null && button.dataset.memoryPeriod === this.period)
174
+ );
175
+ });
176
+ const range = getElementByIdOrNull<HTMLElement>('memory-timeline-range');
177
+ if (range) {
178
+ range.textContent =
179
+ data.from === data.to
180
+ ? dayLabel(data.from)
181
+ : `${dayLabel(data.from)} – ${dayLabel(data.to)}`;
182
+ }
183
+ const count = getElementByIdOrNull<HTMLElement>('memory-timeline-count');
184
+ if (count) count.textContent = `${data.total} saved`;
185
+ this.renderChips(data.counts);
186
+
187
+ const list = getElementByIdOrNull<HTMLElement>('memory-timeline');
188
+ if (!list) return;
189
+ if (data.days.length === 0) {
190
+ list.innerHTML = `<div class="mt-empty">Nothing saved${
191
+ this.query === '' ? '' : ` matching “${escapeHtml(this.query)}”`
192
+ } in this period.</div>`;
193
+ return;
194
+ }
195
+ list.innerHTML = data.days
196
+ .map(
197
+ (day) =>
198
+ `<section class="mt-day"><h3 class="mt-day-head"><span>${escapeHtml(
199
+ dayLabel(day.day)
200
+ )}</span><span class="mt-count">${day.total}</span></h3>${day.groups
201
+ .map((group) => this.renderGroup(group))
202
+ .join('')}</section>`
203
+ )
204
+ .join('');
205
+ if (this.selectedId !== null) {
206
+ document
207
+ .querySelector(`#memory-timeline [data-record-id="${CSS.escape(this.selectedId)}"]`)
208
+ ?.classList.add('selected');
209
+ }
210
+ }
211
+
212
+ private renderChips(counts: Record<string, number>): void {
213
+ const container = getElementByIdOrNull<HTMLElement>('memory-group-chips');
214
+ if (!container) return;
215
+ container.innerHTML = Object.entries(counts)
216
+ .filter(([group, count]) => count > 0 || this.groups.has(group))
217
+ .map(([group, count]) => {
218
+ const info = groupInfo(group);
219
+ return `<button type="button" class="memory-chip" data-group="${escapeAttr(
220
+ group
221
+ )}" aria-pressed="${this.groups.has(group)}" title="${escapeAttr(
222
+ info.hint
223
+ )}"><span class="mt-dot" style="background:${info.color}"></span>${escapeHtml(
224
+ info.label
225
+ )} <span class="mt-count">${count}</span></button>`;
226
+ })
227
+ .join('');
228
+ }
229
+
230
+ private renderGroup(group: TimelineGroup): string {
231
+ const info = groupInfo(group.group);
232
+ const body =
233
+ group.items !== undefined
234
+ ? group.items.map((item) => this.renderItem(item)).join('')
235
+ : (group.records ?? []).map((record) => this.renderRecord(record)).join('');
236
+ return `<details class="mt-group" open><summary class="mt-group-head" title="${escapeAttr(
237
+ info.hint
238
+ )}"><span class="mt-dot" style="background:${info.color}"></span>${escapeHtml(
239
+ info.label
240
+ )}<span class="mt-count">${group.count}</span></summary>${body}</details>`;
241
+ }
242
+
243
+ private renderRecord(record: TimelineRecord): string {
244
+ const via = record.via === null ? '' : (VIA_LABELS[record.via] ?? record.via);
245
+ const status =
246
+ record.status === null || record.status === 'active'
247
+ ? ''
248
+ : `<span class="mt-badge">${escapeHtml(record.status)}</span>`;
249
+ return `<button type="button" class="mt-row" data-record-id="${escapeAttr(
250
+ record.id
251
+ )}"><span class="mt-time">${escapeHtml(record.time)}</span><span class="mt-body"><span class="mt-topic">${escapeHtml(
252
+ record.topic
253
+ )}${status}</span><span class="mt-summary">${escapeHtml(record.summary)}</span>${
254
+ via === '' ? '' : `<span class="mt-via">from ${escapeHtml(via)}</span>`
255
+ }</span></button>`;
256
+ }
257
+
258
+ private renderItem(item: TimelineItem): string {
259
+ const latest = item.revisions[0];
260
+ const updates = item.revisions.length;
261
+ return `<details class="mt-item"><summary class="mt-item-head"><span class="mt-time">${escapeHtml(
262
+ latest?.time ?? ''
263
+ )}</span><span class="mt-body"><span class="mt-topic">${escapeHtml(
264
+ item.title ?? item.topic
265
+ )}</span><span class="mt-summary">${escapeHtml(latest?.summary ?? '')}</span></span><span class="mt-count">${updates} ${
266
+ updates === 1 ? 'update' : 'updates'
267
+ }</span></summary>${item.revisions
268
+ .map(
269
+ (revision) =>
270
+ `<button type="button" class="mt-row mt-revision" data-record-id="${escapeAttr(
271
+ revision.id
272
+ )}"><span class="mt-time">${escapeHtml(revision.time)}</span><span class="mt-body"><span class="mt-op">${escapeHtml(
273
+ OPERATION_LABELS[revision.operation ?? ''] ?? revision.operation ?? ''
274
+ )}${revision.revision === null ? '' : ` · revision ${revision.revision}`}</span><span class="mt-summary">${escapeHtml(
275
+ revision.summary
276
+ )}</span></span></button>`
277
+ )
278
+ .join('')}</details>`;
279
+ }
280
+ }
@@ -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 index page only on initial load (no page selected yet)
183
+ // Auto-open the agent's table of contents only on initial load (no page selected yet)
184
184
  if (!this.currentPath) {
185
- const indexNode = tree.find((n) => n.name === 'index.md');
186
- if (!mobile && indexNode) {
187
- this.openPage(indexNode.path);
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
  }
@@ -50,22 +50,50 @@ export interface GraphDetailResponse {
50
50
  [key: string]: unknown;
51
51
  }
52
52
 
53
- export interface SimilarDecision {
54
- id?: string | number;
55
- topic?: string;
56
- decision?: string;
57
- reasoning?: string;
58
- relationship?: string;
59
- similarity?: number;
60
- [key: string]: unknown;
53
+ export interface TimelineRecord {
54
+ id: string;
55
+ kind: string | null;
56
+ status: string | null;
57
+ topic: string;
58
+ summary: string;
59
+ time: string;
60
+ via: string | null;
61
61
  }
62
62
 
63
- export interface SimilarDecisionsResponse {
64
- similar: SimilarDecision[];
65
- [key: string]: unknown;
63
+ export interface TimelineItem {
64
+ commitmentId: string;
65
+ title: string | null;
66
+ topic: string;
67
+ revisions: Array<{
68
+ id: string;
69
+ revision: number | null;
70
+ operation: string | null;
71
+ status: string | null;
72
+ summary: string;
73
+ time: string;
74
+ }>;
75
+ }
76
+
77
+ export interface TimelineGroup {
78
+ group: string;
79
+ count: number;
80
+ records?: TimelineRecord[];
81
+ items?: TimelineItem[];
82
+ }
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 }>;
66
87
  }
67
88
 
68
- export type GraphSimilarResponse = GraphResponse & SimilarDecisionsResponse;
89
+ export interface MemoryTimelineResponse {
90
+ from: string;
91
+ to: string;
92
+ timeZone: string;
93
+ total: number;
94
+ counts: Record<string, number>;
95
+ days: Array<{ day: string; total: number; groups: TimelineGroup[] }>;
96
+ }
69
97
 
70
98
  export interface CheckpointSummary {
71
99
  id?: string;
@@ -660,45 +688,18 @@ export class API {
660
688
  // Graph API
661
689
  // =============================================
662
690
 
663
- /**
664
- * Get graph data
665
- * @param {Object} params - Query parameters
666
- * @returns {Promise<Object>} Graph data
667
- */
668
- static async getGraph(params: QueryParams = {}): Promise<GraphResponse> {
669
- // cluster: false by default to avoid slow embedding calculations
670
- return this.get<GraphResponse>('/graph', { cluster: 'false', ...params });
671
- }
672
-
673
- static async getGraphDetail(nodeId: string): Promise<GraphDetailResponse> {
674
- return this.get<GraphDetailResponse>('/graph/detail', { id: nodeId });
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');
675
694
  }
676
695
 
677
- /**
678
- * Get similar decisions for a node
679
- * @param {string} nodeId - Node ID
680
- * @returns {Promise<Object>} Similar decisions
681
- */
682
- static async getSimilarDecisions(nodeId: string): Promise<GraphSimilarResponse> {
683
- return this.get<GraphSimilarResponse>('/graph/similar', { id: nodeId });
696
+ /** What was saved in a period, by day, kind and item. */
697
+ static async getMemoryTimeline(params: QueryParams): Promise<MemoryTimelineResponse> {
698
+ return this.get<MemoryTimelineResponse>('/api/memory/timeline', params);
684
699
  }
685
700
 
686
- /**
687
- * Update decision outcome
688
- * @param {string} id - Decision ID
689
- * @param {string} outcome - Outcome value
690
- * @param {string} reason - Optional reason
691
- * @returns {Promise<Object>} Update result
692
- */
693
- static async updateOutcome(
694
- id: string,
695
- outcome: string,
696
- reason: string | null = null
697
- ): Promise<JsonRecord> {
698
- return this.post<JsonRecord, { id: string; outcome: string; reason: string | null }>(
699
- '/graph/update',
700
- { id, outcome, reason }
701
- );
701
+ static async getGraphDetail(nodeId: string): Promise<GraphDetailResponse> {
702
+ return this.get<GraphDetailResponse>('/graph/detail', { id: nodeId });
702
703
  }
703
704
 
704
705
  // =============================================
@@ -115,16 +115,16 @@ button:not([class*="p-"]):not([class*="px-"]):not([class*="py-"]) { padding: 6px
115
115
  max-width: 100% !important;
116
116
  border-right: none;
117
117
  border-bottom: 1px solid #e5e7eb;
118
- max-height: 35vh;
118
+ max-height: 55vh;
119
119
  }
120
120
  #sidebar-resize-handle {
121
121
  display: none;
122
122
  }
123
123
  #graph-container {
124
- min-height: 45vh;
124
+ min-height: 40vh;
125
125
  }
126
126
  #graph-canvas {
127
- min-height: 45vh;
127
+ min-height: 40vh;
128
128
  }
129
129
  }
130
130
  @keyframes float {
@@ -255,14 +255,264 @@ button:not([class*="p-"]):not([class*="px-"]):not([class*="py-"]) { padding: 6px
255
255
  flex-wrap: wrap;
256
256
  }
257
257
 
258
- .memory-controls #topic-filter {
258
+ .memory-controls #memory-timeline-search {
259
259
  min-width: 0;
260
- flex: 0 1 10rem;
260
+ flex: 1 1 12rem;
261
+ }
262
+
263
+ /* ============================================================================
264
+ MEMORY TIMELINE - period, kinds, then day > kind > item > update
265
+ ============================================================================ */
266
+
267
+ .memory-periods,
268
+ .memory-groups {
269
+ display: flex;
270
+ flex-wrap: wrap;
271
+ align-items: center;
272
+ gap: 4px;
273
+ }
274
+
275
+ .memory-chip {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ gap: 6px;
279
+ padding: 3px 10px;
280
+ border: 1px solid #e5e1d8;
281
+ border-radius: 9999px;
282
+ background: #fff;
283
+ color: #3f3a35;
284
+ font-size: 12px;
285
+ line-height: 18px;
286
+ cursor: pointer;
287
+ }
288
+
289
+ .memory-chip:hover {
290
+ background: #fffaf0;
291
+ }
292
+
293
+ .memory-chip[aria-pressed='true'] {
294
+ background: #131313;
295
+ border-color: #131313;
296
+ color: #fff;
297
+ }
298
+
299
+ .memory-chip[aria-pressed='true'] .mt-count {
300
+ color: #d4d0c8;
301
+ }
302
+
303
+ .memory-day {
304
+ padding: 2px 6px;
305
+ border: 1px solid #e5e1d8;
306
+ border-radius: 6px;
307
+ font-size: 12px;
308
+ color: #3f3a35;
309
+ }
310
+
311
+ .mt-head {
312
+ display: flex;
313
+ align-items: baseline;
314
+ justify-content: space-between;
315
+ gap: 8px;
316
+ padding: 8px 12px;
317
+ border-bottom: 1px solid #ede9e1;
318
+ }
319
+
320
+ .mt-range {
321
+ font-size: 13px;
322
+ font-weight: 600;
323
+ color: #1a1a1a;
324
+ }
325
+
326
+ .mt-total,
327
+ .mt-count {
328
+ font-size: 12px;
329
+ color: #8a847c;
330
+ font-variant-numeric: tabular-nums;
331
+ }
332
+
333
+ .mt-empty {
334
+ padding: 24px 16px;
335
+ text-align: center;
336
+ font-size: 13px;
337
+ color: #8a847c;
338
+ }
339
+
340
+ .mt-error {
341
+ color: #b42318;
342
+ }
343
+
344
+ #memory-timeline[aria-busy='true'] {
345
+ opacity: 0.6;
346
+ }
347
+
348
+ .mt-day-head {
349
+ position: sticky;
350
+ top: 0;
351
+ z-index: 1;
352
+ display: flex;
353
+ justify-content: space-between;
354
+ padding: 8px 12px;
355
+ background: #f7f5f0;
356
+ border-bottom: 1px solid #ede9e1;
357
+ font-size: 13px;
358
+ font-weight: 600;
359
+ color: #1a1a1a;
360
+ }
361
+
362
+ .mt-group-head,
363
+ .mt-item-head {
364
+ display: flex;
365
+ align-items: center;
366
+ gap: 8px;
367
+ cursor: pointer;
368
+ list-style: none;
369
+ }
370
+
371
+ .mt-group-head::-webkit-details-marker,
372
+ .mt-item-head::-webkit-details-marker {
373
+ display: none;
374
+ }
375
+
376
+ .mt-group-head {
377
+ padding: 6px 12px;
378
+ font-size: 12px;
379
+ font-weight: 600;
380
+ color: #3f3a35;
381
+ }
382
+
383
+ .mt-group-head .mt-count {
384
+ margin-left: auto;
261
385
  }
262
386
 
263
- .memory-controls #search-input {
387
+ .mt-dot {
388
+ width: 10px;
389
+ height: 10px;
390
+ border-radius: 9999px;
391
+ flex-shrink: 0;
392
+ }
393
+
394
+ .mt-row,
395
+ .mt-item-head {
396
+ display: flex;
397
+ align-items: flex-start;
398
+ gap: 10px;
399
+ width: 100%;
400
+ padding: 6px 12px 6px 30px;
401
+ border: 0;
402
+ border-bottom: 1px solid #f5f2ed;
403
+ background: transparent;
404
+ text-align: left;
405
+ cursor: pointer;
406
+ }
407
+
408
+ .mt-row:hover,
409
+ .mt-item-head:hover {
410
+ background: #fffdf5;
411
+ }
412
+
413
+ .mt-row.selected {
414
+ background: #fff4cc;
415
+ }
416
+
417
+ .mt-revision {
418
+ padding-left: 54px;
419
+ }
420
+
421
+ .mt-time {
422
+ flex-shrink: 0;
423
+ width: 40px;
424
+ font-size: 12px;
425
+ color: #8a847c;
426
+ font-variant-numeric: tabular-nums;
427
+ }
428
+
429
+ .mt-body {
430
+ display: flex;
431
+ flex-direction: column;
432
+ gap: 2px;
264
433
  min-width: 0;
265
- flex: 1 1 12rem;
434
+ flex: 1;
435
+ }
436
+
437
+ .mt-topic,
438
+ .mt-op {
439
+ font-size: 13px;
440
+ font-weight: 600;
441
+ color: #1a1a1a;
442
+ overflow-wrap: anywhere;
443
+ }
444
+
445
+ .mt-op {
446
+ font-weight: 500;
447
+ color: #6b6560;
448
+ }
449
+
450
+ .mt-summary {
451
+ font-size: 12px;
452
+ line-height: 1.45;
453
+ color: #4a4540;
454
+ display: -webkit-box;
455
+ -webkit-line-clamp: 2;
456
+ -webkit-box-orient: vertical;
457
+ overflow: hidden;
458
+ }
459
+
460
+ .mt-via {
461
+ font-size: 11px;
462
+ color: #8a847c;
463
+ }
464
+
465
+ .mt-badge {
466
+ margin-left: 6px;
467
+ padding: 0 6px;
468
+ border-radius: 9999px;
469
+ background: #f1eee8;
470
+ font-size: 11px;
471
+ font-weight: 500;
472
+ color: #6b6560;
473
+ }
474
+
475
+ .mt-item-head .mt-count {
476
+ flex-shrink: 0;
477
+ }
478
+
479
+ #graph-empty {
480
+ padding: 0 24px;
481
+ text-align: center;
482
+ font-size: 14px;
483
+ color: #8a847c;
484
+ }
485
+
486
+ .legend-heading {
487
+ margin-bottom: 6px;
488
+ font-size: 12px;
489
+ font-weight: 600;
490
+ color: #3f3a35;
491
+ }
492
+
493
+ .legend-row {
494
+ display: flex;
495
+ align-items: center;
496
+ gap: 8px;
497
+ margin-bottom: 4px;
498
+ font-size: 13px;
499
+ color: #1a1a1a;
500
+ }
501
+
502
+ .legend-label {
503
+ flex: 1;
504
+ }
505
+
506
+ .legend-count {
507
+ color: #8a847c;
508
+ font-variant-numeric: tabular-nums;
509
+ }
510
+
511
+ .legend-dot {
512
+ width: 12px;
513
+ height: 12px;
514
+ border-radius: 9999px;
515
+ flex-shrink: 0;
266
516
  }
267
517
 
268
518
  /* ============================================================================