claude-memory-admin 1.5.2 → 1.7.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/README.md +96 -129
- package/package.json +1 -1
- package/public/app.mjs +13 -34
- package/public/dialog.mjs +7 -0
- package/public/dialogs/bulk-delete.mjs +2 -1
- package/public/dialogs/trash.mjs +53 -0
- package/public/index.html +1 -0
- package/public/parts.mjs +10 -3
- package/public/state.mjs +31 -4
- package/public/store.mjs +13 -7
- package/public/styles.css +1 -1
- package/public/ui.mjs +9 -17
- package/public/views/cleanup.mjs +63 -0
- package/public/views/context.mjs +2 -1
- package/public/views/cost-issue.mjs +70 -0
- package/public/views/environment.mjs +14 -0
- package/public/views/header.mjs +105 -46
- package/public/views/issue.mjs +12 -20
- package/public/views/memories.mjs +5 -32
- package/public/views/memory-list.mjs +121 -0
- package/public/views/memory.mjs +32 -0
- package/public/views/path-check.mjs +2 -1
- package/public/views/rtk.mjs +148 -0
- package/public/views/search.mjs +4 -5
- package/public/views/segments.mjs +37 -0
- package/public/views/sessions.mjs +2 -2
- package/public/views/settings.mjs +2 -1
- package/public/views/stores.mjs +3 -3
- package/public/views/tools.mjs +76 -0
- package/public/views/worklist.mjs +33 -0
- package/server.mjs +9 -0
- package/src/rtk.mjs +179 -0
- package/public/views/health.mjs +0 -21
- package/public/views/prune.mjs +0 -191
- package/public/views/trash.mjs +0 -37
package/public/views/issue.mjs
CHANGED
|
@@ -3,10 +3,11 @@ import { node } from '/dom.mjs';
|
|
|
3
3
|
import { confirmDialog } from '/dialog.mjs';
|
|
4
4
|
import { api, toast } from '/api.mjs';
|
|
5
5
|
import { state } from '/state.mjs';
|
|
6
|
-
import { issue,
|
|
6
|
+
import { issue, goTo } from '/parts.mjs';
|
|
7
7
|
import { openStore, selectMemory, restoreFromTrash } from '/store.mjs';
|
|
8
8
|
import { openDeleteDialog } from '/dialogs/delete.mjs';
|
|
9
9
|
import { openAddEntryDialog } from '/dialogs/add-entry.mjs';
|
|
10
|
+
import { openHookEditor } from '/dialogs/hook-editor.mjs';
|
|
10
11
|
|
|
11
12
|
async function removeIndexLine(lineIndex, expectedText, done) {
|
|
12
13
|
const go = await confirmDialog({
|
|
@@ -138,17 +139,6 @@ export function renderIssue(item, memories) {
|
|
|
138
139
|
);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
if (item.kind === 'long-hooks') {
|
|
142
|
-
return issue(
|
|
143
|
-
`${item.count} index hook${item.count === 1 ? '' : 's'} over 200 characters`,
|
|
144
|
-
`longest is ${item.longest.hookLength} chars on "${item.longest.title}" - hooks load every session, so these are the cheapest thing to trim.`,
|
|
145
|
-
{
|
|
146
|
-
bad,
|
|
147
|
-
action: { label: 'Open Prune', run: () => goToTab('prune') },
|
|
148
|
-
},
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
142
|
if (item.kind === 'duplicate-name') {
|
|
153
143
|
return issue(
|
|
154
144
|
`Two memories claim the name "${item.name}"`,
|
|
@@ -205,22 +195,24 @@ export function renderIssue(item, memories) {
|
|
|
205
195
|
}
|
|
206
196
|
|
|
207
197
|
if (item.kind === 'hook-repeats-description') {
|
|
208
|
-
|
|
198
|
+
const row = issue(
|
|
209
199
|
`Hook repeats the description: ${item.file}`,
|
|
210
200
|
`MEMORY.md line ${item.index + 1} - ${item.hookLength} characters restating what the file's own description already says. Only the hook loads every session.`,
|
|
211
|
-
{
|
|
212
|
-
bad,
|
|
213
|
-
secondary: { label: 'Open', run: () => selectMemory(item.file) },
|
|
214
|
-
action: { label: 'Open Prune', run: () => goToTab('prune') },
|
|
215
|
-
},
|
|
201
|
+
{ bad, secondary: { label: 'Open', run: () => selectMemory(item.file) } },
|
|
216
202
|
);
|
|
203
|
+
row.append(node('button', {
|
|
204
|
+
class: ui.buttonPrimarySmall,
|
|
205
|
+
text: 'Edit hook',
|
|
206
|
+
onclick: () => openHookEditor(row, item.index),
|
|
207
|
+
}));
|
|
208
|
+
return row;
|
|
217
209
|
}
|
|
218
210
|
|
|
219
211
|
if (item.kind === 'empty-section') {
|
|
220
212
|
return issue(
|
|
221
213
|
`Empty section in MEMORY.md: "${item.section}"`,
|
|
222
214
|
`line ${item.index + 1} - the heading has no entries under it, and headings count against the 200-line budget too.`,
|
|
223
|
-
{ bad, action: { label: 'Open MEMORY.md', run: () =>
|
|
215
|
+
{ bad, action: { label: 'Open MEMORY.md', run: () => goTo('memory', 'index') } },
|
|
224
216
|
);
|
|
225
217
|
}
|
|
226
218
|
|
|
@@ -236,7 +228,7 @@ export function renderIssue(item, memories) {
|
|
|
236
228
|
return issue(
|
|
237
229
|
`${item.count} index ${item.count === 1 ? 'entry spills' : 'entries spill'} onto extra lines`,
|
|
238
230
|
`${item.extraLines} extra line${item.extraLines === 1 ? '' : 's'} of the 200-line budget - the guidance is one line per entry, with the detail in the topic file.`,
|
|
239
|
-
{ bad, action: { label: 'Open MEMORY.md', run: () =>
|
|
231
|
+
{ bad, action: { label: 'Open MEMORY.md', run: () => goTo('memory', 'index') } },
|
|
240
232
|
);
|
|
241
233
|
}
|
|
242
234
|
|
|
@@ -3,8 +3,9 @@ import { el, node } from '/dom.mjs';
|
|
|
3
3
|
import { renderMarkdown } from '/markdown.mjs';
|
|
4
4
|
import { state } from '/state.mjs';
|
|
5
5
|
import { selectMemory, loadSessionsForProvenance } from '/store.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { goTo, sessionDate } from '/parts.mjs';
|
|
7
7
|
import { openDeleteDialog } from '/dialogs/delete.mjs';
|
|
8
|
+
import { renderMemoryList } from '/views/memory-list.mjs';
|
|
8
9
|
|
|
9
10
|
export function memoryLookup(project) {
|
|
10
11
|
const byName = new Map();
|
|
@@ -26,25 +27,6 @@ export function renderBody(container, memory, project) {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function memoryButton(memory) {
|
|
30
|
-
const active = state.selected === memory.file;
|
|
31
|
-
return node('button', {
|
|
32
|
-
class: ui.memoryItem(active),
|
|
33
|
-
'data-memory': memory.file,
|
|
34
|
-
'data-active': active ? '' : null,
|
|
35
|
-
onclick: () => selectMemory(memory.file),
|
|
36
|
-
}, [
|
|
37
|
-
node('span', { class: ui.memoryTop }, [
|
|
38
|
-
node('span', { class: ui.memoryName, text: memory.name }),
|
|
39
|
-
node('span', { class: ui.typeBadge(memory.type), text: memory.type }),
|
|
40
|
-
memory.status !== 'indexed'
|
|
41
|
-
? node('span', { class: ui.badge('warn'), text: memory.status })
|
|
42
|
-
: null,
|
|
43
|
-
]),
|
|
44
|
-
memory.description ? node('span', { class: ui.memoryDesc, text: memory.description }) : null,
|
|
45
|
-
]);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
30
|
export function renderMemories(container) {
|
|
49
31
|
const project = state.store;
|
|
50
32
|
if (!project.memories.length) {
|
|
@@ -53,16 +35,7 @@ export function renderMemories(container) {
|
|
|
53
35
|
}
|
|
54
36
|
|
|
55
37
|
const left = node('div');
|
|
56
|
-
|
|
57
|
-
for (const memory of project.memories) {
|
|
58
|
-
const key = memory.section || (memory.status === 'indexed' ? 'Index' : 'Not in the index');
|
|
59
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
60
|
-
groups.get(key).push(memory);
|
|
61
|
-
}
|
|
62
|
-
for (const [section, memories] of groups) {
|
|
63
|
-
left.append(node('div', { class: ui.sectionLabel, text: `${section} · ${memories.length}` }));
|
|
64
|
-
for (const memory of memories) left.append(memoryButton(memory));
|
|
65
|
-
}
|
|
38
|
+
renderMemoryList(left);
|
|
66
39
|
|
|
67
40
|
const right = node('div', { id: 'detail', class: ui.detailPane });
|
|
68
41
|
container.append(node('div', { class: ui.split }, [left, right]));
|
|
@@ -165,8 +138,8 @@ function provenanceLine(memory) {
|
|
|
165
138
|
row.append(node('button', {
|
|
166
139
|
class: ui.provenanceLink(true),
|
|
167
140
|
text: label,
|
|
168
|
-
title: 'Open
|
|
169
|
-
onclick: () =>
|
|
141
|
+
title: 'Open Environment \u203a Sessions',
|
|
142
|
+
onclick: () => goTo('environment', 'sessions'),
|
|
170
143
|
}));
|
|
171
144
|
if (known?.gitBranch) row.append(node('span', { text: `on ${known.gitBranch}` }));
|
|
172
145
|
if (origin.modified) row.append(node('span', { text: sessionDate(origin.modified).slice(0, 10) }));
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as ui from '/ui.mjs';
|
|
2
|
+
import { node } from '/dom.mjs';
|
|
3
|
+
import { state } from '/state.mjs';
|
|
4
|
+
import { paint } from '/bus.mjs';
|
|
5
|
+
import { formatAge } from '/parts.mjs';
|
|
6
|
+
import { selectMemory } from '/store.mjs';
|
|
7
|
+
import { openBulkDeleteDialog } from '/dialogs/bulk-delete.mjs';
|
|
8
|
+
|
|
9
|
+
const SORTS = {
|
|
10
|
+
section: { label: 'by section', compare: null },
|
|
11
|
+
oldest: { label: 'oldest first', compare: (a, b) => (b.ageDays ?? -1) - (a.ageDays ?? -1) },
|
|
12
|
+
largest: { label: 'largest first', compare: (a, b) => b.bytes - a.bytes },
|
|
13
|
+
unlinked: { label: 'least linked', compare: (a, b) => a.inbound.length - b.inbound.length || (b.ageDays ?? 0) - (a.ageDays ?? 0) },
|
|
14
|
+
name: { label: 'by name', compare: (a, b) => a.name.localeCompare(b.name) },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function memoryButton(memory) {
|
|
18
|
+
const active = state.selected === memory.file;
|
|
19
|
+
return node('button', {
|
|
20
|
+
class: ui.memoryItem(active),
|
|
21
|
+
'data-memory': memory.file,
|
|
22
|
+
'data-active': active ? '' : null,
|
|
23
|
+
onclick: () => selectMemory(memory.file),
|
|
24
|
+
}, [
|
|
25
|
+
node('span', { class: ui.memoryTop }, [
|
|
26
|
+
node('span', { class: ui.memoryName, text: memory.name }),
|
|
27
|
+
node('span', { class: ui.typeBadge(memory.type), text: memory.type }),
|
|
28
|
+
memory.status !== 'indexed'
|
|
29
|
+
? node('span', { class: ui.badge('warn'), text: memory.status })
|
|
30
|
+
: null,
|
|
31
|
+
]),
|
|
32
|
+
memory.description ? node('span', { class: ui.memoryDesc, text: memory.description }) : null,
|
|
33
|
+
node('span', { class: ui.memoryFacts }, [
|
|
34
|
+
node('span', { class: ui.memoryFact((memory.ageDays ?? 0) > 180), text: formatAge(memory.ageDays) }),
|
|
35
|
+
node('span', { text: `${(memory.bytes / 1024).toFixed(1)}K` }),
|
|
36
|
+
node('span', { class: ui.memoryFact(!memory.inbound.length), text: `${memory.inbound.length} in` }),
|
|
37
|
+
]),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function memoryRow(memory, onToggle) {
|
|
42
|
+
const row = node('div', { class: ui.listItemRow });
|
|
43
|
+
if (state.selecting) {
|
|
44
|
+
const box = node('input', { type: 'checkbox', class: ui.checkbox, 'aria-label': `Select ${memory.name}` });
|
|
45
|
+
box.checked = state.listSelection.has(memory.file);
|
|
46
|
+
box.addEventListener('change', () => {
|
|
47
|
+
if (box.checked) state.listSelection.add(memory.file);
|
|
48
|
+
else state.listSelection.delete(memory.file);
|
|
49
|
+
onToggle();
|
|
50
|
+
});
|
|
51
|
+
row.append(box);
|
|
52
|
+
}
|
|
53
|
+
row.append(memoryButton(memory));
|
|
54
|
+
return row;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setSort(value) {
|
|
58
|
+
state.listSort = value;
|
|
59
|
+
localStorage.setItem('memoryListSort', value);
|
|
60
|
+
paint('tab');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toolbar() {
|
|
64
|
+
const bar = node('div', { class: ui.listBar });
|
|
65
|
+
const sort = node('select', { class: ui.select, 'aria-label': 'Sort memories' });
|
|
66
|
+
for (const [key, config] of Object.entries(SORTS)) {
|
|
67
|
+
sort.append(node('option', { value: key, text: config.label }));
|
|
68
|
+
}
|
|
69
|
+
sort.value = state.listSort;
|
|
70
|
+
sort.onchange = () => setSort(sort.value);
|
|
71
|
+
|
|
72
|
+
const count = node('span', { class: ui.note });
|
|
73
|
+
const deleteButton = node('button', { class: ui.buttonDangerSmall });
|
|
74
|
+
const selectButton = node('button', {
|
|
75
|
+
class: ui.buttonSmall,
|
|
76
|
+
text: state.selecting ? 'Done' : 'Select',
|
|
77
|
+
onclick: () => {
|
|
78
|
+
state.selecting = !state.selecting;
|
|
79
|
+
if (!state.selecting) state.listSelection.clear();
|
|
80
|
+
paint('tab');
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
bar.append(sort, node('span', { class: ui.listSpacer }), count, selectButton);
|
|
85
|
+
if (state.selecting) bar.append(deleteButton);
|
|
86
|
+
deleteButton.onclick = () => openBulkDeleteDialog([...state.listSelection]);
|
|
87
|
+
|
|
88
|
+
const update = () => {
|
|
89
|
+
const size = state.listSelection.size;
|
|
90
|
+
count.textContent = size ? `${size} selected` : '';
|
|
91
|
+
deleteButton.textContent = `Delete ${size || ''}`.trim();
|
|
92
|
+
deleteButton.disabled = size === 0;
|
|
93
|
+
};
|
|
94
|
+
update();
|
|
95
|
+
return { bar, update };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function renderMemoryList(container) {
|
|
99
|
+
const project = state.store;
|
|
100
|
+
const { bar, update } = toolbar();
|
|
101
|
+
container.append(bar);
|
|
102
|
+
|
|
103
|
+
const compare = SORTS[state.listSort].compare;
|
|
104
|
+
if (!compare) {
|
|
105
|
+
const groups = new Map();
|
|
106
|
+
for (const memory of project.memories) {
|
|
107
|
+
const key = memory.section || (memory.status === 'indexed' ? 'Index' : 'Not in the index');
|
|
108
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
109
|
+
groups.get(key).push(memory);
|
|
110
|
+
}
|
|
111
|
+
for (const [section, memories] of groups) {
|
|
112
|
+
container.append(node('div', { class: ui.sectionLabel, text: `${section} · ${memories.length}` }));
|
|
113
|
+
for (const memory of memories) container.append(memoryRow(memory, update));
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const memory of [...project.memories].sort(compare)) {
|
|
119
|
+
container.append(memoryRow(memory, update));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as ui from '/ui.mjs';
|
|
2
|
+
import { node } from '/dom.mjs';
|
|
3
|
+
import { state } from '/state.mjs';
|
|
4
|
+
import { paint } from '/bus.mjs';
|
|
5
|
+
import { renderSegmented } from '/views/segments.mjs';
|
|
6
|
+
import { renderMemories } from '/views/memories.mjs';
|
|
7
|
+
import { renderIndex } from '/views/index.mjs';
|
|
8
|
+
import { renderGraph } from '/graph.mjs';
|
|
9
|
+
import { selectMemory } from '/store.mjs';
|
|
10
|
+
|
|
11
|
+
function renderMemoryGraph(container) {
|
|
12
|
+
const wrap = node('div', { id: 'graph-wrap', class: ui.graphWrap });
|
|
13
|
+
container.append(wrap);
|
|
14
|
+
renderGraph(wrap, state.store.graph, {
|
|
15
|
+
selected: state.selected,
|
|
16
|
+
spread: state.spread,
|
|
17
|
+
onSelect: (file) => selectMemory(file),
|
|
18
|
+
onSpreadChange: (value) => {
|
|
19
|
+
state.spread = value;
|
|
20
|
+
localStorage.setItem('graphSpread', String(value));
|
|
21
|
+
paint('tab');
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderMemory(container) {
|
|
27
|
+
return renderSegmented(container, 'memory', {
|
|
28
|
+
list: renderMemories,
|
|
29
|
+
index: renderIndex,
|
|
30
|
+
graph: renderMemoryGraph,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as ui from '/ui.mjs';
|
|
|
2
2
|
import { node } from '/dom.mjs';
|
|
3
3
|
import { api } from '/api.mjs';
|
|
4
4
|
import { state } from '/state.mjs';
|
|
5
|
+
import { isAt } from '/parts.mjs';
|
|
5
6
|
import { paint } from '/bus.mjs';
|
|
6
7
|
import { renderIssue } from '/views/issue.mjs';
|
|
7
8
|
|
|
@@ -23,7 +24,7 @@ async function fetchPathCheck() {
|
|
|
23
24
|
}
|
|
24
25
|
if (state.storeId !== id) return;
|
|
25
26
|
state.pathCheck = next;
|
|
26
|
-
if (
|
|
27
|
+
if (isAt('cleanup')) paint('tab');
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function startPathCheck({ remember = true } = {}) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as ui from '/ui.mjs';
|
|
2
|
+
import { node } from '/dom.mjs';
|
|
3
|
+
|
|
4
|
+
const compact = (value) => {
|
|
5
|
+
const rounded = Math.round(value);
|
|
6
|
+
if (Math.abs(rounded) >= 1000000) return `${(rounded / 1000000).toFixed(1)}M`;
|
|
7
|
+
if (Math.abs(rounded) >= 1000) return `${(rounded / 1000).toFixed(1)}K`;
|
|
8
|
+
return String(rounded);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const clamp = (percent) => Math.min(100, Math.max(0, percent));
|
|
12
|
+
|
|
13
|
+
const fact = (value, label) => node('span', {}, [
|
|
14
|
+
node('b', { class: ui.meterFactValue, text: value }),
|
|
15
|
+
document.createTextNode(` ${label}`),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function meter(percent, unit, note, facts) {
|
|
19
|
+
return node('div', { class: ui.meter }, [
|
|
20
|
+
node('div', { class: ui.meterTop }, [
|
|
21
|
+
node('span', { class: ui.meterValue, text: `${percent.toFixed(1)}%` }),
|
|
22
|
+
node('span', { class: ui.meterUnit, text: unit }),
|
|
23
|
+
]),
|
|
24
|
+
node('div', { class: ui.meterBar }, [
|
|
25
|
+
node('div', { class: ui.meterFill('accent'), style: `width:${clamp(percent).toFixed(1)}%` }),
|
|
26
|
+
]),
|
|
27
|
+
note ? node('p', { class: ui.meterNote, text: note }) : null,
|
|
28
|
+
facts.length ? node('div', { class: ui.meterFacts }, facts) : null,
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function head(report, actions) {
|
|
33
|
+
return node('div', { class: ui.sectionLabelRow }, [
|
|
34
|
+
node('span', { class: ui.sectionLabelInline, text: report.version ? `rtk ${report.version}` : 'rtk' }),
|
|
35
|
+
node('span', { class: ui.note, text: report.path || '' }),
|
|
36
|
+
node('span', { class: ui.listSpacer }),
|
|
37
|
+
node('button', { class: ui.buttonSmall, text: 'Re-read', onclick: actions.reread }),
|
|
38
|
+
node('button', { class: ui.buttonSmall, text: 'Turn off', onclick: actions.turnOff }),
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function projectSection(report) {
|
|
43
|
+
const project = report.project;
|
|
44
|
+
if (!project) return [];
|
|
45
|
+
if (!project.commands) {
|
|
46
|
+
return [
|
|
47
|
+
node('div', { class: ui.sectionLabel, text: 'This project' }),
|
|
48
|
+
node('p', { class: ui.noteTight, text: `rtk has filtered nothing in ${report.projectDir} yet.` }),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
return [node('div', { class: ui.sectionLabel, text: 'This project' }), meter(
|
|
52
|
+
project.savedPct,
|
|
53
|
+
'of what rtk read in this project never reached the model',
|
|
54
|
+
'rtk scopes its ledger by working directory, so this covers every command run from this project path.',
|
|
55
|
+
[
|
|
56
|
+
fact(compact(project.saved), 'tokens saved'),
|
|
57
|
+
fact(compact(project.commands), 'commands filtered'),
|
|
58
|
+
fact(`${compact(project.input)} → ${compact(project.output)}`, 'in / out'),
|
|
59
|
+
fact(`${compact(project.avgMs)}ms`, 'average'),
|
|
60
|
+
],
|
|
61
|
+
)];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function machineMeter(report) {
|
|
65
|
+
const machine = report.machine;
|
|
66
|
+
if (!machine) return null;
|
|
67
|
+
const window = machine.window;
|
|
68
|
+
const facts = [
|
|
69
|
+
fact(compact(machine.saved), 'tokens saved'),
|
|
70
|
+
fact(compact(machine.commands), 'commands filtered'),
|
|
71
|
+
fact(`${compact(machine.avgMs)}ms`, 'average'),
|
|
72
|
+
];
|
|
73
|
+
if (window && window.commands) {
|
|
74
|
+
facts.push(fact(`${compact(window.saved)} (${window.savedPct.toFixed(1)}%)`, `saved in the last ${window.days} days`));
|
|
75
|
+
}
|
|
76
|
+
return meter(
|
|
77
|
+
machine.savedPct,
|
|
78
|
+
'of everything rtk has ever filtered on this machine',
|
|
79
|
+
null,
|
|
80
|
+
facts,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function missedRow(item) {
|
|
85
|
+
const saving = `${compact(item.savedTokens)} tokens (${item.savedPct.toFixed(0)}%)`;
|
|
86
|
+
return node('div', { class: ui.issue(false) }, [
|
|
87
|
+
node('div', { class: ui.issueBody }, [
|
|
88
|
+
node('div', { class: ui.issueTitle, text: `${item.command} ran ${item.count.toLocaleString()} times unfiltered` }),
|
|
89
|
+
node('div', { class: ui.issueDetail, text: `${item.equivalent} would have saved about ${saving}` }),
|
|
90
|
+
]),
|
|
91
|
+
node('span', { class: ui.badge('neutral'), text: item.category || 'Other' }),
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function discoverSection(report) {
|
|
96
|
+
const discover = report.discover;
|
|
97
|
+
if (!discover || !discover.commands) return [];
|
|
98
|
+
|
|
99
|
+
const parts = [
|
|
100
|
+
node('div', { class: ui.sectionLabel, text: 'Left on the table' }),
|
|
101
|
+
meter(
|
|
102
|
+
discover.adoptionPct,
|
|
103
|
+
`of the commands in the last ${discover.sinceDays} days already went through rtk`,
|
|
104
|
+
`Counted across ${discover.sessionsScanned.toLocaleString()} session transcripts for this project.`,
|
|
105
|
+
[
|
|
106
|
+
fact(discover.alreadyRtk.toLocaleString(), 'already filtered'),
|
|
107
|
+
fact(discover.commands.toLocaleString(), 'commands seen'),
|
|
108
|
+
fact(discover.sessionsScanned.toLocaleString(), 'sessions scanned'),
|
|
109
|
+
],
|
|
110
|
+
),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
if (!discover.missed.length) {
|
|
114
|
+
parts.push(node('p', { class: ui.okLine, text: 'Nothing rtk knows how to filter was run raw.' }));
|
|
115
|
+
return parts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const item of discover.missed) parts.push(missedRow(item));
|
|
119
|
+
const hidden = discover.missedTotal - discover.missed.length;
|
|
120
|
+
const left = hidden === 1 ? '1 smaller one is not shown' : `${hidden} smaller ones are not shown`;
|
|
121
|
+
parts.push(node('p', { class: ui.note, text: hidden > 0
|
|
122
|
+
? `Estimates from rtk itself, worst first; ${left}. They count what the filter would have stripped, not what the model would have ignored.`
|
|
123
|
+
: 'Estimates from rtk itself, worst first. They count what the filter would have stripped, not what the model would have ignored.' }));
|
|
124
|
+
return parts;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function errorNotes(report) {
|
|
128
|
+
if (!report.errors.length) return [];
|
|
129
|
+
return [
|
|
130
|
+
node('div', { class: ui.sectionLabel, text: 'Not read' }),
|
|
131
|
+
...report.errors.map((error) => node('p', { class: ui.noteTight, text: `${error.step}: ${error.message}` })),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function renderRtk(container, report, actions) {
|
|
136
|
+
container.append(head(report, actions));
|
|
137
|
+
for (const part of projectSection(report)) container.append(part);
|
|
138
|
+
|
|
139
|
+
container.append(node('div', { class: ui.sectionLabel, text: report.project ? 'Every project' : 'This machine' }));
|
|
140
|
+
const machine = machineMeter(report);
|
|
141
|
+
container.append(machine || node('p', { class: ui.noteTight, text: 'The machine-wide ledger could not be read.' }));
|
|
142
|
+
if (!report.project && !report.projectDir) {
|
|
143
|
+
container.append(node('p', { class: ui.note, text: 'This entry is the user scope rather than a project, so rtk has no working directory to slice its ledger by.' }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const part of discoverSection(report)) container.append(part);
|
|
147
|
+
for (const part of errorNotes(report)) container.append(part);
|
|
148
|
+
}
|
package/public/views/search.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { el, node } from '/dom.mjs';
|
|
|
3
3
|
import { api, toast } from '/api.mjs';
|
|
4
4
|
import { state } from '/state.mjs';
|
|
5
5
|
import { paint } from '/bus.mjs';
|
|
6
|
-
import { highlight } from '/parts.mjs';
|
|
6
|
+
import { highlight, goTo } from '/parts.mjs';
|
|
7
7
|
import { openStore } from '/store.mjs';
|
|
8
8
|
|
|
9
9
|
let searchTimer = null;
|
|
@@ -52,7 +52,7 @@ export function renderSearch() {
|
|
|
52
52
|
for (const result of group.results) {
|
|
53
53
|
const button = node('button', {
|
|
54
54
|
class: ui.result,
|
|
55
|
-
onclick: () => openFromSearch(group.id, result.file),
|
|
55
|
+
onclick: () => openFromSearch(group.id, result.file, 'list'),
|
|
56
56
|
});
|
|
57
57
|
const top = node('div', { class: ui.resultTop });
|
|
58
58
|
const name = node('span', { class: ui.resultName });
|
|
@@ -85,12 +85,11 @@ export function renderSearch() {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
async function openFromSearch(id, file,
|
|
88
|
+
async function openFromSearch(id, file, segment = 'list') {
|
|
89
89
|
clearSearch();
|
|
90
90
|
if (id !== state.storeId) await openStore(id);
|
|
91
|
-
state.tab = tab;
|
|
92
91
|
if (file) state.selected = file;
|
|
93
|
-
|
|
92
|
+
goTo('memory', segment);
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
export function clearSearch() {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as ui from '/ui.mjs';
|
|
2
|
+
import { node } from '/dom.mjs';
|
|
3
|
+
import { state } from '/state.mjs';
|
|
4
|
+
import { goTo } from '/parts.mjs';
|
|
5
|
+
import { segmentsFor } from '/views/header.mjs';
|
|
6
|
+
|
|
7
|
+
export function segmentBar(tab) {
|
|
8
|
+
const segments = segmentsFor(tab);
|
|
9
|
+
if (segments.length < 2) return null;
|
|
10
|
+
return node('div', { class: ui.segmentBar }, [
|
|
11
|
+
node('div', { class: ui.segmentGroup, role: 'tablist' }, segments.map((seg) => node('button', {
|
|
12
|
+
class: ui.segment(state.segment[tab] === seg.id),
|
|
13
|
+
role: 'tab',
|
|
14
|
+
'aria-selected': String(state.segment[tab] === seg.id),
|
|
15
|
+
onclick: () => goTo(tab, seg.id),
|
|
16
|
+
}, [
|
|
17
|
+
document.createTextNode(seg.label),
|
|
18
|
+
seg.badge ? node('span', { class: ui.tabBadge(seg.tone) }, [document.createTextNode(seg.badge)]) : null,
|
|
19
|
+
]))),
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function activeSegment(tab) {
|
|
24
|
+
const segments = segmentsFor(tab);
|
|
25
|
+
if (!segments.length) return null;
|
|
26
|
+
const current = state.segment[tab];
|
|
27
|
+
return segments.some((seg) => seg.id === current) ? current : segments[0].id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderSegmented(container, tab, views) {
|
|
31
|
+
const bar = segmentBar(tab);
|
|
32
|
+
if (bar) container.append(bar);
|
|
33
|
+
const host = node('div');
|
|
34
|
+
container.append(host);
|
|
35
|
+
const segment = activeSegment(tab);
|
|
36
|
+
return views[segment] ? views[segment](host) : null;
|
|
37
|
+
}
|
|
@@ -2,7 +2,7 @@ import * as ui from '/ui.mjs';
|
|
|
2
2
|
import { node } from '/dom.mjs';
|
|
3
3
|
import { api } from '/api.mjs';
|
|
4
4
|
import { state } from '/state.mjs';
|
|
5
|
-
import { sizeLabel, sessionDate } from '/parts.mjs';
|
|
5
|
+
import { sizeLabel, sessionDate, isAt } from '/parts.mjs';
|
|
6
6
|
import { selectMemory } from '/store.mjs';
|
|
7
7
|
import { paint } from '/bus.mjs';
|
|
8
8
|
|
|
@@ -170,7 +170,7 @@ export async function renderSessions(container) {
|
|
|
170
170
|
container.textContent = '';
|
|
171
171
|
return container.append(node('p', { class: ui.note, text: err.message }));
|
|
172
172
|
}
|
|
173
|
-
if (
|
|
173
|
+
if (!isAt('environment', 'sessions')) return;
|
|
174
174
|
state.aux.sessions = data;
|
|
175
175
|
}
|
|
176
176
|
container.textContent = '';
|
|
@@ -2,6 +2,7 @@ import * as ui from '/ui.mjs';
|
|
|
2
2
|
import { node } from '/dom.mjs';
|
|
3
3
|
import { api } from '/api.mjs';
|
|
4
4
|
import { state } from '/state.mjs';
|
|
5
|
+
import { isAt } from '/parts.mjs';
|
|
5
6
|
|
|
6
7
|
const SETTINGS_PROBLEMS = {
|
|
7
8
|
unparseable: (p) => [
|
|
@@ -34,7 +35,7 @@ export async function renderSettings(container) {
|
|
|
34
35
|
container.textContent = '';
|
|
35
36
|
return container.append(node('p', { class: ui.note, text: err.message }));
|
|
36
37
|
}
|
|
37
|
-
if (
|
|
38
|
+
if (!isAt('environment', 'settings')) return;
|
|
38
39
|
}
|
|
39
40
|
container.textContent = '';
|
|
40
41
|
|
package/public/views/stores.mjs
CHANGED
|
@@ -13,9 +13,9 @@ function storeSubtitle(store) {
|
|
|
13
13
|
|
|
14
14
|
function issueTitle(store) {
|
|
15
15
|
const parts = [];
|
|
16
|
-
if (store.issueCount) parts.push(`${store.issueCount} in
|
|
17
|
-
|
|
18
|
-
if (
|
|
16
|
+
if (store.issueCount) parts.push(`${store.issueCount} to fix in Cleanup`);
|
|
17
|
+
const environment = (store.context || 0) + (store.settings || 0);
|
|
18
|
+
if (environment) parts.push(`${environment} in Environment`);
|
|
19
19
|
if (store.kind === 'auto' && store.resolvedBy === 'unresolved') {
|
|
20
20
|
parts.push('Real path could not be resolved - showing the raw folder name');
|
|
21
21
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as ui from '/ui.mjs';
|
|
2
|
+
import { node } from '/dom.mjs';
|
|
3
|
+
import { api } from '/api.mjs';
|
|
4
|
+
import { state } from '/state.mjs';
|
|
5
|
+
import { isAt } from '/parts.mjs';
|
|
6
|
+
import { paint } from '/bus.mjs';
|
|
7
|
+
import { renderRtk } from '/views/rtk.mjs';
|
|
8
|
+
|
|
9
|
+
const OPT_IN = 'toolsOptIn';
|
|
10
|
+
|
|
11
|
+
const installedRtk = () => state.tools.find((tool) => tool.id === 'rtk' && tool.found) || null;
|
|
12
|
+
|
|
13
|
+
async function fetchRtk() {
|
|
14
|
+
const id = state.storeId;
|
|
15
|
+
let next;
|
|
16
|
+
try {
|
|
17
|
+
next = { data: await api(`/api/stores/${encodeURIComponent(id)}/tools/rtk`) };
|
|
18
|
+
} catch (err) {
|
|
19
|
+
next = { error: err.message };
|
|
20
|
+
}
|
|
21
|
+
if (state.storeId !== id) return;
|
|
22
|
+
state.aux.rtk = next;
|
|
23
|
+
if (isAt('environment', 'tools')) paint('tab');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function startRtk({ remember = true } = {}) {
|
|
27
|
+
if (remember) localStorage.setItem(OPT_IN, '1');
|
|
28
|
+
state.aux.rtk = { running: true };
|
|
29
|
+
fetchRtk();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readRtk() {
|
|
33
|
+
startRtk();
|
|
34
|
+
paint('tab');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function turnOff() {
|
|
38
|
+
localStorage.removeItem(OPT_IN);
|
|
39
|
+
state.aux.rtk = null;
|
|
40
|
+
paint('tab');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function gate(container, tool) {
|
|
44
|
+
container.append(node('div', { class: ui.issue(false) }, [
|
|
45
|
+
node('div', { class: ui.issueBody }, [
|
|
46
|
+
node('div', { class: ui.issueTitle, text: 'rtk is installed, and is not being read' }),
|
|
47
|
+
node('div', { class: ui.issueDetail, text: `${tool.path} keeps a ledger of what its filters stripped before it reached the model. Reading it runs rtk on this machine, so it stays off until you ask.` }),
|
|
48
|
+
]),
|
|
49
|
+
node('button', { class: ui.buttonSmall, text: 'Read rtk stats', onclick: readRtk }),
|
|
50
|
+
]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function failure(container, message) {
|
|
54
|
+
container.append(node('div', { class: ui.issue(true) }, [
|
|
55
|
+
node('div', { class: ui.issueBody }, [
|
|
56
|
+
node('div', { class: ui.issueTitle, text: 'rtk could not be read' }),
|
|
57
|
+
node('div', { class: ui.issueDetail, text: message }),
|
|
58
|
+
]),
|
|
59
|
+
node('button', { class: ui.buttonSmall, text: 'Turn off', onclick: turnOff }),
|
|
60
|
+
node('button', { class: ui.buttonSmall, text: 'Try again', onclick: readRtk }),
|
|
61
|
+
]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function renderTools(container) {
|
|
65
|
+
const tool = installedRtk();
|
|
66
|
+
if (!tool) return;
|
|
67
|
+
|
|
68
|
+
if (!state.aux.rtk && localStorage.getItem(OPT_IN) === '1') startRtk({ remember: false });
|
|
69
|
+
|
|
70
|
+
const report = state.aux.rtk;
|
|
71
|
+
if (!report) return gate(container, tool);
|
|
72
|
+
if (report.running) return container.append(node('p', { class: ui.note, text: 'Reading the rtk ledger…' }));
|
|
73
|
+
if (report.error) return failure(container, report.error);
|
|
74
|
+
|
|
75
|
+
renderRtk(container, report.data, { reread: readRtk, turnOff });
|
|
76
|
+
}
|