claude-memory-admin 1.4.0 → 1.5.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 +26 -3
- package/package.json +1 -1
- package/public/app.mjs +245 -3
- package/public/styles.css +1 -1
- package/public/ui.mjs +15 -0
- package/server.mjs +9 -0
- package/src/checks.mjs +58 -0
- package/src/model.mjs +40 -1
- package/src/projects.mjs +6 -2
- package/src/sessions.mjs +174 -0
package/README.md
CHANGED
|
@@ -80,7 +80,9 @@ project is to the cliff, and makes it quick to get back under it.
|
|
|
80
80
|
session transcripts on a retention period but never touches `memory/`, so a
|
|
81
81
|
project eventually loses the only proof of what it was called. **Remember
|
|
82
82
|
path** records one you confirm, and is the single thing this app writes
|
|
83
|
-
outside a `memory/` directory.
|
|
83
|
+
outside a `memory/` directory. It is now offered *before* that happens: the
|
|
84
|
+
project header counts down the days until the last transcript naming the
|
|
85
|
+
project is swept, rather than waiting until the name is already gone.
|
|
84
86
|
- **Where the store is** is read the way Claude Code reads it: `autoMemoryDirectory`
|
|
85
87
|
from any settings layer, managed policy through project and local, not just
|
|
86
88
|
`~/.claude/settings.json`. A value that is neither absolute nor `~/`-prefixed
|
|
@@ -96,13 +98,28 @@ project is to the cliff, and makes it quick to get back under it.
|
|
|
96
98
|
- **Prune** (see below).
|
|
97
99
|
- **Graph** the wikilinks between memories. Hovering dims everything that is not
|
|
98
100
|
a neighbour, which is the only practical way to read a dense cluster.
|
|
101
|
+
- **Where each memory came from.** Claude Code stamps `originSessionId` into a
|
|
102
|
+
memory's frontmatter, and the transcript it names sits next to the store until
|
|
103
|
+
the sweep takes it. The memory reads *written in "Release process notes", on
|
|
104
|
+
`main`* while that transcript is there, and says so plainly once it is gone:
|
|
105
|
+
a swept id is struck through in red, like a dead wikilink, because why the
|
|
106
|
+
memory exists can no longer be traced from anything on disk.
|
|
107
|
+
- **Sessions**: the transcripts beside a store, with the retention window drawn
|
|
108
|
+
the way MEMORY.md's cutoff is - each session a tick, the sweep line where it
|
|
109
|
+
falls. Titles are the ones Claude Code generated, falling back to the session
|
|
110
|
+
slug and then the opening prompt; a session that names itself nowhere in the
|
|
111
|
+
part read is shown by id rather than given an invented name. Only the head of
|
|
112
|
+
each file is ever read, so a 200MB store of transcripts costs a quarter of a
|
|
113
|
+
second, and nothing on this tab deletes one.
|
|
99
114
|
- **Health**: orphans, dangling pointers, broken wikilinks, files linked only
|
|
100
115
|
mid-sentence, `name` fields that disagree with the filename, two files claiming
|
|
101
116
|
one `name`, a file bulleted twice, a blank `description`, a `type` outside the
|
|
102
117
|
four documented ones, a memory that is frontmatter and little else, a hook that
|
|
103
118
|
only restates the description it points at, a heading with nothing under it, and
|
|
104
|
-
entries that spill onto a second line
|
|
105
|
-
|
|
119
|
+
entries that spill onto a second line, a memory whose origin transcript has
|
|
120
|
+
been swept, a project whose last proof of its own path is about to be, and
|
|
121
|
+
sessions that produced no memory at all while auto memory was on. An orphan
|
|
122
|
+
can be given the `MEMORY.md` bullet it is missing without leaving the page.
|
|
106
123
|
- **Every count is visible before you click.** Each tab carries a badge, coloured
|
|
107
124
|
amber for something to tidy and red for something actively broken, and the
|
|
108
125
|
sidebar gives each store a dot of its worst severity - memory, instructions and
|
|
@@ -312,6 +329,11 @@ claude-memory-admin --root /tmp/memory-snapshot
|
|
|
312
329
|
chain, unless you switch on the path check above, which then also walks that one
|
|
313
330
|
project's directory. It only ever reads: no path a memory names is opened, only
|
|
314
331
|
looked up in an index built from the project itself.
|
|
332
|
+
- Session transcripts are read at the head only, 16KB and then at most 128KB, and
|
|
333
|
+
never deleted or rewritten. A transcript here reaches 13MB and the app has no
|
|
334
|
+
reason to hold one in memory. A session id read out of frontmatter is matched
|
|
335
|
+
against a strict id shape before it is joined to a path, and only ever looked
|
|
336
|
+
for in the store's own directory.
|
|
315
337
|
- Every write target must resolve to a plain `.md` file inside that project's own
|
|
316
338
|
`memory/` directory. `..`, absolute paths and subdirectories are refused.
|
|
317
339
|
- One exception, and only when you ask for it: **Remember path** writes
|
|
@@ -355,6 +377,7 @@ ready to serve and never builds anything. After editing the source, run
|
|
|
355
377
|
| `src/projects.mjs` | Project discovery, slug → real path resolution |
|
|
356
378
|
| `src/settings.mjs` | Layered reads of Claude Code's settings files, and the report behind the Settings tab |
|
|
357
379
|
| `src/pathcache.mjs` | The opt-in record of confirmed project paths |
|
|
380
|
+
| `src/sessions.mjs` | Session transcripts: bounded head reads, retention, provenance |
|
|
358
381
|
| `src/stores.mjs` | Store discovery: the user scope, auto memory and the three agent scopes |
|
|
359
382
|
| `src/instructions.mjs` | CLAUDE.md chain, `@` imports and rules resolution |
|
|
360
383
|
| `src/parse.mjs` | `MEMORY.md` and frontmatter parsers, wikilinks |
|
package/package.json
CHANGED
package/public/app.mjs
CHANGED
|
@@ -17,7 +17,7 @@ const state = {
|
|
|
17
17
|
pruneSort: 'oldest',
|
|
18
18
|
pruneSelection: new Set(),
|
|
19
19
|
indexView: localStorage.getItem('memoryIndexView') === 'source' ? 'source' : 'rendered',
|
|
20
|
-
aux: { instructions: null, settings: null },
|
|
20
|
+
aux: { instructions: null, settings: null, sessions: null },
|
|
21
21
|
pathCheck: null,
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -153,6 +153,7 @@ const TABS = [
|
|
|
153
153
|
{ id: 'graph', label: 'Graph' },
|
|
154
154
|
{ id: 'prune', label: 'Prune' },
|
|
155
155
|
{ id: 'context', label: 'Context' },
|
|
156
|
+
{ id: 'sessions', label: 'Sessions' },
|
|
156
157
|
{ id: 'health', label: 'Health' },
|
|
157
158
|
{ id: 'settings', label: 'Settings' },
|
|
158
159
|
{ id: 'trash', label: 'Trash' },
|
|
@@ -169,6 +170,7 @@ function renderTabs() {
|
|
|
169
170
|
for (const tab of TABS) {
|
|
170
171
|
if (global && tab.id !== 'context') continue;
|
|
171
172
|
if (!global && tab.id === 'context' && !hasProjectDir) continue;
|
|
173
|
+
if (tab.id === 'sessions' && !state.store.sessions?.count) continue;
|
|
172
174
|
let badge = null;
|
|
173
175
|
let tone = 'neutral';
|
|
174
176
|
if (tab.id === 'memories') badge = String(state.store.memories.length);
|
|
@@ -181,6 +183,10 @@ function renderTabs() {
|
|
|
181
183
|
badge = '!';
|
|
182
184
|
tone = state.store.stats.index.level === 'over' ? 'bad' : 'warn';
|
|
183
185
|
}
|
|
186
|
+
if (tab.id === 'sessions' && state.store.sessions) {
|
|
187
|
+
badge = String(state.store.sessions.count);
|
|
188
|
+
if (state.store.sessions.expiringCount) tone = 'warn';
|
|
189
|
+
}
|
|
184
190
|
if (tab.id === 'context' && state.aux.instructions?.problems.length) {
|
|
185
191
|
badge = String(state.aux.instructions.problems.length);
|
|
186
192
|
tone = worstSeverity(state.aux.instructions.problems);
|
|
@@ -278,13 +284,16 @@ function renderDetail() {
|
|
|
278
284
|
];
|
|
279
285
|
if (!memory.nameMatchesFile) rows.push(['name', `${memory.name} (differs from filename)`]);
|
|
280
286
|
for (const [key, value] of Object.entries(memory.metadata)) {
|
|
281
|
-
if (key !== 'type' && key !== 'modified') rows.push([key, value]);
|
|
287
|
+
if (key !== 'type' && key !== 'modified' && key !== 'originSessionId') rows.push([key, value]);
|
|
282
288
|
}
|
|
283
289
|
for (const [key, value] of rows) {
|
|
284
290
|
meta.append(node('dt', { class: ui.metaKey, text: key }), node('dd', { class: ui.metaValue, text: String(value) }));
|
|
285
291
|
}
|
|
286
292
|
card.append(meta);
|
|
287
293
|
|
|
294
|
+
const origin = provenanceLine(memory);
|
|
295
|
+
if (origin) card.append(origin);
|
|
296
|
+
|
|
288
297
|
const body = node('div', { class: ui.prose });
|
|
289
298
|
card.append(body);
|
|
290
299
|
renderBody(body, memory, state.store);
|
|
@@ -863,6 +872,32 @@ function renderIssue(item, memories) {
|
|
|
863
872
|
);
|
|
864
873
|
}
|
|
865
874
|
|
|
875
|
+
if (item.kind === 'provenance-expired') {
|
|
876
|
+
return issue(
|
|
877
|
+
`Provenance gone: ${item.name}`,
|
|
878
|
+
`the session it was written in (${item.sessionId}) has been swept, so why this memory exists can no longer be traced. Nothing recovers it - judge the memory on what it says.`,
|
|
879
|
+
{ bad, action: { label: 'Open', run: () => selectMemory(item.file) } },
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (item.kind === 'path-evidence-expiring') {
|
|
884
|
+
return issue(
|
|
885
|
+
item.days === 0
|
|
886
|
+
? 'The last transcript naming this project is due to be swept'
|
|
887
|
+
: `The last transcript naming this project is swept in ${item.days} days`,
|
|
888
|
+
`${item.path} is recovered from the session transcripts, and Claude Code drops those after ${item.retentionDays} days while never touching memory/. Once the last one goes this store shows its raw folder name instead. Remember path records it now.`,
|
|
889
|
+
{ bad, action: { label: 'Remember path\u2026', run: openRememberPathDialog } },
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (item.kind === 'no-memory-despite-sessions') {
|
|
894
|
+
return issue(
|
|
895
|
+
`${item.sessionCount} ${item.sessionCount === 1 ? 'session' : 'sessions'}, and no memory written`,
|
|
896
|
+
`auto memory is on for this project, but nothing has been saved in the ${item.retentionDays} days of transcripts kept here. Either nothing was worth remembering, or memory is not being written.`,
|
|
897
|
+
{ bad },
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
866
901
|
return issue(item.kind, JSON.stringify(item));
|
|
867
902
|
}
|
|
868
903
|
|
|
@@ -887,6 +922,157 @@ const SETTINGS_PROBLEMS = {
|
|
|
887
922
|
|
|
888
923
|
const show = (value) => (value === undefined ? 'unset' : JSON.stringify(value));
|
|
889
924
|
|
|
925
|
+
const TITLE_SOURCE = {
|
|
926
|
+
'ai-title': 'title Claude Code generated for this session',
|
|
927
|
+
slug: 'session slug, no generated title found in the part read',
|
|
928
|
+
prompt: 'first prompt, no generated title found in the part read',
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const sizeLabel = (bytes) => (bytes >= 1048576
|
|
932
|
+
? `${(bytes / 1048576).toFixed(1)} MB`
|
|
933
|
+
: `${(bytes / 1024).toFixed(1)} KB`);
|
|
934
|
+
|
|
935
|
+
function sessionDate(iso) {
|
|
936
|
+
return iso ? iso.replace('T', ' ').replace(/\..*$/, '') : 'unknown';
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function sessionRow(session, memoriesFrom) {
|
|
940
|
+
const soon = session.expiresInDays !== null && session.expiresInDays <= 7;
|
|
941
|
+
const caret = node('span', { class: ui.contextCaret, text: '\u25b8' });
|
|
942
|
+
|
|
943
|
+
const tags = [];
|
|
944
|
+
if (session.gitBranch) tags.push(node('span', { class: ui.badge('neutral'), text: session.gitBranch }));
|
|
945
|
+
if (session.model) tags.push(node('span', { class: ui.badge('neutral'), text: session.model }));
|
|
946
|
+
if (session.expiresInDays !== null) {
|
|
947
|
+
tags.push(node('span', {
|
|
948
|
+
class: ui.badge(soon ? 'warn' : 'neutral'),
|
|
949
|
+
text: session.expiresInDays === 0 ? 'due to be swept' : `swept in ${session.expiresInDays}d`,
|
|
950
|
+
}));
|
|
951
|
+
}
|
|
952
|
+
for (const memory of memoriesFrom) {
|
|
953
|
+
tags.push(node('span', { class: ui.badge('project'), text: memory.name }));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const body = node('div', { class: ui.contextBody, hidden: true });
|
|
957
|
+
const facts = node('dl', { class: ui.metaList });
|
|
958
|
+
const rows = [
|
|
959
|
+
['session', session.id],
|
|
960
|
+
['last active', `${sessionDate(session.modified)} (file mtime)`],
|
|
961
|
+
['size', sizeLabel(session.bytes)],
|
|
962
|
+
];
|
|
963
|
+
if (session.cwd) rows.push(['cwd', session.cwd]);
|
|
964
|
+
if (session.version) rows.push(['version', session.version]);
|
|
965
|
+
if (session.titleFrom) rows.push(['title from', TITLE_SOURCE[session.titleFrom] || session.titleFrom]);
|
|
966
|
+
for (const [key, value] of rows) {
|
|
967
|
+
facts.append(node('dt', { class: ui.metaKey, text: key }), node('dd', { class: ui.metaValue, text: value }));
|
|
968
|
+
}
|
|
969
|
+
body.append(facts);
|
|
970
|
+
if (session.prompt) {
|
|
971
|
+
body.append(node('div', { class: ui.sectionLabel, text: 'Opened with' }));
|
|
972
|
+
body.append(node('p', { class: ui.note, text: session.prompt }));
|
|
973
|
+
}
|
|
974
|
+
if (memoriesFrom.length) {
|
|
975
|
+
body.append(node('div', { class: ui.sectionLabel, text: 'Memories written here' }));
|
|
976
|
+
const chips = node('div', { class: ui.linkRow });
|
|
977
|
+
for (const memory of memoriesFrom) {
|
|
978
|
+
chips.append(node('button', {
|
|
979
|
+
class: ui.chip(true),
|
|
980
|
+
text: memory.name,
|
|
981
|
+
onclick: () => selectMemory(memory.file),
|
|
982
|
+
}));
|
|
983
|
+
}
|
|
984
|
+
body.append(chips);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const button = node('button', {
|
|
988
|
+
class: ui.contextRowButton,
|
|
989
|
+
'aria-expanded': 'false',
|
|
990
|
+
onclick: (event) => {
|
|
991
|
+
const open = body.hidden;
|
|
992
|
+
body.hidden = !open;
|
|
993
|
+
caret.textContent = open ? '\u25be' : '\u25b8';
|
|
994
|
+
event.currentTarget.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
995
|
+
},
|
|
996
|
+
}, [
|
|
997
|
+
node('div', { class: ui.contextMain }, [
|
|
998
|
+
node('div', { class: ui.contextTags }, tags),
|
|
999
|
+
node('div', { class: ui.contextFile }, [caret, document.createTextNode(session.title || session.id)]),
|
|
1000
|
+
]),
|
|
1001
|
+
node('div', { class: ui.contextSize, text: sessionDate(session.modified).slice(0, 10) }),
|
|
1002
|
+
]);
|
|
1003
|
+
|
|
1004
|
+
return node('div', {}, [button, body]);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function retentionTrack(sessions, days) {
|
|
1008
|
+
const track = node('div', { class: ui.retentionTrack }, [node('span', { class: ui.retentionSweep })]);
|
|
1009
|
+
for (const session of sessions) {
|
|
1010
|
+
if (session.expiresInDays === null) continue;
|
|
1011
|
+
const share = days > 0 ? Math.min(1, session.expiresInDays / days) : 1;
|
|
1012
|
+
track.append(node('span', {
|
|
1013
|
+
class: ui.retentionTick(session.expiresInDays <= 7),
|
|
1014
|
+
style: `left: ${(share * 100).toFixed(2)}%`,
|
|
1015
|
+
title: `${session.title || session.id}\nswept in ${session.expiresInDays} days`,
|
|
1016
|
+
}));
|
|
1017
|
+
}
|
|
1018
|
+
return node('div', {}, [
|
|
1019
|
+
track,
|
|
1020
|
+
node('div', { class: ui.retentionScale }, [
|
|
1021
|
+
node('span', { text: 'swept' }),
|
|
1022
|
+
node('span', { text: `today · kept ${days} days` }),
|
|
1023
|
+
]),
|
|
1024
|
+
]);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function renderSessions(container) {
|
|
1028
|
+
let data = state.aux.sessions;
|
|
1029
|
+
if (!data) {
|
|
1030
|
+
container.append(node('p', { class: ui.note, text: 'Reading session transcripts\u2026' }));
|
|
1031
|
+
try {
|
|
1032
|
+
data = await api(`/api/stores/${encodeURIComponent(state.storeId)}/sessions`);
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
container.textContent = '';
|
|
1035
|
+
return container.append(node('p', { class: ui.note, text: err.message }));
|
|
1036
|
+
}
|
|
1037
|
+
if (state.tab !== 'sessions') return;
|
|
1038
|
+
state.aux.sessions = data;
|
|
1039
|
+
}
|
|
1040
|
+
container.textContent = '';
|
|
1041
|
+
|
|
1042
|
+
const byOrigin = new Map();
|
|
1043
|
+
for (const memory of state.store.memories) {
|
|
1044
|
+
if (!memory.origin?.present) continue;
|
|
1045
|
+
if (!byOrigin.has(memory.origin.sessionId)) byOrigin.set(memory.origin.sessionId, []);
|
|
1046
|
+
byOrigin.get(memory.origin.sessionId).push(memory);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
container.append(node('p', { class: ui.noteTight, text: 'The transcripts sitting beside this store\u2019s memory. Claude Code deletes them once they pass the retention period and never touches memory/, so this is the evidence that expires while the memories stay. This tab only reads, and only the head of each file - nothing here deletes a transcript.' }));
|
|
1050
|
+
|
|
1051
|
+
container.append(node('div', { class: ui.meter }, [
|
|
1052
|
+
node('div', { class: ui.meterTop }, [
|
|
1053
|
+
node('span', { class: ui.meterValue, text: String(data.count) }),
|
|
1054
|
+
node('span', { class: ui.meterUnit, text: data.count === 1 ? 'session kept' : 'sessions kept' }),
|
|
1055
|
+
]),
|
|
1056
|
+
retentionTrack(data.sessions, data.days),
|
|
1057
|
+
node('div', { class: ui.meterFacts }, [
|
|
1058
|
+
node('span', {}, [node('b', { class: ui.meterFactValue, text: sizeLabel(data.bytes) }), document.createTextNode(' on disk')]),
|
|
1059
|
+
node('span', {}, [node('b', { class: ui.meterFactValue, text: `${data.days} days` }), document.createTextNode(' retention (cleanupPeriodDays)')]),
|
|
1060
|
+
node('span', {}, [
|
|
1061
|
+
node('b', { class: ui.meterFactValue, text: data.evidenceExpiresInDays === null ? '-' : `${data.evidenceExpiresInDays} days` }),
|
|
1062
|
+
document.createTextNode(' until the last proof of this path goes'),
|
|
1063
|
+
]),
|
|
1064
|
+
]),
|
|
1065
|
+
node('p', { class: ui.meterNote, text: 'Sweep dates come from each file\u2019s mtime, which any copy or restore resets, so they are an estimate of when Claude Code will drop them rather than something the transcript recorded about itself.' }),
|
|
1066
|
+
]));
|
|
1067
|
+
|
|
1068
|
+
container.append(node('div', { class: ui.sectionLabel, text: `Sessions \u00b7 ${data.count}` }));
|
|
1069
|
+
const card = node('div', { class: ui.card });
|
|
1070
|
+
for (const session of data.sessions) {
|
|
1071
|
+
card.append(sessionRow(session, byOrigin.get(session.id) || []));
|
|
1072
|
+
}
|
|
1073
|
+
container.append(card);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
890
1076
|
async function renderSettings(container) {
|
|
891
1077
|
let data = state.aux.settings;
|
|
892
1078
|
if (!data) {
|
|
@@ -1935,6 +2121,7 @@ function renderTab() {
|
|
|
1935
2121
|
if (state.tab === 'memories') renderMemories(container);
|
|
1936
2122
|
else if (state.tab === 'prune') renderPrune(container);
|
|
1937
2123
|
else if (state.tab === 'context') renderContext(container);
|
|
2124
|
+
else if (state.tab === 'sessions') renderSessions(container);
|
|
1938
2125
|
else if (state.tab === 'index') renderIndex(container);
|
|
1939
2126
|
else if (state.tab === 'health') renderHealth(container);
|
|
1940
2127
|
else if (state.tab === 'settings') renderSettings(container);
|
|
@@ -1987,6 +2174,49 @@ async function sweepIssues() {
|
|
|
1987
2174
|
renderStores();
|
|
1988
2175
|
}
|
|
1989
2176
|
|
|
2177
|
+
function provenanceLine(memory) {
|
|
2178
|
+
const origin = memory.origin;
|
|
2179
|
+
if (!origin) return null;
|
|
2180
|
+
|
|
2181
|
+
const known = state.aux.sessions?.sessions.find((session) => session.id === origin.sessionId);
|
|
2182
|
+
const label = origin.present ? (known?.title || origin.sessionId) : origin.sessionId;
|
|
2183
|
+
|
|
2184
|
+
const row = node('div', { class: ui.provenanceRow }, [node('span', { text: 'written in' })]);
|
|
2185
|
+
|
|
2186
|
+
if (origin.present) {
|
|
2187
|
+
row.append(node('button', {
|
|
2188
|
+
class: ui.provenanceLink(true),
|
|
2189
|
+
text: label,
|
|
2190
|
+
title: 'Open the Sessions tab',
|
|
2191
|
+
onclick: () => goToTab('sessions'),
|
|
2192
|
+
}));
|
|
2193
|
+
if (known?.gitBranch) row.append(node('span', { text: `on ${known.gitBranch}` }));
|
|
2194
|
+
if (origin.modified) row.append(node('span', { text: sessionDate(origin.modified).slice(0, 10) }));
|
|
2195
|
+
if (!known) loadSessionsForProvenance();
|
|
2196
|
+
} else {
|
|
2197
|
+
row.append(node('span', {
|
|
2198
|
+
class: ui.provenanceLink(false),
|
|
2199
|
+
text: label,
|
|
2200
|
+
title: 'This transcript is no longer on disk',
|
|
2201
|
+
}));
|
|
2202
|
+
row.append(node('span', { text: '- transcript swept, so why this memory exists can no longer be traced' }));
|
|
2203
|
+
}
|
|
2204
|
+
return row;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
async function loadSessionsForProvenance() {
|
|
2208
|
+
if (state.aux.sessions || !state.store?.sessions?.count) return;
|
|
2209
|
+
const id = state.storeId;
|
|
2210
|
+
try {
|
|
2211
|
+
const data = await api(`/api/stores/${encodeURIComponent(id)}/sessions`);
|
|
2212
|
+
if (state.storeId !== id) return;
|
|
2213
|
+
state.aux.sessions = data;
|
|
2214
|
+
renderTab();
|
|
2215
|
+
} catch {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
1990
2220
|
function renderStoreHeader() {
|
|
1991
2221
|
const store = state.store;
|
|
1992
2222
|
el('project-title').textContent = store.label;
|
|
@@ -2029,6 +2259,18 @@ function renderStoreHeader() {
|
|
|
2029
2259
|
]));
|
|
2030
2260
|
}
|
|
2031
2261
|
|
|
2262
|
+
const evidence = store.sessions?.evidenceExpiresInDays;
|
|
2263
|
+
if (!store.sessions?.remembered
|
|
2264
|
+
&& (store.resolvedBy === 'transcript' || store.resolvedBy === 'repo-root')
|
|
2265
|
+
&& typeof evidence === 'number' && evidence <= 14) {
|
|
2266
|
+
sub.append(node('span', { class: ui.subWarn }, [
|
|
2267
|
+
document.createTextNode(evidence === 0
|
|
2268
|
+
? 'the last transcript proving this path is due to be swept - the store keeps its memories and loses its name '
|
|
2269
|
+
: `the last transcript proving this path is swept in ${evidence} days, and nothing else records it `),
|
|
2270
|
+
node('button', { class: ui.linkButton, text: 'Remember path\u2026', onclick: openRememberPathDialog }),
|
|
2271
|
+
]));
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2032
2274
|
const auto = store.autoMemory;
|
|
2033
2275
|
if (auto && auto.known && !auto.enabled) {
|
|
2034
2276
|
sub.append(node('span', { class: ui.subWarn, text: auto.scope === 'env'
|
|
@@ -2040,7 +2282,7 @@ function renderStoreHeader() {
|
|
|
2040
2282
|
async function openStore(id, { keepTab = false } = {}) {
|
|
2041
2283
|
if (id !== state.storeId) state.pruneSelection.clear();
|
|
2042
2284
|
state.storeId = id;
|
|
2043
|
-
state.aux = { instructions: null, settings: null };
|
|
2285
|
+
state.aux = { instructions: null, settings: null, sessions: null };
|
|
2044
2286
|
state.pathCheck = null;
|
|
2045
2287
|
if (!keepTab) {
|
|
2046
2288
|
const opening = state.stores.find((store) => store.id === id);
|
package/public/styles.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-content:"";--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;--font-mono:ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--color-red-50:oklch(97.1% .013 17.4);--color-red-300:oklch(80.8% .114 19.6);--color-red-400:oklch(70.4% .16 22.2);--color-red-500:oklch(63.7% .208 25.3);--color-red-600:oklch(57.7% .215 27.3);--color-red-950:oklch(25.8% .092 26);--color-amber-50:oklch(97.7% .021 84.6);--color-amber-300:oklch(87.9% .153 91.6);--color-amber-400:oklch(82.8% .171 84.4);--color-amber-600:oklch(66.6% .15 58.3);--color-amber-700:oklch(55.5% .13 49);--color-amber-950:oklch(27.9% .077 45.6);--color-green-50:oklch(98.2% .018 155.8);--color-green-300:oklch(87.1% .12 154.4);--color-green-400:oklch(79.2% .15 151.7);--color-green-600:oklch(58.5% .145 149.2);--color-green-700:oklch(52.7% .132 150.1);--color-green-950:oklch(26.6% .065 152.9);--color-blue-50:oklch(97% .014 253.3);--color-blue-200:oklch(88% .06 253.3);--color-blue-300:oklch(78.4% .124 239.1);--color-blue-400:oklch(75.2% .138 243.7);--color-blue-600:oklch(56.5% .163 253.3);--color-blue-700:oklch(48.5% .15 253.3);--color-blue-800:oklch(41% .125 253.3);--color-blue-950:oklch(24% .07 253.3);--color-violet-50:oklch(96.9% .016 300);--color-violet-300:oklch(82.7% .104 300);--color-violet-700:oklch(49.6% .19 300);--color-violet-950:oklch(28.3% .109 300);--color-slate-50:oklch(98.4% .003 253.3);--color-slate-100:oklch(96.4% .005 253.3);--color-slate-200:oklch(92.6% .009 253.3);--color-slate-300:oklch(86.6% .014 253.3);--color-slate-400:oklch(70.4% .021 253.3);--color-slate-500:oklch(55.6% .026 253.3);--color-slate-600:oklch(44.8% .028 253.3);--color-slate-700:oklch(36.8% .028 253.3);--color-slate-800:oklch(26.4% .026 253.3);--color-slate-900:oklch(20.4% .024 253.3);--color-slate-950:oklch(14.2% .022 253.3);--spacing:.25rem;--container-2xl:42rem;--text-xs:.6875rem;--text-xs--line-height:1.45;--text-sm:.8125rem;--text-sm--line-height:1.5;--text-base:.875rem;--text-base--line-height:1.6;--text-lg:1rem;--text-lg--line-height:1.5;--text-xl:1.25rem;--text-xl--line-height:1.3;--text-2xl:1.5rem;--text-2xl--line-height:1.2;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.12em;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.12s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-slate-0:oklch(100% 0 253.3);--spacing-control-x-sm:calc(var(--spacing) * 2);--spacing-control-y-sm:calc(var(--spacing) * 1);--spacing-control-x-md:calc(var(--spacing) * 3);--spacing-control-y-md:calc(var(--spacing) * 2);--spacing-control-x-lg:calc(var(--spacing) * 3.5);--spacing-control-y-lg:calc(var(--spacing) * 2.5);--spacing-control-h-sm:calc(var(--spacing) * 6.5);--spacing-control-h-md:calc(var(--spacing) * 9.5);--spacing-control-h-lg:calc(var(--spacing) * 9.5);--text-2xs:.625rem;--text-2xs--line-height:1.4;--tracking-tightest:-.03em}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{color-scheme:light}:root[data-theme=dark]{color-scheme:dark}html,body{height:100%}body{background-color:var(--ui-canvas);color:var(--ui-fg);font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--text-base--line-height)}::selection{background-color:var(--ui-accent-surface);color:var(--ui-accent)}:focus-visible{outline:2px solid var(--ui-accent);outline-offset:1px}}@layer components{.prose{overflow-wrap:anywhere}.prose>:first-child{margin-top:0}.prose>:last-child{margin-bottom:0}.prose h1,.prose h2,.prose h3,.prose h4{margin:calc(var(--spacing) * 6) 0 calc(var(--spacing) * 2);letter-spacing:var(--tracking-tightest);font-weight:650;line-height:1.25}.prose h1{font-size:var(--text-lg)}.prose h2{font-size:var(--text-base);padding-bottom:calc(var(--spacing) * 1.5);border-bottom:1px solid var(--ui-line)}.prose h3,.prose h4{font-size:var(--text-base);color:var(--ui-fg-muted)}.prose p{margin:calc(var(--spacing) * 3) 0}.prose ul,.prose ol{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 5)}.prose ul{list-style-type:"– "}.prose ol{list-style-type:decimal}.prose li{margin:calc(var(--spacing) * 1) 0}.prose li::marker{color:var(--ui-accent);font-family:var(--font-mono)}.prose li>ul,.prose li>ol{margin:calc(var(--spacing) * 1) 0}.prose a{color:var(--ui-accent);text-underline-offset:2px;text-decoration:underline}.prose a:hover{color:var(--ui-accent-hover)}.prose strong{color:var(--ui-fg);font-weight:650}.prose pre{margin:calc(var(--spacing) * 3) 0;padding:calc(var(--spacing) * 3);border:1px solid var(--ui-line);border-radius:var(--radius-md);background-color:var(--ui-surface-sunken);font-size:var(--text-xs);line-height:1.6;overflow-x:auto}.prose code{font-family:var(--font-mono);font-size:.9em}.prose :not(pre)>code{border:1px solid var(--ui-line);border-radius:var(--radius-sm);background-color:var(--ui-surface-sunken);color:var(--ui-accent);padding:1px 4px}.prose blockquote{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 3);border-left:2px solid var(--ui-accent-line);color:var(--ui-fg-muted)}.prose hr{margin:calc(var(--spacing) * 5) 0;border:0;border-top:1px dashed var(--ui-line-strong)}.prose table{margin:calc(var(--spacing) * 3) 0;border-collapse:collapse;font-size:var(--text-sm);display:block;overflow-x:auto}.prose th,.prose td{padding:calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5);border:1px solid var(--ui-line);text-align:left}.prose th{background-color:var(--ui-surface-sunken);font-weight:600}.graph svg{cursor:grab;width:100%;display:block}.graph .node circle{cursor:pointer;stroke-width:1.5px}.graph .node circle.hit{fill:#0000;stroke:none}.graph .node text{fill:var(--ui-fg-muted);font-family:var(--font-mono);pointer-events:none;font-size:9px}.graph .node.hi circle{stroke:var(--ui-accent);stroke-width:2.5px}.graph .node.hi text{fill:var(--ui-fg);font-weight:600}.graph .node.dim{opacity:.18}.graph .node.dim text{display:none}.graph .edge{stroke:var(--ui-line-strong)}.graph .edge.dim{opacity:.05}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.top-3{top:calc(var(--spacing) * 3)}.top-3\.5{top:calc(var(--spacing) * 3.5)}.right-3\.5{right:calc(var(--spacing) * 3.5)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-5{bottom:calc(var(--spacing) * 5)}.left-0{left:0}.left-1\/2{left:50%}.left-3\.5{left:calc(var(--spacing) * 3.5)}.z-10{z-index:10}.z-20{z-index:20}.z-100{z-index:100}.z-200{z-index:200}.float-right{float:right}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.my-1{margin-block:var(--spacing)}.my-2\.5{margin-block:calc(var(--spacing) * 2.5)}.mt-0{margin-top:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.h-2{height:calc(var(--spacing) * 2)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[28rem\]{max-height:28rem}.max-h-\[82vh\]{max-height:82vh}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-control-h-lg{min-height:var(--spacing-control-h-lg)}.min-h-control-h-md{min-height:var(--spacing-control-h-md)}.min-h-control-h-sm{min-height:var(--spacing-control-h-sm)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.min-w-0{min-width:0}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-control-h-sm{min-width:var(--spacing-control-h-sm)}.flex-1{flex:1}.flex-none{flex:none}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[3rem_1fr\]{grid-template-columns:3rem 1fr}.grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-px{gap:1px}.gap-x-3\.5{column-gap:calc(var(--spacing) * 3.5)}.gap-y-1{row-gap:var(--spacing)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-none{--tw-border-style:none;border-style:none}.border-accent{border-color:var(--ui-accent)}.border-accent-line{border-color:var(--ui-accent-line)}.border-current{border-color:currentColor}.border-danger-line{border-color:var(--ui-danger-line)}.border-line{border-color:var(--ui-line)}.border-transparent{border-color:#0000}.border-l-danger{border-left-color:var(--ui-danger)}.border-l-warn{border-left-color:var(--ui-warn)}.bg-accent{background-color:var(--ui-accent)}.bg-accent-surface{background-color:var(--ui-accent-surface)}.bg-canvas{background-color:var(--ui-canvas)}.bg-danger{background-color:var(--ui-danger)}.bg-danger-surface{background-color:var(--ui-danger-surface)}.bg-fg{background-color:var(--ui-fg)}.bg-line-strong{background-color:var(--ui-line-strong)}.bg-ok{background-color:var(--ui-ok)}.bg-ok-surface{background-color:var(--ui-ok-surface)}.bg-scope-local-surface{background-color:var(--ui-scope-local-surface)}.bg-scope-managed-surface{background-color:var(--ui-scope-managed-surface)}.bg-scope-project-surface{background-color:var(--ui-scope-project-surface)}.bg-scope-user-surface{background-color:var(--ui-scope-user-surface)}.bg-scrim{background-color:var(--ui-scrim)}.bg-surface{background-color:var(--ui-surface)}.bg-surface-sunken{background-color:var(--ui-surface-sunken)}.bg-transparent{background-color:#0000}.bg-type-feedback-surface{background-color:var(--ui-type-feedback-surface)}.bg-type-project-surface{background-color:var(--ui-type-project-surface)}.bg-type-reference-surface{background-color:var(--ui-type-reference-surface)}.bg-type-user-surface{background-color:var(--ui-type-user-surface)}.bg-warn{background-color:var(--ui-warn)}.bg-warn-surface{background-color:var(--ui-warn-surface)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-control-x-lg{padding-inline:var(--spacing-control-x-lg)}.px-control-x-md{padding-inline:var(--spacing-control-x-md)}.px-control-x-sm{padding-inline:var(--spacing-control-x-sm)}.px-px{padding-inline:1px}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-\[15vh\]{padding-block:15vh}.py-control-y-lg{padding-block:var(--spacing-control-y-lg)}.py-control-y-md{padding-block:var(--spacing-control-y-md)}.py-control-y-sm{padding-block:var(--spacing-control-y-sm)}.py-px{padding-block:1px}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-3\.5{padding-top:calc(var(--spacing) * 3.5)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-3\.5{padding-bottom:calc(var(--spacing) * 3.5)}.pb-4\.5{padding-bottom:calc(var(--spacing) * 4.5)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-2xs{font-size:var(--text-2xs);line-height:var(--tw-leading,var(--text-2xs--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tightest{--tw-tracking:var(--tracking-tightest);letter-spacing:var(--tracking-tightest)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--ui-accent)}.text-canvas{color:var(--ui-canvas)}.text-danger{color:var(--ui-danger)}.text-fg{color:var(--ui-fg)}.text-fg-muted{color:var(--ui-fg-muted)}.text-fg-subtle{color:var(--ui-fg-subtle)}.text-ok{color:var(--ui-ok)}.text-on-accent{color:var(--ui-on-accent)}.text-scope-local{color:var(--ui-scope-local)}.text-scope-managed{color:var(--ui-scope-managed)}.text-scope-project{color:var(--ui-scope-project)}.text-scope-user{color:var(--ui-scope-user)}.text-type-feedback{color:var(--ui-type-feedback)}.text-type-project{color:var(--ui-type-project)}.text-type-reference{color:var(--ui-type-reference)}.text-type-user{color:var(--ui-type-user)}.text-warn{color:var(--ui-warn)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-danger{-webkit-text-decoration-color:var(--ui-danger);-webkit-text-decoration-color:var(--ui-danger);text-decoration-color:var(--ui-danger)}.decoration-line-strong{-webkit-text-decoration-color:var(--ui-line-strong);-webkit-text-decoration-color:var(--ui-line-strong);text-decoration-color:var(--ui-line-strong)}.accent-accent{accent-color:var(--ui-accent)}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-card{--tw-shadow:var(--ui-shadow-card);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--ui-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-fg-subtle::placeholder{color:var(--ui-fg-subtle)}.before\:flex-1:before{content:var(--tw-content);flex:1}.before\:border-t:before{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.before\:border-dashed:before{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.before\:border-danger:before{content:var(--tw-content);border-color:var(--ui-danger)}.after\:flex-1:after{content:var(--tw-content);flex:1}.after\:border-t:after{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.after\:border-dashed:after{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.after\:border-danger:after{content:var(--tw-content);border-color:var(--ui-danger)}.first\:mt-0:first-child{margin-top:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--ui-accent)}.hover\:border-accent-hover:hover{border-color:var(--ui-accent-hover)}.hover\:border-accent-line:hover{border-color:var(--ui-accent-line)}.hover\:border-line-strong:hover{border-color:var(--ui-line-strong)}.hover\:bg-accent-hover:hover{background-color:var(--ui-accent-hover)}.hover\:bg-danger:hover{background-color:var(--ui-danger)}.hover\:bg-surface-sunken:hover{background-color:var(--ui-surface-sunken)}.hover\:text-accent-hover:hover{color:var(--ui-accent-hover)}.hover\:text-fg:hover{color:var(--ui-fg)}.hover\:text-on-accent:hover{color:var(--ui-on-accent)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:border-accent:focus{border-color:var(--ui-accent)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-45:disabled{opacity:.45}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.xl\:sticky{position:sticky}.xl\:top-4{top:calc(var(--spacing) * 4)}.xl\:max-h-\[calc\(100vh-9rem\)\]{max-height:calc(100vh - 9rem)}.xl\:grid-cols-\[minmax\(280px\,380px\)_1fr\]{grid-template-columns:minmax(280px,380px) 1fr}.xl\:overflow-y-auto{overflow-y:auto}}.dark\:invert:where([data-theme=dark],[data-theme=dark] *){--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.\[\&\>span\]\:block>span{display:block}}:root{--ui-canvas:var(--color-slate-50);--ui-surface:var(--color-slate-0);--ui-surface-sunken:var(--color-slate-100);--ui-surface-raised:var(--color-slate-0);--ui-line:var(--color-slate-200);--ui-line-strong:var(--color-slate-300);--ui-fg:var(--color-slate-900);--ui-fg-muted:var(--color-slate-500);--ui-fg-subtle:var(--color-slate-400);--ui-accent:var(--color-blue-600);--ui-accent-hover:var(--color-blue-700);--ui-accent-surface:var(--color-blue-50);--ui-accent-line:var(--color-blue-200);--ui-on-accent:var(--color-slate-0);--ui-danger:var(--color-red-600);--ui-danger-surface:var(--color-red-50);--ui-danger-line:var(--color-red-300);--ui-warn:var(--color-amber-700);--ui-warn-surface:var(--color-amber-50);--ui-warn-line:var(--color-amber-300);--ui-ok:var(--color-green-600);--ui-ok-surface:var(--color-green-50);--ui-ok-line:var(--color-green-300);--ui-type-project:var(--color-blue-700);--ui-type-project-surface:var(--color-blue-50);--ui-type-feedback:var(--color-amber-700);--ui-type-feedback-surface:var(--color-amber-50);--ui-type-user:var(--color-violet-700);--ui-type-user-surface:var(--color-violet-50);--ui-type-reference:var(--color-green-700);--ui-type-reference-surface:var(--color-green-50);--ui-scope-managed:var(--color-red-600);--ui-scope-managed-surface:var(--color-red-50);--ui-scope-user:var(--color-blue-700);--ui-scope-user-surface:var(--color-blue-50);--ui-scope-project:var(--color-amber-700);--ui-scope-project-surface:var(--color-amber-50);--ui-scope-local:var(--color-slate-600);--ui-scope-local-surface:var(--color-slate-100);--ui-shadow-card:0 1px 2px #0000000d;--ui-shadow-pop:0 1px 2px #0000000f, 0 12px 32px #0000001a;--ui-scrim:#0f172a73}:root[data-theme=dark]{--ui-canvas:var(--color-slate-950);--ui-surface:var(--color-slate-900);--ui-surface-sunken:var(--color-slate-950);--ui-surface-raised:var(--color-slate-800);--ui-line:var(--color-slate-800);--ui-line-strong:var(--color-slate-700);--ui-fg:var(--color-slate-100);--ui-fg-muted:var(--color-slate-400);--ui-fg-subtle:var(--color-slate-500);--ui-accent:var(--color-blue-400);--ui-accent-hover:var(--color-blue-300);--ui-accent-surface:var(--color-blue-950);--ui-accent-line:var(--color-blue-800);--ui-on-accent:var(--color-slate-950);--ui-danger:var(--color-red-400);--ui-danger-surface:var(--color-red-950);--ui-danger-line:var(--color-red-500);--ui-warn:var(--color-amber-400);--ui-warn-surface:var(--color-amber-950);--ui-warn-line:var(--color-amber-600);--ui-ok:var(--color-green-400);--ui-ok-surface:var(--color-green-950);--ui-ok-line:var(--color-green-600);--ui-type-project:var(--color-blue-300);--ui-type-project-surface:var(--color-blue-950);--ui-type-feedback:var(--color-amber-300);--ui-type-feedback-surface:var(--color-amber-950);--ui-type-user:var(--color-violet-300);--ui-type-user-surface:var(--color-violet-950);--ui-type-reference:var(--color-green-300);--ui-type-reference-surface:var(--color-green-950);--ui-scope-managed:var(--color-red-400);--ui-scope-managed-surface:var(--color-red-950);--ui-scope-user:var(--color-blue-300);--ui-scope-user-surface:var(--color-blue-950);--ui-scope-project:var(--color-amber-300);--ui-scope-project-surface:var(--color-amber-950);--ui-scope-local:var(--color-slate-400);--ui-scope-local-surface:var(--color-slate-800);--ui-shadow-card:0 1px 2px #0006;--ui-shadow-pop:0 1px 2px #00000080, 0 12px 32px #00000073;--ui-scrim:#020610b3}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-content:"";--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;--font-mono:ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--color-red-50:oklch(97.1% .013 17.4);--color-red-300:oklch(80.8% .114 19.6);--color-red-400:oklch(70.4% .16 22.2);--color-red-500:oklch(63.7% .208 25.3);--color-red-600:oklch(57.7% .215 27.3);--color-red-950:oklch(25.8% .092 26);--color-amber-50:oklch(97.7% .021 84.6);--color-amber-300:oklch(87.9% .153 91.6);--color-amber-400:oklch(82.8% .171 84.4);--color-amber-600:oklch(66.6% .15 58.3);--color-amber-700:oklch(55.5% .13 49);--color-amber-950:oklch(27.9% .077 45.6);--color-green-50:oklch(98.2% .018 155.8);--color-green-300:oklch(87.1% .12 154.4);--color-green-400:oklch(79.2% .15 151.7);--color-green-600:oklch(58.5% .145 149.2);--color-green-700:oklch(52.7% .132 150.1);--color-green-950:oklch(26.6% .065 152.9);--color-blue-50:oklch(97% .014 253.3);--color-blue-200:oklch(88% .06 253.3);--color-blue-300:oklch(78.4% .124 239.1);--color-blue-400:oklch(75.2% .138 243.7);--color-blue-600:oklch(56.5% .163 253.3);--color-blue-700:oklch(48.5% .15 253.3);--color-blue-800:oklch(41% .125 253.3);--color-blue-950:oklch(24% .07 253.3);--color-violet-50:oklch(96.9% .016 300);--color-violet-300:oklch(82.7% .104 300);--color-violet-700:oklch(49.6% .19 300);--color-violet-950:oklch(28.3% .109 300);--color-slate-50:oklch(98.4% .003 253.3);--color-slate-100:oklch(96.4% .005 253.3);--color-slate-200:oklch(92.6% .009 253.3);--color-slate-300:oklch(86.6% .014 253.3);--color-slate-400:oklch(70.4% .021 253.3);--color-slate-500:oklch(55.6% .026 253.3);--color-slate-600:oklch(44.8% .028 253.3);--color-slate-700:oklch(36.8% .028 253.3);--color-slate-800:oklch(26.4% .026 253.3);--color-slate-900:oklch(20.4% .024 253.3);--color-slate-950:oklch(14.2% .022 253.3);--spacing:.25rem;--container-2xl:42rem;--text-xs:.6875rem;--text-xs--line-height:1.45;--text-sm:.8125rem;--text-sm--line-height:1.5;--text-base:.875rem;--text-base--line-height:1.6;--text-lg:1rem;--text-lg--line-height:1.5;--text-xl:1.25rem;--text-xl--line-height:1.3;--text-2xl:1.5rem;--text-2xl--line-height:1.2;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.12em;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.12s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-slate-0:oklch(100% 0 253.3);--spacing-control-x-sm:calc(var(--spacing) * 2);--spacing-control-y-sm:calc(var(--spacing) * 1);--spacing-control-x-md:calc(var(--spacing) * 3);--spacing-control-y-md:calc(var(--spacing) * 2);--spacing-control-x-lg:calc(var(--spacing) * 3.5);--spacing-control-y-lg:calc(var(--spacing) * 2.5);--spacing-control-h-sm:calc(var(--spacing) * 6.5);--spacing-control-h-md:calc(var(--spacing) * 9.5);--spacing-control-h-lg:calc(var(--spacing) * 9.5);--text-2xs:.625rem;--text-2xs--line-height:1.4;--tracking-tightest:-.03em}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{color-scheme:light}:root[data-theme=dark]{color-scheme:dark}html,body{height:100%}body{background-color:var(--ui-canvas);color:var(--ui-fg);font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--text-base--line-height)}::selection{background-color:var(--ui-accent-surface);color:var(--ui-accent)}:focus-visible{outline:2px solid var(--ui-accent);outline-offset:1px}}@layer components{.prose{overflow-wrap:anywhere}.prose>:first-child{margin-top:0}.prose>:last-child{margin-bottom:0}.prose h1,.prose h2,.prose h3,.prose h4{margin:calc(var(--spacing) * 6) 0 calc(var(--spacing) * 2);letter-spacing:var(--tracking-tightest);font-weight:650;line-height:1.25}.prose h1{font-size:var(--text-lg)}.prose h2{font-size:var(--text-base);padding-bottom:calc(var(--spacing) * 1.5);border-bottom:1px solid var(--ui-line)}.prose h3,.prose h4{font-size:var(--text-base);color:var(--ui-fg-muted)}.prose p{margin:calc(var(--spacing) * 3) 0}.prose ul,.prose ol{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 5)}.prose ul{list-style-type:"– "}.prose ol{list-style-type:decimal}.prose li{margin:calc(var(--spacing) * 1) 0}.prose li::marker{color:var(--ui-accent);font-family:var(--font-mono)}.prose li>ul,.prose li>ol{margin:calc(var(--spacing) * 1) 0}.prose a{color:var(--ui-accent);text-underline-offset:2px;text-decoration:underline}.prose a:hover{color:var(--ui-accent-hover)}.prose strong{color:var(--ui-fg);font-weight:650}.prose pre{margin:calc(var(--spacing) * 3) 0;padding:calc(var(--spacing) * 3);border:1px solid var(--ui-line);border-radius:var(--radius-md);background-color:var(--ui-surface-sunken);font-size:var(--text-xs);line-height:1.6;overflow-x:auto}.prose code{font-family:var(--font-mono);font-size:.9em}.prose :not(pre)>code{border:1px solid var(--ui-line);border-radius:var(--radius-sm);background-color:var(--ui-surface-sunken);color:var(--ui-accent);padding:1px 4px}.prose blockquote{margin:calc(var(--spacing) * 3) 0;padding-left:calc(var(--spacing) * 3);border-left:2px solid var(--ui-accent-line);color:var(--ui-fg-muted)}.prose hr{margin:calc(var(--spacing) * 5) 0;border:0;border-top:1px dashed var(--ui-line-strong)}.prose table{margin:calc(var(--spacing) * 3) 0;border-collapse:collapse;font-size:var(--text-sm);display:block;overflow-x:auto}.prose th,.prose td{padding:calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5);border:1px solid var(--ui-line);text-align:left}.prose th{background-color:var(--ui-surface-sunken);font-weight:600}.graph svg{cursor:grab;width:100%;display:block}.graph .node circle{cursor:pointer;stroke-width:1.5px}.graph .node circle.hit{fill:#0000;stroke:none}.graph .node text{fill:var(--ui-fg-muted);font-family:var(--font-mono);pointer-events:none;font-size:9px}.graph .node.hi circle{stroke:var(--ui-accent);stroke-width:2.5px}.graph .node.hi text{fill:var(--ui-fg);font-weight:600}.graph .node.dim{opacity:.18}.graph .node.dim text{display:none}.graph .edge{stroke:var(--ui-line-strong)}.graph .edge.dim{opacity:.05}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{inset-block:0}.top-0{top:0}.top-2{top:calc(var(--spacing) * 2)}.top-3{top:calc(var(--spacing) * 3)}.top-3\.5{top:calc(var(--spacing) * 3.5)}.right-3\.5{right:calc(var(--spacing) * 3.5)}.bottom-2{bottom:calc(var(--spacing) * 2)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-5{bottom:calc(var(--spacing) * 5)}.left-0{left:0}.left-1\/2{left:50%}.left-3\.5{left:calc(var(--spacing) * 3.5)}.z-10{z-index:10}.z-20{z-index:20}.z-100{z-index:100}.z-200{z-index:200}.float-right{float:right}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.my-1{margin-block:var(--spacing)}.my-2\.5{margin-block:calc(var(--spacing) * 2.5)}.mt-0{margin-top:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.h-2{height:calc(var(--spacing) * 2)}.h-9{height:calc(var(--spacing) * 9)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[28rem\]{max-height:28rem}.max-h-\[82vh\]{max-height:82vh}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-control-h-lg{min-height:var(--spacing-control-h-lg)}.min-h-control-h-md{min-height:var(--spacing-control-h-md)}.min-h-control-h-sm{min-height:var(--spacing-control-h-sm)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.min-w-0{min-width:0}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-control-h-sm{min-width:var(--spacing-control-h-sm)}.flex-1{flex:1}.flex-none{flex:none}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[3rem_1fr\]{grid-template-columns:3rem 1fr}.grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-px{gap:1px}.gap-x-3\.5{column-gap:calc(var(--spacing) * 3.5)}.gap-y-1{row-gap:var(--spacing)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-accent{border-color:var(--ui-accent)}.border-accent-line{border-color:var(--ui-accent-line)}.border-current{border-color:currentColor}.border-danger{border-color:var(--ui-danger)}.border-danger-line{border-color:var(--ui-danger-line)}.border-line{border-color:var(--ui-line)}.border-transparent{border-color:#0000}.border-l-danger{border-left-color:var(--ui-danger)}.border-l-warn{border-left-color:var(--ui-warn)}.bg-accent{background-color:var(--ui-accent)}.bg-accent-surface{background-color:var(--ui-accent-surface)}.bg-canvas{background-color:var(--ui-canvas)}.bg-danger{background-color:var(--ui-danger)}.bg-danger-surface{background-color:var(--ui-danger-surface)}.bg-fg{background-color:var(--ui-fg)}.bg-line-strong{background-color:var(--ui-line-strong)}.bg-ok{background-color:var(--ui-ok)}.bg-ok-surface{background-color:var(--ui-ok-surface)}.bg-scope-local-surface{background-color:var(--ui-scope-local-surface)}.bg-scope-managed-surface{background-color:var(--ui-scope-managed-surface)}.bg-scope-project-surface{background-color:var(--ui-scope-project-surface)}.bg-scope-user-surface{background-color:var(--ui-scope-user-surface)}.bg-scrim{background-color:var(--ui-scrim)}.bg-surface{background-color:var(--ui-surface)}.bg-surface-sunken{background-color:var(--ui-surface-sunken)}.bg-transparent{background-color:#0000}.bg-type-feedback-surface{background-color:var(--ui-type-feedback-surface)}.bg-type-project-surface{background-color:var(--ui-type-project-surface)}.bg-type-reference-surface{background-color:var(--ui-type-reference-surface)}.bg-type-user-surface{background-color:var(--ui-type-user-surface)}.bg-warn{background-color:var(--ui-warn)}.bg-warn-surface{background-color:var(--ui-warn-surface)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-control-x-lg{padding-inline:var(--spacing-control-x-lg)}.px-control-x-md{padding-inline:var(--spacing-control-x-md)}.px-control-x-sm{padding-inline:var(--spacing-control-x-sm)}.px-px{padding-inline:1px}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-\[15vh\]{padding-block:15vh}.py-control-y-lg{padding-block:var(--spacing-control-y-lg)}.py-control-y-md{padding-block:var(--spacing-control-y-md)}.py-control-y-sm{padding-block:var(--spacing-control-y-sm)}.py-px{padding-block:1px}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-3\.5{padding-top:calc(var(--spacing) * 3.5)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-3\.5{padding-bottom:calc(var(--spacing) * 3.5)}.pb-4\.5{padding-bottom:calc(var(--spacing) * 4.5)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-2xs{font-size:var(--text-2xs);line-height:var(--tw-leading,var(--text-2xs--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tightest{--tw-tracking:var(--tracking-tightest);letter-spacing:var(--tracking-tightest)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--ui-accent)}.text-canvas{color:var(--ui-canvas)}.text-danger{color:var(--ui-danger)}.text-fg{color:var(--ui-fg)}.text-fg-muted{color:var(--ui-fg-muted)}.text-fg-subtle{color:var(--ui-fg-subtle)}.text-ok{color:var(--ui-ok)}.text-on-accent{color:var(--ui-on-accent)}.text-scope-local{color:var(--ui-scope-local)}.text-scope-managed{color:var(--ui-scope-managed)}.text-scope-project{color:var(--ui-scope-project)}.text-scope-user{color:var(--ui-scope-user)}.text-type-feedback{color:var(--ui-type-feedback)}.text-type-project{color:var(--ui-type-project)}.text-type-reference{color:var(--ui-type-reference)}.text-type-user{color:var(--ui-type-user)}.text-warn{color:var(--ui-warn)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-danger{-webkit-text-decoration-color:var(--ui-danger);-webkit-text-decoration-color:var(--ui-danger);text-decoration-color:var(--ui-danger)}.decoration-line-strong{-webkit-text-decoration-color:var(--ui-line-strong);-webkit-text-decoration-color:var(--ui-line-strong);text-decoration-color:var(--ui-line-strong)}.accent-accent{accent-color:var(--ui-accent)}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-card{--tw-shadow:var(--ui-shadow-card);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--ui-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-fg-subtle::placeholder{color:var(--ui-fg-subtle)}.before\:flex-1:before{content:var(--tw-content);flex:1}.before\:border-t:before{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.before\:border-dashed:before{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.before\:border-danger:before{content:var(--tw-content);border-color:var(--ui-danger)}.after\:flex-1:after{content:var(--tw-content);flex:1}.after\:border-t:after{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:1px}.after\:border-dashed:after{content:var(--tw-content);--tw-border-style:dashed;border-style:dashed}.after\:border-danger:after{content:var(--tw-content);border-color:var(--ui-danger)}.first\:mt-0:first-child{margin-top:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--ui-accent)}.hover\:border-accent-hover:hover{border-color:var(--ui-accent-hover)}.hover\:border-accent-line:hover{border-color:var(--ui-accent-line)}.hover\:border-line-strong:hover{border-color:var(--ui-line-strong)}.hover\:bg-accent-hover:hover{background-color:var(--ui-accent-hover)}.hover\:bg-danger:hover{background-color:var(--ui-danger)}.hover\:bg-surface-sunken:hover{background-color:var(--ui-surface-sunken)}.hover\:text-accent-hover:hover{color:var(--ui-accent-hover)}.hover\:text-fg:hover{color:var(--ui-fg)}.hover\:text-on-accent:hover{color:var(--ui-on-accent)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:border-accent:focus{border-color:var(--ui-accent)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-45:disabled{opacity:.45}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.xl\:sticky{position:sticky}.xl\:top-4{top:calc(var(--spacing) * 4)}.xl\:max-h-\[calc\(100vh-9rem\)\]{max-height:calc(100vh - 9rem)}.xl\:grid-cols-\[minmax\(280px\,380px\)_1fr\]{grid-template-columns:minmax(280px,380px) 1fr}.xl\:overflow-y-auto{overflow-y:auto}}.dark\:invert:where([data-theme=dark],[data-theme=dark] *){--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.\[\&\>span\]\:block>span{display:block}}:root{--ui-canvas:var(--color-slate-50);--ui-surface:var(--color-slate-0);--ui-surface-sunken:var(--color-slate-100);--ui-surface-raised:var(--color-slate-0);--ui-line:var(--color-slate-200);--ui-line-strong:var(--color-slate-300);--ui-fg:var(--color-slate-900);--ui-fg-muted:var(--color-slate-500);--ui-fg-subtle:var(--color-slate-400);--ui-accent:var(--color-blue-600);--ui-accent-hover:var(--color-blue-700);--ui-accent-surface:var(--color-blue-50);--ui-accent-line:var(--color-blue-200);--ui-on-accent:var(--color-slate-0);--ui-danger:var(--color-red-600);--ui-danger-surface:var(--color-red-50);--ui-danger-line:var(--color-red-300);--ui-warn:var(--color-amber-700);--ui-warn-surface:var(--color-amber-50);--ui-warn-line:var(--color-amber-300);--ui-ok:var(--color-green-600);--ui-ok-surface:var(--color-green-50);--ui-ok-line:var(--color-green-300);--ui-type-project:var(--color-blue-700);--ui-type-project-surface:var(--color-blue-50);--ui-type-feedback:var(--color-amber-700);--ui-type-feedback-surface:var(--color-amber-50);--ui-type-user:var(--color-violet-700);--ui-type-user-surface:var(--color-violet-50);--ui-type-reference:var(--color-green-700);--ui-type-reference-surface:var(--color-green-50);--ui-scope-managed:var(--color-red-600);--ui-scope-managed-surface:var(--color-red-50);--ui-scope-user:var(--color-blue-700);--ui-scope-user-surface:var(--color-blue-50);--ui-scope-project:var(--color-amber-700);--ui-scope-project-surface:var(--color-amber-50);--ui-scope-local:var(--color-slate-600);--ui-scope-local-surface:var(--color-slate-100);--ui-shadow-card:0 1px 2px #0000000d;--ui-shadow-pop:0 1px 2px #0000000f, 0 12px 32px #0000001a;--ui-scrim:#0f172a73}:root[data-theme=dark]{--ui-canvas:var(--color-slate-950);--ui-surface:var(--color-slate-900);--ui-surface-sunken:var(--color-slate-950);--ui-surface-raised:var(--color-slate-800);--ui-line:var(--color-slate-800);--ui-line-strong:var(--color-slate-700);--ui-fg:var(--color-slate-100);--ui-fg-muted:var(--color-slate-400);--ui-fg-subtle:var(--color-slate-500);--ui-accent:var(--color-blue-400);--ui-accent-hover:var(--color-blue-300);--ui-accent-surface:var(--color-blue-950);--ui-accent-line:var(--color-blue-800);--ui-on-accent:var(--color-slate-950);--ui-danger:var(--color-red-400);--ui-danger-surface:var(--color-red-950);--ui-danger-line:var(--color-red-500);--ui-warn:var(--color-amber-400);--ui-warn-surface:var(--color-amber-950);--ui-warn-line:var(--color-amber-600);--ui-ok:var(--color-green-400);--ui-ok-surface:var(--color-green-950);--ui-ok-line:var(--color-green-600);--ui-type-project:var(--color-blue-300);--ui-type-project-surface:var(--color-blue-950);--ui-type-feedback:var(--color-amber-300);--ui-type-feedback-surface:var(--color-amber-950);--ui-type-user:var(--color-violet-300);--ui-type-user-surface:var(--color-violet-950);--ui-type-reference:var(--color-green-300);--ui-type-reference-surface:var(--color-green-950);--ui-scope-managed:var(--color-red-400);--ui-scope-managed-surface:var(--color-red-950);--ui-scope-user:var(--color-blue-300);--ui-scope-user-surface:var(--color-blue-950);--ui-scope-project:var(--color-amber-300);--ui-scope-project-surface:var(--color-amber-950);--ui-scope-local:var(--color-slate-400);--ui-scope-local-surface:var(--color-slate-800);--ui-shadow-card:0 1px 2px #0006;--ui-shadow-pop:0 1px 2px #00000080, 0 12px 32px #00000073;--ui-scrim:#020610b3}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
package/public/ui.mjs
CHANGED
|
@@ -182,6 +182,21 @@ export const indexLineText = ({ kind = 'text', clickable = false, dropped = fals
|
|
|
182
182
|
dropped && 'line-through decoration-line-strong',
|
|
183
183
|
);
|
|
184
184
|
|
|
185
|
+
export const retentionTrack = 'relative mt-1 h-9 overflow-hidden rounded-md border border-line bg-surface-sunken';
|
|
186
|
+
export const retentionSweep = 'absolute inset-y-0 left-0 border-l border-dashed border-danger';
|
|
187
|
+
const retentionTickBase = 'absolute top-2 bottom-2 w-0.5 -translate-x-1/2 rounded-full';
|
|
188
|
+
const retentionTickTone = { soon: 'bg-warn', safe: 'bg-accent' };
|
|
189
|
+
export const retentionTick = (soon) => cx(retentionTickBase, soon ? retentionTickTone.soon : retentionTickTone.safe);
|
|
190
|
+
export const retentionScale = 'mt-1.5 flex justify-between font-mono text-2xs text-fg-subtle';
|
|
191
|
+
|
|
192
|
+
export const provenanceRow = 'mt-0.5 mb-3.5 flex flex-wrap items-baseline gap-1.5 text-xs text-fg-muted';
|
|
193
|
+
const provenanceLinkBase = 'cursor-pointer rounded-sm border border-transparent bg-transparent px-1 font-medium';
|
|
194
|
+
const provenanceLinkState = {
|
|
195
|
+
live: 'bg-accent-surface text-accent hover:border-accent-line',
|
|
196
|
+
dead: 'cursor-help bg-danger-surface text-danger line-through',
|
|
197
|
+
};
|
|
198
|
+
export const provenanceLink = (live) => cx(provenanceLinkBase, live ? provenanceLinkState.live : provenanceLinkState.dead);
|
|
199
|
+
|
|
185
200
|
export const cutLine = 'my-2.5 flex items-center gap-2.5 before:flex-1 before:border-t before:border-dashed before:border-danger after:flex-1 after:border-t after:border-dashed after:border-danger';
|
|
186
201
|
export const cutLabel = 'flex-none font-mono text-2xs whitespace-nowrap text-danger';
|
|
187
202
|
|
package/server.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { listStores } from './src/stores.mjs';
|
|
|
16
16
|
import { forgetPath, rememberPath } from './src/pathcache.mjs';
|
|
17
17
|
import { searchAll } from './src/search.mjs';
|
|
18
18
|
import { verifyPaths } from './src/pathcheck.mjs';
|
|
19
|
+
import { sessionsWithSummaries, transcriptDir } from './src/sessions.mjs';
|
|
19
20
|
import {
|
|
20
21
|
addIndexEntry,
|
|
21
22
|
addIndexEntryPreview,
|
|
@@ -267,6 +268,14 @@ async function handleApi(req, res, url) {
|
|
|
267
268
|
return sendJson(res, 200, verifyPaths(projectDir, memories));
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
if (action === 'sessions' && req.method === 'GET') {
|
|
272
|
+
const slugDir = transcriptDir(store);
|
|
273
|
+
if (!slugDir) {
|
|
274
|
+
return sendJson(res, 400, { error: 'This store keeps no session transcripts.' });
|
|
275
|
+
}
|
|
276
|
+
return sendJson(res, 200, sessionsWithSummaries(slugDir, { projectDir: storeProjectDir(store) }));
|
|
277
|
+
}
|
|
278
|
+
|
|
270
279
|
if (action === 'settings' && req.method === 'GET') {
|
|
271
280
|
return sendJson(res, 200, settingsReport({ projectDir: storeProjectDir(store) }));
|
|
272
281
|
}
|
package/src/checks.mjs
CHANGED
|
@@ -5,6 +5,10 @@ export const VALID_TYPES = ['user', 'feedback', 'project', 'reference'];
|
|
|
5
5
|
export const EMPTY_BODY_CHARS = 40;
|
|
6
6
|
export const EMPTY_INSTRUCTION_CHARS = 10;
|
|
7
7
|
export const HOOK_ECHO_OVERLAP = 0.9;
|
|
8
|
+
export const EVIDENCE_WARNING_DAYS = 14;
|
|
9
|
+
export const EVIDENCE_CRITICAL_DAYS = 7;
|
|
10
|
+
|
|
11
|
+
const TRANSCRIPT_RESOLVED = new Set(['transcript', 'repo-root']);
|
|
8
12
|
|
|
9
13
|
const VALID_TYPE_SET = new Set(VALID_TYPES);
|
|
10
14
|
|
|
@@ -169,6 +173,60 @@ export function checkEmptyInstructionFile(files) {
|
|
|
169
173
|
return empty;
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
export function checkProvenanceExpired(memories, resolveOrigin) {
|
|
177
|
+
if (typeof resolveOrigin !== 'function') return [];
|
|
178
|
+
return memories
|
|
179
|
+
.map((memory) => ({ memory, sessionId: memory.metadata?.originSessionId }))
|
|
180
|
+
.filter(({ sessionId }) => typeof sessionId === 'string' && sessionId)
|
|
181
|
+
.filter(({ sessionId }) => !resolveOrigin(sessionId))
|
|
182
|
+
.map(({ memory, sessionId }) => ({
|
|
183
|
+
kind: 'provenance-expired',
|
|
184
|
+
severity: 'warn',
|
|
185
|
+
file: memory.file,
|
|
186
|
+
name: memory.name,
|
|
187
|
+
sessionId,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function checkPathEvidenceExpiring(store, retention, { remembered = false } = {}) {
|
|
192
|
+
if (!store || store.kind !== 'auto' || remembered) return [];
|
|
193
|
+
if (!TRANSCRIPT_RESOLVED.has(store.resolvedBy)) return [];
|
|
194
|
+
|
|
195
|
+
const days = retention?.evidenceExpiresInDays;
|
|
196
|
+
if (typeof days !== 'number' || days > EVIDENCE_WARNING_DAYS) return [];
|
|
197
|
+
|
|
198
|
+
return [{
|
|
199
|
+
kind: 'path-evidence-expiring',
|
|
200
|
+
severity: days <= EVIDENCE_CRITICAL_DAYS ? 'bad' : 'warn',
|
|
201
|
+
path: store.path,
|
|
202
|
+
days,
|
|
203
|
+
retentionDays: retention.days,
|
|
204
|
+
sessionCount: retention.count,
|
|
205
|
+
}];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function checkNoMemoryDespiteSessions(store, memories, retention) {
|
|
209
|
+
if (!store || store.kind !== 'auto' || memories.length) return [];
|
|
210
|
+
if (!store.autoMemory?.enabled || !store.autoMemory?.known) return [];
|
|
211
|
+
if (!retention || !retention.count) return [];
|
|
212
|
+
|
|
213
|
+
return [{
|
|
214
|
+
kind: 'no-memory-despite-sessions',
|
|
215
|
+
severity: 'warn',
|
|
216
|
+
sessionCount: retention.count,
|
|
217
|
+
retentionDays: retention.days,
|
|
218
|
+
}];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function sessionChecks(store, memories, retention, { remembered = false, resolveOrigin = null } = {}) {
|
|
222
|
+
if (!retention) return [];
|
|
223
|
+
return [
|
|
224
|
+
...checkProvenanceExpired(memories, resolveOrigin),
|
|
225
|
+
...checkPathEvidenceExpiring(store, retention, { remembered }),
|
|
226
|
+
...checkNoMemoryDespiteSessions(store, memories, retention),
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
|
|
172
230
|
export function memoryChecks(memories, index) {
|
|
173
231
|
return [
|
|
174
232
|
...checkDuplicateNames(memories),
|
package/src/model.mjs
CHANGED
|
@@ -15,7 +15,9 @@ import { parseIndex, parseFrontmatter, extractWikilinks } from './parse.mjs';
|
|
|
15
15
|
import { listMemoryFiles, memoryDir, resolveProjectPath, shortLabel } from './projects.mjs';
|
|
16
16
|
import { autoMemoryState } from './settings.mjs';
|
|
17
17
|
import { ageInDays, estimateTokens, findDuplicates, indexStats } from './stats.mjs';
|
|
18
|
-
import { memoryChecks } from './checks.mjs';
|
|
18
|
+
import { memoryChecks, sessionChecks } from './checks.mjs';
|
|
19
|
+
import { listSessions, originSession, retention, transcriptDir } from './sessions.mjs';
|
|
20
|
+
import { rememberedPath } from './pathcache.mjs';
|
|
19
21
|
|
|
20
22
|
export const TRASH_DIR = '.trash';
|
|
21
23
|
|
|
@@ -95,9 +97,30 @@ function buildResolver(memories) {
|
|
|
95
97
|
return (target) => byName.get(target) || byStem.get(target) || null;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
function provenance(memory, resolveOrigin) {
|
|
101
|
+
const sessionId = memory.metadata?.originSessionId;
|
|
102
|
+
if (typeof sessionId !== 'string' || !sessionId) return null;
|
|
103
|
+
const session = resolveOrigin ? resolveOrigin(sessionId) : null;
|
|
104
|
+
return { sessionId, present: Boolean(session), modified: session ? session.modified : null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sessionContext(store) {
|
|
108
|
+
const slugDir = transcriptDir(store);
|
|
109
|
+
if (!slugDir) return { slugDir: null, retention: null, resolveOrigin: null, remembered: false };
|
|
110
|
+
|
|
111
|
+
const projectDir = store.pathExists ? store.path : null;
|
|
112
|
+
return {
|
|
113
|
+
slugDir,
|
|
114
|
+
retention: retention(listSessions(slugDir), { projectDir }),
|
|
115
|
+
resolveOrigin: (sessionId) => originSession(slugDir, sessionId),
|
|
116
|
+
remembered: Boolean(store.slug && rememberedPath(store.slug)),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
export function buildStore(store) {
|
|
99
121
|
const { dir } = store;
|
|
100
122
|
const hasMemoryDir = fs.existsSync(dir);
|
|
123
|
+
const sessions = sessionContext(store);
|
|
101
124
|
|
|
102
125
|
const indexRaw = readIfExists(path.join(dir, 'MEMORY.md'));
|
|
103
126
|
const index = indexRaw === null ? null : parseIndex(indexRaw);
|
|
@@ -139,6 +162,7 @@ export function buildStore(store) {
|
|
|
139
162
|
: referencedFiles.has(memory.file)
|
|
140
163
|
? 'referenced'
|
|
141
164
|
: 'orphan';
|
|
165
|
+
memory.origin = provenance(memory, sessions.resolveOrigin);
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
const existingFiles = new Set(files);
|
|
@@ -175,6 +199,10 @@ export function buildStore(store) {
|
|
|
175
199
|
? [{ kind: 'long-hooks', severity: 'warn', count: health.longHooks.length, longest: health.longHooks[0] }]
|
|
176
200
|
: []),
|
|
177
201
|
...memoryChecks(memories, index),
|
|
202
|
+
...sessionChecks(store, memories, sessions.retention, {
|
|
203
|
+
remembered: sessions.remembered,
|
|
204
|
+
resolveOrigin: sessions.resolveOrigin,
|
|
205
|
+
}),
|
|
178
206
|
];
|
|
179
207
|
health.issues.sort((a, b) => (a.severity === 'bad' ? 0 : 1) - (b.severity === 'bad' ? 0 : 1));
|
|
180
208
|
health.issueCount = health.issues.length;
|
|
@@ -209,6 +237,16 @@ export function buildStore(store) {
|
|
|
209
237
|
},
|
|
210
238
|
duplicates: findDuplicates(memories),
|
|
211
239
|
trash: listTrash(dir),
|
|
240
|
+
sessions: sessions.retention
|
|
241
|
+
? {
|
|
242
|
+
count: sessions.retention.count,
|
|
243
|
+
bytes: sessions.retention.bytes,
|
|
244
|
+
retentionDays: sessions.retention.days,
|
|
245
|
+
evidenceExpiresInDays: sessions.retention.evidenceExpiresInDays,
|
|
246
|
+
expiringCount: sessions.retention.expiringCount,
|
|
247
|
+
remembered: sessions.remembered,
|
|
248
|
+
}
|
|
249
|
+
: null,
|
|
212
250
|
};
|
|
213
251
|
}
|
|
214
252
|
|
|
@@ -224,6 +262,7 @@ export function buildProject(root, slug) {
|
|
|
224
262
|
kind: 'auto',
|
|
225
263
|
dir: memoryDir(root, slug),
|
|
226
264
|
path: resolved.path,
|
|
265
|
+
pathExists: resolved.exists,
|
|
227
266
|
label: shortLabel(resolved.path),
|
|
228
267
|
resolvedBy: resolved.resolvedBy,
|
|
229
268
|
// The decode that could not be confirmed, offered as a starting point when
|
package/src/projects.mjs
CHANGED
|
@@ -51,12 +51,16 @@ export function projectsRoot() {
|
|
|
51
51
|
return resolveRoot().path;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// The escalation ladder for reading the head of a transcript: the small read
|
|
55
|
+
// almost always suffices and the large one is the fallback.
|
|
56
|
+
export const HEAD_READS = [16384, 131072];
|
|
57
|
+
|
|
54
58
|
/**
|
|
55
59
|
* Read the head of a file without pulling a multi-megabyte transcript into
|
|
56
60
|
* memory. The `cwd` field shows up within the first few lines in practice, so
|
|
57
61
|
* the small read almost always suffices and the large one is a fallback.
|
|
58
62
|
*/
|
|
59
|
-
function readHead(file, bytes) {
|
|
63
|
+
export function readHead(file, bytes) {
|
|
60
64
|
const fd = fs.openSync(file, 'r');
|
|
61
65
|
try {
|
|
62
66
|
const buf = Buffer.alloc(bytes);
|
|
@@ -90,7 +94,7 @@ function findTranscripts(dir, depth = 2) {
|
|
|
90
94
|
|
|
91
95
|
/** The first `cwd` in one transcript, escalating the read only if the small one missed. */
|
|
92
96
|
function cwdInTranscript(file) {
|
|
93
|
-
for (const size of
|
|
97
|
+
for (const size of HEAD_READS) {
|
|
94
98
|
let head;
|
|
95
99
|
try {
|
|
96
100
|
head = readHead(file, size);
|
package/src/sessions.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { HEAD_READS, readHead } from './projects.mjs';
|
|
4
|
+
import { cleanupPeriodDays } from './settings.mjs';
|
|
5
|
+
import { ageInDays } from './stats.mjs';
|
|
6
|
+
|
|
7
|
+
export const EXPIRY_WARNING_DAYS = 7;
|
|
8
|
+
export const PROMPT_TITLE_CHARS = 80;
|
|
9
|
+
export const PROMPT_PREVIEW_CHARS = 400;
|
|
10
|
+
|
|
11
|
+
const SESSION_ID = /^[0-9a-fA-F][0-9a-fA-F-]{7,63}$/;
|
|
12
|
+
const SELF_DESCRIBING_KEYS = ['slug', 'gitBranch', 'model', 'version', 'cwd'];
|
|
13
|
+
const WRAPPED_BY_CLAUDE_CODE = /^<(local-command-[a-z]+|command-(message|name|args)|system-reminder)>/;
|
|
14
|
+
|
|
15
|
+
const isSubagentTranscript = (name) => name.startsWith('agent-');
|
|
16
|
+
const isSessionTranscript = (name) => name.endsWith('.jsonl') && !isSubagentTranscript(name);
|
|
17
|
+
|
|
18
|
+
export function listSessions(slugDir) {
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = fs.readdirSync(slugDir, { withFileTypes: true });
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sessions = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isFile() || !isSessionTranscript(entry.name)) continue;
|
|
29
|
+
const file = path.join(slugDir, entry.name);
|
|
30
|
+
let stat;
|
|
31
|
+
try {
|
|
32
|
+
stat = fs.statSync(file);
|
|
33
|
+
} catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
sessions.push({
|
|
37
|
+
id: entry.name.replace(/\.jsonl$/, ''),
|
|
38
|
+
file,
|
|
39
|
+
bytes: stat.size,
|
|
40
|
+
modified: new Date(stat.mtimeMs).toISOString(),
|
|
41
|
+
modifiedMs: stat.mtimeMs,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return sessions.sort((a, b) => b.modifiedMs - a.modifiedMs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function promptText(content) {
|
|
48
|
+
if (typeof content === 'string') return content;
|
|
49
|
+
if (!Array.isArray(content)) return null;
|
|
50
|
+
const joined = content
|
|
51
|
+
.filter((block) => block && block.type === 'text' && typeof block.text === 'string')
|
|
52
|
+
.map((block) => block.text)
|
|
53
|
+
.join(' ');
|
|
54
|
+
return joined || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function absorbRecord(record, found) {
|
|
58
|
+
if (!record || typeof record !== 'object') return;
|
|
59
|
+
|
|
60
|
+
if (record.type === 'ai-title' && typeof record.aiTitle === 'string' && record.aiTitle) {
|
|
61
|
+
found.aiTitle = record.aiTitle;
|
|
62
|
+
}
|
|
63
|
+
for (const key of SELF_DESCRIBING_KEYS) {
|
|
64
|
+
const value = typeof record[key] === 'string' ? record[key] : record.message?.[key];
|
|
65
|
+
if (found[key] === null && typeof value === 'string' && value) found[key] = value;
|
|
66
|
+
}
|
|
67
|
+
if (found.prompt === null && record.type === 'user' && !record.isSidechain) {
|
|
68
|
+
const text = promptText(record.message?.content)?.trim();
|
|
69
|
+
if (text && !WRAPPED_BY_CLAUDE_CODE.test(text)) found.prompt = text;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function absorbChunk(head, found) {
|
|
74
|
+
for (const line of head.split('\n')) {
|
|
75
|
+
if (!line.startsWith('{')) continue;
|
|
76
|
+
try {
|
|
77
|
+
absorbRecord(JSON.parse(line), found);
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function condense(text, chars) {
|
|
85
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
86
|
+
return flat.length > chars ? `${flat.slice(0, chars - 1)}…` : flat;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function sessionSummary(file, { bytes = null } = {}) {
|
|
90
|
+
const found = { aiTitle: null, slug: null, gitBranch: null, model: null, version: null, cwd: null, prompt: null };
|
|
91
|
+
|
|
92
|
+
let size = bytes;
|
|
93
|
+
if (size === null) {
|
|
94
|
+
try {
|
|
95
|
+
size = fs.statSync(file).size;
|
|
96
|
+
} catch {
|
|
97
|
+
size = 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const read of HEAD_READS) {
|
|
102
|
+
let head;
|
|
103
|
+
try {
|
|
104
|
+
head = readHead(file, read);
|
|
105
|
+
} catch {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
absorbChunk(head, found);
|
|
109
|
+
if (found.aiTitle || size <= read) break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const titleFrom = found.aiTitle ? 'ai-title' : found.slug ? 'slug' : found.prompt ? 'prompt' : null;
|
|
113
|
+
const title = found.aiTitle
|
|
114
|
+
|| found.slug
|
|
115
|
+
|| (found.prompt ? condense(found.prompt, PROMPT_TITLE_CHARS) : null);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
title,
|
|
119
|
+
titleFrom,
|
|
120
|
+
slug: found.slug,
|
|
121
|
+
gitBranch: found.gitBranch,
|
|
122
|
+
model: found.model,
|
|
123
|
+
version: found.version,
|
|
124
|
+
cwd: found.cwd,
|
|
125
|
+
prompt: found.prompt ? condense(found.prompt, PROMPT_PREVIEW_CHARS) : null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function retention(sessions, { projectDir = null, now = Date.now() } = {}) {
|
|
130
|
+
const days = cleanupPeriodDays(projectDir ? { projectDir } : {});
|
|
131
|
+
const dated = sessions.map((session) => {
|
|
132
|
+
const ageDays = ageInDays(session.modified, now);
|
|
133
|
+
return { ...session, ageDays, expiresInDays: ageDays === null ? null : Math.max(0, days - ageDays) };
|
|
134
|
+
});
|
|
135
|
+
const remaining = dated.map((session) => session.expiresInDays).filter((value) => value !== null);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
days,
|
|
139
|
+
dateFrom: 'mtime',
|
|
140
|
+
sessions: dated,
|
|
141
|
+
count: dated.length,
|
|
142
|
+
bytes: dated.reduce((sum, session) => sum + session.bytes, 0),
|
|
143
|
+
evidenceExpiresInDays: remaining.length ? Math.max(...remaining) : null,
|
|
144
|
+
expiringCount: remaining.filter((value) => value <= EXPIRY_WARNING_DAYS).length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function originSession(slugDir, sessionId) {
|
|
149
|
+
if (typeof sessionId !== 'string' || !SESSION_ID.test(sessionId)) return null;
|
|
150
|
+
const file = path.join(slugDir, `${sessionId}.jsonl`);
|
|
151
|
+
let stat;
|
|
152
|
+
try {
|
|
153
|
+
stat = fs.statSync(file);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (!stat.isFile()) return null;
|
|
158
|
+
return { id: sessionId, file, bytes: stat.size, modified: new Date(stat.mtimeMs).toISOString() };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function transcriptDir(store) {
|
|
162
|
+
return store && store.kind === 'auto' && store.dir ? path.dirname(store.dir) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function sessionsWithSummaries(slugDir, { projectDir = null, now = Date.now() } = {}) {
|
|
166
|
+
const report = retention(listSessions(slugDir), { projectDir, now });
|
|
167
|
+
return {
|
|
168
|
+
...report,
|
|
169
|
+
sessions: report.sessions.map((session) => ({
|
|
170
|
+
...session,
|
|
171
|
+
...sessionSummary(session.file, { bytes: session.bytes }),
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|