kimi-code-memory-mcp-server 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -1
- package/README.en.md +7 -0
- package/README.md +11 -2
- package/dist/refine/store.d.ts +4 -0
- package/dist/refine/store.js +14 -0
- package/dist/refine/store.js.map +1 -1
- package/dist/refined-manager.d.ts +4 -0
- package/dist/refined-manager.js +5 -0
- package/dist/refined-manager.js.map +1 -1
- package/dist/theme-manager.d.ts +1 -0
- package/dist/theme-manager.js +7 -0
- package/dist/theme-manager.js.map +1 -1
- package/dist/tools/context-tools.js +56 -0
- package/dist/tools/context-tools.js.map +1 -1
- package/dist/tools/system-tools.js +24 -1
- package/dist/tools/system-tools.js.map +1 -1
- package/dist/tools/theme-tools.js +23 -0
- package/dist/tools/theme-tools.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vis/api.d.ts +35 -0
- package/dist/vis/api.js +233 -19
- package/dist/vis/api.js.map +1 -1
- package/dist/vis/server.js +237 -3
- package/dist/vis/server.js.map +1 -1
- package/dist/vis/static/api.js +57 -0
- package/dist/vis/static/app.js +1397 -324
- package/dist/vis/static/index.html +23 -93
- package/dist/vis/static/state.js +26 -0
- package/dist/vis/static/style.css +1190 -344
- package/dist/vis/static/utils/helpers.js +94 -0
- package/dist/vis/static/utils/markdown.js +81 -0
- package/package.json +1 -1
package/dist/vis/static/app.js
CHANGED
|
@@ -1,395 +1,1468 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
import { state, LS_KEY_AUTO_VIS } from './state.js';
|
|
2
|
+
import {
|
|
3
|
+
formatDate,
|
|
4
|
+
escapeHtml,
|
|
5
|
+
iconKimiMemory,
|
|
6
|
+
iconPanelLeftClose,
|
|
7
|
+
iconPanelLeftOpen,
|
|
8
|
+
setStatus,
|
|
9
|
+
getInitialCollapsed,
|
|
10
|
+
updateCollapsedClass,
|
|
11
|
+
toggleSidebar,
|
|
12
|
+
toggleMobileSidebar,
|
|
13
|
+
sectionFor,
|
|
14
|
+
workspaceFolderName,
|
|
15
|
+
updateDocumentTitle,
|
|
16
|
+
} from './utils/helpers.js';
|
|
17
|
+
import { renderMarkdown } from './utils/markdown.js';
|
|
18
|
+
import {
|
|
19
|
+
api,
|
|
20
|
+
listMemoryFolders,
|
|
21
|
+
writeMemory,
|
|
22
|
+
deleteMemoryFile,
|
|
23
|
+
createMemoryFolder,
|
|
24
|
+
renameMemoryFolder,
|
|
25
|
+
deleteMemoryFolder,
|
|
26
|
+
deleteThemeApi,
|
|
27
|
+
deleteSearchViewApi,
|
|
28
|
+
} from './api.js';
|
|
29
|
+
|
|
30
|
+
const sections = [
|
|
31
|
+
{ id: 'workspace', label: 'Workspace', icon: '◈' },
|
|
32
|
+
{ id: 'themes', label: 'Themes', icon: '◉' },
|
|
33
|
+
{ id: 'searches', label: 'Searches', icon: '🔍' },
|
|
34
|
+
{ id: 'decisions', label: 'Decisions', icon: '◆' },
|
|
35
|
+
{ id: 'memories', label: 'Memories', icon: '▣' },
|
|
36
|
+
{ id: 'settings', label: 'Settings', icon: '⚙' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const navGroups = [
|
|
40
|
+
{
|
|
41
|
+
label: 'Workspace',
|
|
42
|
+
items: [{ id: 'workspace', label: 'Workspace', icon: '◈' }],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'Analysis',
|
|
46
|
+
items: [
|
|
47
|
+
{ id: 'themes', label: 'Themes', icon: '◉' },
|
|
48
|
+
{ id: 'searches', label: 'Searches', icon: '🔍' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: 'Memory',
|
|
53
|
+
items: [
|
|
54
|
+
{ id: 'decisions', label: 'Decisions', icon: '◆' },
|
|
55
|
+
{ id: 'memories', label: 'Memories', icon: '▣' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: 'System',
|
|
60
|
+
items: [{ id: 'settings', label: 'Settings', icon: '⚙' }],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const $ = (sel) => document.querySelector(sel);
|
|
65
|
+
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
|
66
|
+
|
|
67
|
+
function setHash(view, theme) {
|
|
68
|
+
const hash = theme ? `#${view}/${encodeURIComponent(theme)}` : `#${view}`;
|
|
69
|
+
if (location.hash !== hash) {
|
|
70
|
+
history.pushState(null, '', hash);
|
|
37
71
|
}
|
|
72
|
+
}
|
|
38
73
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
74
|
+
function parseHash() {
|
|
75
|
+
const raw = location.hash.replace(/^#/, '');
|
|
76
|
+
if (!raw) return { view: 'workspace', theme: null };
|
|
77
|
+
if (raw.startsWith('themes/')) {
|
|
78
|
+
return { view: 'theme-detail', theme: decodeURIComponent(raw.slice(7)) };
|
|
79
|
+
}
|
|
80
|
+
if (raw.startsWith('searches/')) {
|
|
81
|
+
return { view: 'search-detail', key: decodeURIComponent(raw.slice(9)) };
|
|
48
82
|
}
|
|
83
|
+
const known = ['workspace', 'themes', 'searches', 'decisions', 'memories', 'settings', 'theme-detail'];
|
|
84
|
+
if (known.includes(raw)) return { view: raw, theme: null };
|
|
85
|
+
return { view: 'workspace', theme: null };
|
|
86
|
+
}
|
|
49
87
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
88
|
+
function applyHash() {
|
|
89
|
+
const { view, theme, key } = parseHash();
|
|
90
|
+
state.currentView = view;
|
|
91
|
+
const newKey = key || null;
|
|
92
|
+
if (view === 'search-detail' && newKey !== state.currentSearchKey) {
|
|
93
|
+
state.data.searchDetail = null;
|
|
55
94
|
}
|
|
95
|
+
state.currentSearchKey = newKey;
|
|
96
|
+
if (view === 'theme-detail') {
|
|
97
|
+
if (theme !== state.currentTheme) {
|
|
98
|
+
state.data.themeDetail = null;
|
|
99
|
+
}
|
|
100
|
+
state.currentTheme = theme;
|
|
101
|
+
} else {
|
|
102
|
+
state.currentTheme = theme;
|
|
103
|
+
}
|
|
104
|
+
state.sidebarOpenMobile = false;
|
|
105
|
+
$('#sidebar').classList.remove('open');
|
|
106
|
+
renderAll();
|
|
107
|
+
loadDataForView(view, theme, key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderAll() {
|
|
111
|
+
renderSidebarCollapsedTop();
|
|
112
|
+
renderSidebarHeader();
|
|
113
|
+
renderSidebar();
|
|
114
|
+
renderTopbar();
|
|
115
|
+
renderContent();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderSidebarCollapsedTop() {
|
|
119
|
+
const expandIcon = iconPanelLeftOpen();
|
|
120
|
+
$('#sidebarCollapsedTop').innerHTML = `
|
|
121
|
+
<button class="sidebar-toggle-btn" id="sidebarExpandBtn" type="button" aria-label="Expand sidebar">
|
|
122
|
+
${expandIcon}
|
|
123
|
+
</button>
|
|
124
|
+
`;
|
|
125
|
+
$('#sidebarExpandBtn')?.addEventListener('click', toggleSidebar);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderSidebarHeader() {
|
|
129
|
+
const toggleIcon = state.sidebarCollapsed ? iconPanelLeftOpen() : iconPanelLeftClose();
|
|
130
|
+
$('#sidebarHeader').innerHTML = `
|
|
131
|
+
<a class="brand" href="#workspace" title="Kimi Memory">
|
|
132
|
+
${iconKimiMemory()}
|
|
133
|
+
<span>Kimi Memory</span>
|
|
134
|
+
</a>
|
|
135
|
+
<button class="sidebar-toggle-btn" id="sidebarToggleBtn" type="button" aria-label="Toggle sidebar">
|
|
136
|
+
${toggleIcon}
|
|
137
|
+
</button>
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
$('#sidebarToggleBtn').addEventListener('click', toggleSidebar);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderSidebar() {
|
|
144
|
+
const currentSection = sectionFor(state.currentView);
|
|
145
|
+
const currentView = state.currentView;
|
|
56
146
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
147
|
+
$('#sidebarNav').innerHTML = navGroups
|
|
148
|
+
.map((group) => {
|
|
149
|
+
const itemsHtml = group.items
|
|
150
|
+
.map(
|
|
151
|
+
(item) => `
|
|
152
|
+
<a class="nav-item ${item.id === currentView ? 'active' : ''}" href="#${item.id}" data-view="${escapeHtml(
|
|
153
|
+
item.id,
|
|
154
|
+
)}">
|
|
155
|
+
<span class="nav-icon">${escapeHtml(item.icon)}</span>
|
|
156
|
+
<span>${escapeHtml(item.label)}</span>
|
|
157
|
+
</a>
|
|
158
|
+
`,
|
|
159
|
+
)
|
|
160
|
+
.join('');
|
|
161
|
+
const isGroupActive = group.items.some((item) => item.id === currentSection || item.id === currentView);
|
|
162
|
+
return `
|
|
163
|
+
<div class="nav-group" style="${isGroupActive ? '' : 'opacity:0.7'}">
|
|
164
|
+
<div class="nav-group-label">${escapeHtml(group.label)}</div>
|
|
165
|
+
${itemsHtml}
|
|
65
166
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
167
|
+
`;
|
|
168
|
+
})
|
|
169
|
+
.join('');
|
|
170
|
+
|
|
171
|
+
$('#sidebarNav').querySelectorAll('.nav-item').forEach((el) => {
|
|
172
|
+
el.addEventListener('click', (e) => {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
const view = el.dataset.view;
|
|
175
|
+
setHash(view);
|
|
176
|
+
applyHash();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const workspaceId = state.data.workspace?.id || '–';
|
|
181
|
+
$('#sidebarFooter').textContent = workspaceId;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderBreadcrumb() {
|
|
185
|
+
const parts = [{ label: workspaceFolderName(), hash: '#workspace' }];
|
|
186
|
+
if (state.currentView === 'themes' || state.currentView === 'theme-detail') {
|
|
187
|
+
parts.push({ label: 'Themes', hash: '#themes' });
|
|
69
188
|
}
|
|
70
189
|
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
190
|
+
if (state.currentView === 'theme-detail' && state.currentTheme) {
|
|
191
|
+
const displayName = state.data.themeDetail?.displayName || state.currentTheme;
|
|
192
|
+
parts.push({ label: displayName, hash: null });
|
|
193
|
+
} else if (state.currentView === 'decisions') {
|
|
194
|
+
parts.push({ label: 'Decisions', hash: null });
|
|
195
|
+
} else if (state.currentView === 'memories') {
|
|
196
|
+
parts.push({ label: 'Memories', hash: null });
|
|
197
|
+
} else if (state.currentView === 'settings') {
|
|
198
|
+
parts.push({ label: 'Settings', hash: null });
|
|
199
|
+
} else if (state.currentView === 'themes') {
|
|
200
|
+
// list already handled
|
|
77
201
|
}
|
|
78
202
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
203
|
+
return parts
|
|
204
|
+
.map((p, idx) => {
|
|
205
|
+
const isLast = idx === parts.length - 1;
|
|
206
|
+
const content = p.hash && !isLast
|
|
207
|
+
? `<a href="${p.hash}">${escapeHtml(p.label)}</a>`
|
|
208
|
+
: `<span>${escapeHtml(p.label)}</span>`;
|
|
209
|
+
const sep = idx > 0 ? `<span class="sep">/</span>` : '';
|
|
210
|
+
return `${sep}${content}`;
|
|
211
|
+
})
|
|
212
|
+
.join('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderTopbar() {
|
|
216
|
+
$('#topbar').innerHTML = `
|
|
217
|
+
<div class="topbar-left">
|
|
218
|
+
<button class="menu-toggle" id="menuToggle" type="button" aria-label="Toggle menu">☰</button>
|
|
219
|
+
<nav class="breadcrumb" id="breadcrumb">${renderBreadcrumb()}</nav>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="topbar-right">
|
|
222
|
+
<button class="btn btn-secondary btn-sm" id="syncTopbarBtn" type="button" title="Reconcile index.json with filesystem">Sync index</button>
|
|
223
|
+
<button class="btn btn-secondary btn-sm" id="refreshBtn" type="button" title="Refresh current view">↻ Refresh</button>
|
|
224
|
+
<span class="status-badge"><span class="status-dot"></span>Online</span>
|
|
225
|
+
</div>
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
$('#menuToggle').addEventListener('click', toggleMobileSidebar);
|
|
229
|
+
$('#syncTopbarBtn').addEventListener('click', syncIndex);
|
|
230
|
+
$('#refreshBtn').addEventListener('click', () => loadDataForView(state.currentView, state.currentTheme));
|
|
231
|
+
$('#breadcrumb').querySelectorAll('a').forEach((a) => {
|
|
232
|
+
a.addEventListener('click', (e) => {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
const href = a.getAttribute('href');
|
|
235
|
+
if (href) {
|
|
236
|
+
location.hash = href;
|
|
237
|
+
applyHash();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderContent() {
|
|
244
|
+
const content = $('#content');
|
|
245
|
+
switch (state.currentView) {
|
|
246
|
+
case 'workspace':
|
|
247
|
+
content.innerHTML = renderWorkspaceView();
|
|
248
|
+
bindWorkspaceView();
|
|
249
|
+
break;
|
|
250
|
+
case 'themes':
|
|
251
|
+
content.innerHTML = renderThemesView();
|
|
252
|
+
bindThemesView();
|
|
253
|
+
break;
|
|
254
|
+
case 'theme-detail':
|
|
255
|
+
content.innerHTML = renderThemeDetailView();
|
|
256
|
+
bindThemeDetailView();
|
|
257
|
+
break;
|
|
258
|
+
case 'decisions':
|
|
259
|
+
content.innerHTML = renderDecisionsView();
|
|
260
|
+
bindDecisionsView();
|
|
261
|
+
break;
|
|
262
|
+
case 'memories':
|
|
263
|
+
content.innerHTML = renderMemoriesView();
|
|
264
|
+
bindMemoriesView();
|
|
265
|
+
break;
|
|
266
|
+
case 'searches':
|
|
267
|
+
content.innerHTML = renderSearchesView();
|
|
268
|
+
bindSearchesView();
|
|
269
|
+
break;
|
|
270
|
+
case 'search-detail':
|
|
271
|
+
content.innerHTML = renderSearchDetailView();
|
|
272
|
+
bindSearchDetailView();
|
|
273
|
+
break;
|
|
274
|
+
case 'settings':
|
|
275
|
+
content.innerHTML = renderSettingsView();
|
|
276
|
+
bindSettingsView();
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
content.innerHTML = renderWorkspaceView();
|
|
280
|
+
bindWorkspaceView();
|
|
91
281
|
}
|
|
282
|
+
}
|
|
92
283
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
284
|
+
// ---- Workspace view ----
|
|
285
|
+
|
|
286
|
+
function renderWorkspaceView() {
|
|
287
|
+
const editing = state.editingEssence;
|
|
288
|
+
const essence = state.data.workspace?.essence || '';
|
|
289
|
+
const bodyHtml = editing
|
|
290
|
+
? `<textarea class="essence-editor" id="essenceEditor" placeholder="Workspace essence is empty. Write a short constitution here…">${escapeHtml(
|
|
291
|
+
essence,
|
|
292
|
+
)}</textarea>`
|
|
293
|
+
: `<div class="essence-content md-content" id="essenceContent">${
|
|
294
|
+
essence ? renderMarkdown(essence) : '<span class="muted">No essence yet. Click Edit to write one.</span>'
|
|
295
|
+
}</div>`;
|
|
296
|
+
const actionsHtml = editing
|
|
297
|
+
? `
|
|
298
|
+
<button class="btn btn-primary btn-sm" id="saveEssenceBtn" type="button">Save</button>
|
|
299
|
+
<button class="btn btn-secondary btn-sm" id="cancelEditEssenceBtn" type="button">Cancel</button>
|
|
300
|
+
`
|
|
301
|
+
: `<button class="btn btn-secondary btn-sm" id="editEssenceBtn" type="button">Edit</button>`;
|
|
302
|
+
return `
|
|
303
|
+
<section class="view view-active" data-view="workspace">
|
|
304
|
+
<div class="page-header">
|
|
305
|
+
<h1 class="page-title">Workspace</h1>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="stat-grid" id="statsGrid"></div>
|
|
308
|
+
<div class="composer-card">
|
|
309
|
+
<div class="composer-header">
|
|
310
|
+
<h2 class="composer-title">Workspace essence</h2>
|
|
311
|
+
<div class="page-header-actions">${actionsHtml}</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="composer-body">
|
|
314
|
+
${bodyHtml}
|
|
315
|
+
</div>
|
|
316
|
+
<div class="composer-status" id="essenceStatus"></div>
|
|
317
|
+
</div>
|
|
318
|
+
</section>
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function bindWorkspaceView() {
|
|
323
|
+
renderStats(state.data.workspace?.stats || {});
|
|
324
|
+
$('#editEssenceBtn')?.addEventListener('click', startEditEssence);
|
|
325
|
+
$('#saveEssenceBtn')?.addEventListener('click', saveEssence);
|
|
326
|
+
$('#cancelEditEssenceBtn')?.addEventListener('click', cancelEditEssence);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function startEditEssence() {
|
|
330
|
+
state.editingEssence = true;
|
|
331
|
+
renderContent();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function cancelEditEssence() {
|
|
335
|
+
state.editingEssence = false;
|
|
336
|
+
renderContent();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function renderStats(stats) {
|
|
340
|
+
const grid = $('#statsGrid');
|
|
341
|
+
if (!grid) return;
|
|
342
|
+
const entries = Object.entries(stats);
|
|
343
|
+
if (entries.length === 0) {
|
|
344
|
+
grid.innerHTML = '<div class="empty-state">Loading stats…</div>';
|
|
345
|
+
return;
|
|
105
346
|
}
|
|
347
|
+
grid.innerHTML = entries
|
|
348
|
+
.map(
|
|
349
|
+
([key, value]) => `
|
|
350
|
+
<div class="stat-card">
|
|
351
|
+
<div class="stat-value">${escapeHtml(String(value))}</div>
|
|
352
|
+
<div class="stat-label">${escapeHtml(key.replace(/([A-Z])/g, ' $1').toLowerCase())}</div>
|
|
353
|
+
</div>
|
|
354
|
+
`,
|
|
355
|
+
)
|
|
356
|
+
.join('');
|
|
357
|
+
}
|
|
106
358
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
359
|
+
async function loadWorkspace() {
|
|
360
|
+
const data = await api('/api/workspace');
|
|
361
|
+
state.data.workspace = data;
|
|
362
|
+
updateDocumentTitle();
|
|
363
|
+
renderSidebar();
|
|
364
|
+
if (state.currentView === 'workspace') {
|
|
365
|
+
renderStats(data.stats);
|
|
366
|
+
const contentEl = $('#essenceContent');
|
|
367
|
+
if (contentEl && !state.editingEssence) {
|
|
368
|
+
contentEl.innerHTML = data.essence ? renderMarkdown(data.essence) : '<span class="muted">No essence yet. Click Edit to write one.</span>';
|
|
112
369
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<div class="theme-meta">
|
|
119
|
-
<span>${theme.turnCount} turns</span>
|
|
120
|
-
<span>${theme.memoryCount} memories</span>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
`,
|
|
124
|
-
)
|
|
125
|
-
.join('');
|
|
370
|
+
}
|
|
371
|
+
if (state.currentView === 'settings') {
|
|
372
|
+
updateSettingsView();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
126
375
|
|
|
127
|
-
|
|
128
|
-
|
|
376
|
+
async function saveEssence() {
|
|
377
|
+
const content = $('#essenceEditor').value;
|
|
378
|
+
const status = $('#essenceStatus');
|
|
379
|
+
try {
|
|
380
|
+
await api('/api/essence', {
|
|
381
|
+
method: 'POST',
|
|
382
|
+
body: JSON.stringify({ content }),
|
|
129
383
|
});
|
|
384
|
+
if (state.data.workspace) state.data.workspace.essence = content;
|
|
385
|
+
state.editingEssence = false;
|
|
386
|
+
renderContent();
|
|
387
|
+
setStatus(status, 'Essence saved.', 'success');
|
|
388
|
+
} catch (err) {
|
|
389
|
+
setStatus(status, `Save failed: ${err.message}`, 'error');
|
|
130
390
|
}
|
|
391
|
+
}
|
|
131
392
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
393
|
+
async function syncIndex() {
|
|
394
|
+
const btn = $('#syncTopbarBtn');
|
|
395
|
+
if (!btn) return;
|
|
396
|
+
const original = btn.textContent;
|
|
397
|
+
btn.textContent = 'Syncing…';
|
|
398
|
+
btn.disabled = true;
|
|
399
|
+
try {
|
|
400
|
+
await api('/api/sync', { method: 'POST' });
|
|
401
|
+
await loadWorkspace();
|
|
402
|
+
btn.textContent = 'Synced';
|
|
403
|
+
} catch (err) {
|
|
404
|
+
btn.textContent = `Failed: ${err.message}`;
|
|
135
405
|
}
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
btn.textContent = original;
|
|
408
|
+
btn.disabled = false;
|
|
409
|
+
}, 1500);
|
|
410
|
+
}
|
|
136
411
|
|
|
137
|
-
|
|
138
|
-
const data = await api(`/api/themes/${encodeURIComponent(themeName)}`);
|
|
139
|
-
$('#modal-title').textContent = data.displayName || data.theme;
|
|
140
|
-
const body = $('#modal-body');
|
|
412
|
+
// ---- Themes view ----
|
|
141
413
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<
|
|
414
|
+
function renderThemesView() {
|
|
415
|
+
return `
|
|
416
|
+
<section class="view view-active" data-view="themes">
|
|
417
|
+
<div class="page-header">
|
|
418
|
+
<h1 class="page-title">Themes</h1>
|
|
419
|
+
<span class="badge" id="themeCountBadge">0 themes</span>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="search-table-wrap">
|
|
422
|
+
<table class="search-table" id="themesTable">
|
|
423
|
+
<thead>
|
|
424
|
+
<tr>
|
|
425
|
+
<th>Theme</th>
|
|
426
|
+
<th>Created</th>
|
|
427
|
+
<th>Updated</th>
|
|
428
|
+
<th class="search-number">Turns</th>
|
|
429
|
+
<th class="search-number">Memories</th>
|
|
430
|
+
<th class="search-actions">Actions</th>
|
|
431
|
+
</tr>
|
|
432
|
+
</thead>
|
|
433
|
+
<tbody id="themesTableBody"></tbody>
|
|
434
|
+
</table>
|
|
148
435
|
</div>
|
|
149
|
-
|
|
436
|
+
</section>
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function bindThemesView() {
|
|
441
|
+
renderThemes();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function renderThemes() {
|
|
445
|
+
const tbody = $('#themesTableBody');
|
|
446
|
+
const badge = $('#themeCountBadge');
|
|
447
|
+
if (!tbody) return;
|
|
448
|
+
badge.textContent = `${state.data.themes.length} theme${state.data.themes.length === 1 ? '' : 's'}`;
|
|
449
|
+
if (state.data.themes.length === 0) {
|
|
450
|
+
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No themes yet.</td></tr>';
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
tbody.innerHTML = state.data.themes
|
|
454
|
+
.map(
|
|
455
|
+
(theme) => `
|
|
456
|
+
<tr class="search-row theme-row" data-theme="${escapeHtml(theme.name)}">
|
|
457
|
+
<td class="search-query">
|
|
458
|
+
<span class="theme-icon">◆</span>
|
|
459
|
+
${escapeHtml(theme.displayName || theme.name)}
|
|
460
|
+
</td>
|
|
461
|
+
<td>${escapeHtml(theme.createdAt ? new Date(theme.createdAt).toLocaleString() : '-')}</td>
|
|
462
|
+
<td>${escapeHtml(theme.updatedAt ? new Date(theme.updatedAt).toLocaleString() : '-')}</td>
|
|
463
|
+
<td class="search-number">${theme.turnCount ?? 0}</td>
|
|
464
|
+
<td class="search-number">${theme.memoryCount ?? 0}</td>
|
|
465
|
+
<td class="search-actions">
|
|
466
|
+
<button class="btn btn-danger btn-sm" data-action="delete-theme" data-theme="${escapeHtml(
|
|
467
|
+
theme.name,
|
|
468
|
+
)}" type="button">Delete</button>
|
|
469
|
+
</td>
|
|
470
|
+
</tr>
|
|
471
|
+
`,
|
|
472
|
+
)
|
|
473
|
+
.join('');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function loadThemes() {
|
|
477
|
+
state.data.themes = await api('/api/themes');
|
|
478
|
+
if (state.currentView === 'themes') renderThemes();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderThemeDetailView() {
|
|
482
|
+
const detail = state.data.themeDetail;
|
|
483
|
+
const title = detail?.displayName || state.currentTheme || 'Theme';
|
|
484
|
+
if (!detail) {
|
|
485
|
+
return `
|
|
486
|
+
<section class="view view-active" data-view="theme-detail">
|
|
487
|
+
<div class="page-header">
|
|
488
|
+
<h1 class="page-title">${escapeHtml(title)}</h1>
|
|
489
|
+
</div>
|
|
490
|
+
<div class="empty-state">Loading theme…</div>
|
|
491
|
+
</section>
|
|
150
492
|
`;
|
|
493
|
+
}
|
|
494
|
+
return `
|
|
495
|
+
<section class="view view-active" data-view="theme-detail">
|
|
496
|
+
<div class="page-header">
|
|
497
|
+
<div>
|
|
498
|
+
<h1 class="page-title">${escapeHtml(title)}</h1>
|
|
499
|
+
<div class="muted">${escapeHtml(state.currentTheme || '')}</div>
|
|
500
|
+
</div>
|
|
501
|
+
<div class="inline-edit">
|
|
502
|
+
<input id="themeDisplayName" type="text" value="${escapeHtml(title)}" placeholder="Display name" />
|
|
503
|
+
<button id="renameThemeBtn" class="btn btn-primary btn-sm" type="button">Rename</button>
|
|
504
|
+
<div class="page-header-actions">
|
|
505
|
+
<button class="btn btn-secondary btn-sm" id="backToThemesBtn" type="button">Back</button>
|
|
506
|
+
<button class="btn btn-danger btn-sm" id="deleteThemeBtn" type="button">Delete</button>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
<div class="timeline" id="themeTimeline"></div>
|
|
511
|
+
</section>
|
|
512
|
+
`;
|
|
513
|
+
}
|
|
151
514
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<div class="timeline-card">
|
|
192
|
-
<h4>${escapeHtml(memory.title || memory.key)}</h4>
|
|
193
|
-
<p>${escapeHtml(memory.content.slice(0, 240))}${memory.content.length > 240 ? '…' : ''}</p>
|
|
194
|
-
<div class="timeline-meta">${escapeHtml(memory.folder)}/${escapeHtml(memory.key)}</div>
|
|
515
|
+
function bindThemeDetailView() {
|
|
516
|
+
renderThemeTimeline();
|
|
517
|
+
$('#renameThemeBtn')?.addEventListener('click', renameTheme);
|
|
518
|
+
$('#deleteThemeBtn')?.addEventListener('click', () => deleteTheme(state.currentTheme));
|
|
519
|
+
$('#backToThemesBtn')?.addEventListener('click', () => {
|
|
520
|
+
setHash('themes');
|
|
521
|
+
applyHash();
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function renderThemeTimeline() {
|
|
526
|
+
const timeline = $('#themeTimeline');
|
|
527
|
+
if (!timeline) return;
|
|
528
|
+
const detail = state.data.themeDetail;
|
|
529
|
+
if (!detail || !detail.items || detail.items.length === 0) {
|
|
530
|
+
timeline.innerHTML = '<div class="empty-state">No turns or memories linked to this theme.</div>';
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
timeline.innerHTML = detail.items
|
|
535
|
+
.map((item) => {
|
|
536
|
+
if (item.type === 'turn') {
|
|
537
|
+
const turn = item.data;
|
|
538
|
+
const bullets =
|
|
539
|
+
Array.isArray(turn.facts) && turn.facts.length
|
|
540
|
+
? `<ul class="decision-bullets">${turn.facts.map((f) => `<li>${escapeHtml(f)}</li>`).join('')}</ul>`
|
|
541
|
+
: '';
|
|
542
|
+
const tags = [...(turn.entities?.files || []), ...(Object.values(turn.categories || {}).flat())];
|
|
543
|
+
return `
|
|
544
|
+
<div class="timeline-item hit" data-session="${escapeHtml(turn.sessionId)}" data-turn="${turn.turnId}">
|
|
545
|
+
<div class="timeline-dot"></div>
|
|
546
|
+
<div class="timeline-card">
|
|
547
|
+
<div class="timeline-meta">${escapeHtml(turn.sessionId)} · turn ${turn.turnId}</div>
|
|
548
|
+
<h4>${escapeHtml(turn.summary || 'Untitled turn')}</h4>
|
|
549
|
+
${bullets}
|
|
550
|
+
<div class="tag-list">
|
|
551
|
+
${tags
|
|
552
|
+
.map((t) => `<span class="tag${t.includes('.') ? ' file' : ''}">${escapeHtml(t)}</span>`)
|
|
553
|
+
.join('')}
|
|
195
554
|
</div>
|
|
196
555
|
</div>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
.join('');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
$('#theme-save-name').addEventListener('click', async () => {
|
|
203
|
-
const displayName = $('#theme-display-name').value;
|
|
204
|
-
try {
|
|
205
|
-
await api(`/api/themes/${encodeURIComponent(themeName)}`, {
|
|
206
|
-
method: 'POST',
|
|
207
|
-
body: JSON.stringify({ displayName }),
|
|
208
|
-
});
|
|
209
|
-
await loadThemes();
|
|
210
|
-
$('#modal-title').textContent = displayName;
|
|
211
|
-
} catch (err) {
|
|
212
|
-
alert(`Rename failed: ${err.message}`);
|
|
556
|
+
</div>
|
|
557
|
+
`;
|
|
213
558
|
}
|
|
559
|
+
const memory = item.data;
|
|
560
|
+
return `
|
|
561
|
+
<div class="timeline-item memory">
|
|
562
|
+
<div class="timeline-header">
|
|
563
|
+
<span class="badge">Memory</span>
|
|
564
|
+
<span class="timeline-time">${formatDate(memory.timestamp)}</span>
|
|
565
|
+
</div>
|
|
566
|
+
<div class="timeline-card">
|
|
567
|
+
<h4>${escapeHtml(memory.title || memory.key)}</h4>
|
|
568
|
+
<p>${escapeHtml(memory.content.slice(0, 240))}${memory.content.length > 240 ? '…' : ''}</p>
|
|
569
|
+
<div class="timeline-meta">${escapeHtml(memory.folder)}/${escapeHtml(memory.key)}</div>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
`;
|
|
573
|
+
})
|
|
574
|
+
.join('');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function loadThemeDetail(themeName) {
|
|
578
|
+
if (!themeName) return;
|
|
579
|
+
const data = await api(`/api/themes/${encodeURIComponent(themeName)}`);
|
|
580
|
+
state.data.themeDetail = data;
|
|
581
|
+
state.currentTheme = themeName;
|
|
582
|
+
if (state.currentView === 'theme-detail') {
|
|
583
|
+
renderContent();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function renameTheme() {
|
|
588
|
+
const themeName = state.currentTheme;
|
|
589
|
+
const displayName = $('#themeDisplayName').value.trim();
|
|
590
|
+
if (!themeName || !displayName) return;
|
|
591
|
+
try {
|
|
592
|
+
await api(`/api/themes/${encodeURIComponent(themeName)}`, {
|
|
593
|
+
method: 'POST',
|
|
594
|
+
body: JSON.stringify({ displayName }),
|
|
214
595
|
});
|
|
596
|
+
if (state.data.themeDetail) state.data.themeDetail.displayName = displayName;
|
|
597
|
+
await loadThemes();
|
|
598
|
+
renderTopbar();
|
|
599
|
+
} catch (err) {
|
|
600
|
+
alert(`Rename failed: ${err.message}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
215
603
|
|
|
216
|
-
|
|
604
|
+
async function deleteTheme(themeName) {
|
|
605
|
+
if (!themeName) return;
|
|
606
|
+
if (!confirm(`Delete theme "${themeName}"?`)) return;
|
|
607
|
+
try {
|
|
608
|
+
await deleteThemeApi(themeName);
|
|
609
|
+
if (state.currentTheme === themeName) {
|
|
610
|
+
setHash('themes');
|
|
611
|
+
applyHash();
|
|
612
|
+
}
|
|
613
|
+
await loadThemes();
|
|
614
|
+
await loadWorkspace();
|
|
615
|
+
} catch (err) {
|
|
616
|
+
alert(`Delete failed: ${err.message}`);
|
|
217
617
|
}
|
|
618
|
+
}
|
|
218
619
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
620
|
+
// ---- Searches view ----
|
|
621
|
+
|
|
622
|
+
async function deleteSearchView(key) {
|
|
623
|
+
if (!key) return;
|
|
624
|
+
if (!confirm(`Delete search view "${key}"?`)) return;
|
|
625
|
+
const deleteRefinedTurns = confirm('Also delete the refined turns referenced by this view?\n\nCancel keeps the refined turns.');
|
|
626
|
+
try {
|
|
627
|
+
await deleteSearchViewApi(key, deleteRefinedTurns);
|
|
628
|
+
if (state.currentSearchKey === key) {
|
|
629
|
+
setHash('searches');
|
|
630
|
+
applyHash();
|
|
224
631
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
632
|
+
await loadSearches();
|
|
633
|
+
await loadWorkspace();
|
|
634
|
+
} catch (err) {
|
|
635
|
+
alert(`Delete failed: ${err.message}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function renderSearchesView() {
|
|
640
|
+
const searches = state.data.searches || [];
|
|
641
|
+
if (searches.length === 0) {
|
|
642
|
+
return `
|
|
643
|
+
<section class="view view-active" data-view="searches">
|
|
644
|
+
<div class="page-header">
|
|
645
|
+
<h1 class="page-title">Saved Searches</h1>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="empty-state">No saved search views yet.</div>
|
|
648
|
+
</section>
|
|
649
|
+
`;
|
|
650
|
+
}
|
|
651
|
+
const rowsHtml = searches
|
|
652
|
+
.map(
|
|
653
|
+
(s) => `
|
|
654
|
+
<tr class="search-row" data-key="${escapeHtml(s.key)}">
|
|
655
|
+
<td class="search-cell search-query">${escapeHtml(s.query || s.key)}</td>
|
|
656
|
+
<td class="search-cell">${escapeHtml(s.createdAt ? new Date(s.createdAt).toLocaleString() : '-')}</td>
|
|
657
|
+
<td class="search-cell search-number">${s.totalHits ?? s.resultCount ?? 0}</td>
|
|
658
|
+
<td class="search-cell search-number">${s.clusterCount ?? 0}</td>
|
|
659
|
+
<td class="search-actions">
|
|
660
|
+
<button class="btn btn-danger btn-sm" data-action="delete-search" data-key="${escapeHtml(
|
|
661
|
+
s.key,
|
|
662
|
+
)}" type="button">Delete</button>
|
|
663
|
+
</td>
|
|
664
|
+
</tr>
|
|
665
|
+
`,
|
|
666
|
+
)
|
|
667
|
+
.join('');
|
|
668
|
+
return `
|
|
669
|
+
<section class="view view-active" data-view="searches">
|
|
670
|
+
<div class="page-header">
|
|
671
|
+
<h1 class="page-title">Saved Searches</h1>
|
|
672
|
+
</div>
|
|
673
|
+
<div class="search-table-wrap">
|
|
674
|
+
<table class="search-table" id="searchesTable">
|
|
675
|
+
<thead>
|
|
676
|
+
<tr>
|
|
677
|
+
<th>Keywords</th>
|
|
678
|
+
<th>Created</th>
|
|
679
|
+
<th class="search-number">Hits</th>
|
|
680
|
+
<th class="search-number">Clusters</th>
|
|
681
|
+
<th class="search-actions">Actions</th>
|
|
682
|
+
</tr>
|
|
683
|
+
</thead>
|
|
684
|
+
<tbody>${rowsHtml}</tbody>
|
|
685
|
+
</table>
|
|
686
|
+
</div>
|
|
687
|
+
</section>
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function bindSearchesView() {
|
|
692
|
+
// Clicks are handled via content event delegation to avoid duplicate listeners.
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function loadSearches() {
|
|
696
|
+
try {
|
|
697
|
+
const searches = await api('/api/searches');
|
|
698
|
+
state.data.searches = Array.isArray(searches) ? searches : [];
|
|
699
|
+
} catch (err) {
|
|
700
|
+
state.data.searches = [];
|
|
701
|
+
console.error('loadSearches failed:', err);
|
|
702
|
+
}
|
|
703
|
+
if (state.currentView === 'searches') {
|
|
704
|
+
renderContent();
|
|
705
|
+
bindSearchesView();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function renderSearchDetailView() {
|
|
710
|
+
const detail = state.data.searchDetail;
|
|
711
|
+
if (!detail) {
|
|
712
|
+
return `
|
|
713
|
+
<section class="view view-active" data-view="search-detail">
|
|
714
|
+
<div class="page-header">
|
|
715
|
+
<h1 class="page-title">Search Detail</h1>
|
|
716
|
+
</div>
|
|
717
|
+
<div class="empty-state">Loading search view…</div>
|
|
718
|
+
</section>
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
const turnsHtml = (detail.turns || [])
|
|
722
|
+
.map(
|
|
723
|
+
(turn, idx) => `
|
|
724
|
+
<div class="timeline-item ${turn.isHit ? 'hit' : ''}" data-session="${escapeHtml(turn.sessionId)}" data-turn="${turn.turnId}" style="--i:${idx}">
|
|
725
|
+
<div class="timeline-dot"></div>
|
|
726
|
+
<div class="timeline-card">
|
|
727
|
+
<div class="timeline-meta">${escapeHtml(turn.sessionId)} · turn ${turn.turnId}</div>
|
|
728
|
+
<div class="timeline-summary">${escapeHtml(turn.summary || 'No summary')}</div>
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
`,
|
|
732
|
+
)
|
|
733
|
+
.join('');
|
|
734
|
+
return `
|
|
735
|
+
<section class="view view-active" data-view="search-detail">
|
|
736
|
+
<div class="page-header">
|
|
737
|
+
<div>
|
|
738
|
+
<h1 class="page-title">${escapeHtml(detail.query || detail.key)}</h1>
|
|
739
|
+
<div class="muted">${detail.totalHits ?? 0} hits · ${detail.clusterCount ?? 0} clusters · ${escapeHtml(
|
|
740
|
+
detail.createdAt ? new Date(detail.createdAt).toLocaleString() : '',
|
|
741
|
+
)}</div>
|
|
742
|
+
</div>
|
|
743
|
+
<div class="page-header-actions">
|
|
744
|
+
<button class="btn btn-secondary btn-sm" id="backToSearchesBtn" type="button">Back</button>
|
|
745
|
+
<button class="btn btn-danger btn-sm" id="deleteSearchViewBtn" type="button">Delete</button>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
<div class="timeline" id="searchTimeline">${turnsHtml || '<div class="empty-state">No refined turns.</div>'}</div>
|
|
749
|
+
</section>
|
|
750
|
+
`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function bindSearchDetailView() {
|
|
754
|
+
$('#backToSearchesBtn')?.addEventListener('click', () => {
|
|
755
|
+
setHash('searches');
|
|
756
|
+
applyHash();
|
|
757
|
+
});
|
|
758
|
+
$('#deleteSearchViewBtn')?.addEventListener('click', () => deleteSearchView(state.currentSearchKey));
|
|
759
|
+
// Timeline item clicks are handled via content event delegation.
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function loadSearchDetail() {
|
|
763
|
+
const key = state.currentSearchKey;
|
|
764
|
+
if (!key) return;
|
|
765
|
+
try {
|
|
766
|
+
const detail = await api(`/api/searches/${encodeURIComponent(key)}`);
|
|
767
|
+
state.data.searchDetail = detail;
|
|
768
|
+
} catch (err) {
|
|
769
|
+
state.data.searchDetail = null;
|
|
770
|
+
console.error('loadSearchDetail failed:', err);
|
|
771
|
+
}
|
|
772
|
+
if (state.currentView === 'search-detail') {
|
|
773
|
+
renderContent();
|
|
774
|
+
bindSearchDetailView();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ---- Decisions view ----
|
|
779
|
+
|
|
780
|
+
function renderList(items, empty) {
|
|
781
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
782
|
+
return `<p class="muted">${escapeHtml(empty)}</p>`;
|
|
783
|
+
}
|
|
784
|
+
return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function showRefinedTurnModal(sessionId, turnId) {
|
|
788
|
+
try {
|
|
789
|
+
const turn = await api(`/api/refined-turn/${encodeURIComponent(sessionId)}/${turnId}`);
|
|
790
|
+
const factsHtml = renderList(turn.facts, 'No facts');
|
|
791
|
+
const notesHtml = renderList(turn.notes, 'No notes');
|
|
792
|
+
const modal = document.createElement('div');
|
|
793
|
+
modal.className = 'modal-backdrop';
|
|
794
|
+
modal.innerHTML = `
|
|
795
|
+
<div class="modal">
|
|
796
|
+
<div class="modal-header">
|
|
797
|
+
<h3>${escapeHtml(sessionId)} · turn ${turnId}</h3>
|
|
798
|
+
<button class="modal-close" type="button">×</button>
|
|
799
|
+
</div>
|
|
800
|
+
<div class="modal-body">
|
|
801
|
+
<div class="muted">${escapeHtml(turn.timestamp ? new Date(turn.timestamp).toLocaleString() : '')}</div>
|
|
802
|
+
<h4>Summary</h4>
|
|
803
|
+
<p>${escapeHtml(turn.summary || 'No summary')}</p>
|
|
804
|
+
<h4>Facts</h4>
|
|
805
|
+
${factsHtml}
|
|
806
|
+
<h4>Notes</h4>
|
|
807
|
+
${notesHtml}
|
|
808
|
+
</div>
|
|
809
|
+
</div>
|
|
810
|
+
`;
|
|
811
|
+
document.body.appendChild(modal);
|
|
812
|
+
modal.querySelector('.modal-close').addEventListener('click', () => modal.remove());
|
|
813
|
+
modal.addEventListener('click', (e) => {
|
|
814
|
+
if (e.target === modal) modal.remove();
|
|
815
|
+
});
|
|
816
|
+
} catch (err) {
|
|
817
|
+
alert(`Failed to load turn: ${err.message}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function renderDecisionsView() {
|
|
822
|
+
return `
|
|
823
|
+
<section class="view view-active" data-view="decisions">
|
|
824
|
+
<div class="page-header">
|
|
825
|
+
<h1 class="page-title">Recent Decisions</h1>
|
|
826
|
+
<input type="search" class="search-input" id="decisionsFilter" placeholder="Filter decisions…" value="${escapeHtml(
|
|
827
|
+
state.decisionsFilter,
|
|
828
|
+
)}" />
|
|
829
|
+
</div>
|
|
830
|
+
<div class="decisions-list" id="decisionsList"></div>
|
|
831
|
+
</section>
|
|
832
|
+
`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function bindDecisionsView() {
|
|
836
|
+
$('#decisionsFilter').addEventListener('input', (e) => {
|
|
837
|
+
state.decisionsFilter = e.target.value.toLowerCase();
|
|
838
|
+
renderDecisions();
|
|
839
|
+
});
|
|
840
|
+
renderDecisions();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function renderDecisions() {
|
|
844
|
+
const list = $('#decisionsList');
|
|
845
|
+
if (!list) return;
|
|
846
|
+
const filtered = state.data.decisions.filter((d) => {
|
|
847
|
+
if (!state.decisionsFilter) return true;
|
|
848
|
+
const hay = `${d.summary} ${d.decisions.join(' ')} ${d.files.join(' ')} ${d.tags.join(' ')}`.toLowerCase();
|
|
849
|
+
return hay.includes(state.decisionsFilter);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (filtered.length === 0) {
|
|
853
|
+
list.innerHTML = '<div class="empty-state">No matching decisions.</div>';
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
list.innerHTML = filtered
|
|
858
|
+
.map(
|
|
859
|
+
(d) => `
|
|
860
|
+
<div class="decision-row">
|
|
861
|
+
<div class="decision-row-main">
|
|
862
|
+
<div class="decision-row-title">Turn ${d.turnId} · ${escapeHtml(d.sessionId)}</div>
|
|
863
|
+
<div class="decision-row-summary">${escapeHtml(d.summary)}</div>
|
|
237
864
|
<div class="tag-list">
|
|
865
|
+
${d.decisions.map((dec) => `<span class="tag">${escapeHtml(dec)}</span>`).join('')}
|
|
238
866
|
${d.files.map((f) => `<span class="tag file">${escapeHtml(f)}</span>`).join('')}
|
|
239
867
|
${d.tags.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
|
|
240
868
|
</div>
|
|
241
869
|
</div>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
870
|
+
<div class="decision-row-meta">
|
|
871
|
+
<div class="decision-time">${formatDate(d.timestamp)}</div>
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
`,
|
|
875
|
+
)
|
|
876
|
+
.join('');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function loadDecisions() {
|
|
880
|
+
state.data.decisions = await api('/api/decisions?limit=50');
|
|
881
|
+
if (state.currentView === 'decisions') renderDecisions();
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ---- Memories view ----
|
|
885
|
+
|
|
886
|
+
function renderMemoriesView() {
|
|
887
|
+
return `
|
|
888
|
+
<section class="view view-active" data-view="memories">
|
|
889
|
+
<div class="page-header">
|
|
890
|
+
<h1 class="page-title">Memories</h1>
|
|
891
|
+
<button class="btn btn-primary btn-sm" id="newFolderTopBtn" type="button">+ New folder</button>
|
|
892
|
+
</div>
|
|
893
|
+
<div class="memories-layout">
|
|
894
|
+
<div class="folder-tree" id="folderTree">
|
|
895
|
+
<div class="empty-state">Loading folders…</div>
|
|
896
|
+
</div>
|
|
897
|
+
<div class="memory-editor" id="memoryEditor">
|
|
898
|
+
<div class="empty-state">Select a folder or file to get started.</div>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
</section>
|
|
902
|
+
`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function bindMemoriesView() {
|
|
906
|
+
renderFolderTree();
|
|
907
|
+
renderMemoryEditor();
|
|
908
|
+
$('#newFolderTopBtn')?.addEventListener('click', () => createFolderPrompt(state.selectedMemoryFolder));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function renderFolderRow(node, path, depth = 0) {
|
|
912
|
+
const isRoot = path === node.name;
|
|
913
|
+
const isSelected = state.selectedMemoryFolder === path;
|
|
914
|
+
const childrenHtml = (node.children || [])
|
|
915
|
+
.map((child) => renderFolderRow(child, `${path}/${child.name}`, depth + 1))
|
|
916
|
+
.join('');
|
|
917
|
+
|
|
918
|
+
const files = Array.isArray(node.files) ? node.files : [];
|
|
919
|
+
const filesHtml = files.length
|
|
920
|
+
? `<div class="tree-file-list">
|
|
921
|
+
${files
|
|
922
|
+
.map(
|
|
923
|
+
(file) => `
|
|
924
|
+
<div class="tree-file-row ${
|
|
925
|
+
state.selectedMemoryFile?.folder === path && state.selectedMemoryFile?.key === file.key
|
|
926
|
+
? 'active'
|
|
927
|
+
: ''
|
|
928
|
+
}" data-folder="${escapeHtml(path)}" data-key="${escapeHtml(file.key)}" style="padding-left:${
|
|
929
|
+
12 + (depth + 1) * 14
|
|
930
|
+
}px">
|
|
931
|
+
<span class="file-icon">📄</span>
|
|
932
|
+
<span class="file-name">${escapeHtml(file.title || file.key)}</span>
|
|
933
|
+
</div>
|
|
934
|
+
`,
|
|
935
|
+
)
|
|
936
|
+
.join('')}
|
|
937
|
+
</div>`
|
|
938
|
+
: '';
|
|
939
|
+
|
|
940
|
+
return `
|
|
941
|
+
<div class="folder-branch" data-folder="${escapeHtml(path)}">
|
|
942
|
+
<div class="folder-row ${isSelected ? 'active' : ''}" style="padding-left:${12 + depth * 14}px">
|
|
943
|
+
<span class="folder-row-main" data-folder="${escapeHtml(path)}">
|
|
944
|
+
<span class="folder-icon">${isRoot ? '📁' : '📂'}</span>
|
|
945
|
+
<span class="folder-name">${escapeHtml(node.name || 'root')}</span>
|
|
946
|
+
${files.length > 0 ? `<span class="folder-count">${files.length}</span>` : ''}
|
|
947
|
+
</span>
|
|
948
|
+
<span class="folder-actions">
|
|
949
|
+
<button class="icon-btn" data-action="new-file" data-folder="${escapeHtml(path)}" title="New file">✚</button>
|
|
950
|
+
<button class="icon-btn" data-action="rename" data-folder="${escapeHtml(path)}" title="Rename">✎</button>
|
|
951
|
+
${!isRoot ? `<button class="icon-btn" data-action="delete" data-folder="${escapeHtml(path)}" title="Delete">🗑</button>` : ''}
|
|
952
|
+
</span>
|
|
953
|
+
</div>
|
|
954
|
+
${filesHtml}
|
|
955
|
+
${childrenHtml}
|
|
956
|
+
</div>
|
|
957
|
+
`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function renderFolderTree() {
|
|
961
|
+
const tree = $('#folderTree');
|
|
962
|
+
if (!tree) return;
|
|
963
|
+
if (state.memoriesError) {
|
|
964
|
+
tree.innerHTML = `<div class="empty-state" style="color:var(--err)">Failed to load memories.<br><small>${escapeHtml(state.memoriesError)}</small></div>`;
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (!state.data.memories) {
|
|
968
|
+
tree.innerHTML = '<div class="empty-state">Loading folders…</div>';
|
|
969
|
+
return;
|
|
245
970
|
}
|
|
246
971
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
972
|
+
const virtualRoot = state.data.memories;
|
|
973
|
+
const rootsHtml = (virtualRoot.children || [])
|
|
974
|
+
.map((root) => renderFolderRow(root, root.name))
|
|
975
|
+
.join('');
|
|
976
|
+
tree.innerHTML = rootsHtml || '<div class="empty-state">No folders found.</div>';
|
|
977
|
+
|
|
978
|
+
tree.querySelectorAll('.folder-row-main').forEach((el) => {
|
|
979
|
+
el.addEventListener('click', (e) => {
|
|
980
|
+
e.stopPropagation();
|
|
981
|
+
selectFolder(el.dataset.folder);
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
tree.querySelectorAll('.tree-file-row').forEach((el) => {
|
|
986
|
+
el.addEventListener('click', (e) => {
|
|
987
|
+
e.stopPropagation();
|
|
988
|
+
selectFile(el.dataset.folder, el.dataset.key);
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
tree.querySelectorAll('[data-action]').forEach((btn) => {
|
|
993
|
+
btn.addEventListener('click', (e) => {
|
|
994
|
+
e.stopPropagation();
|
|
995
|
+
const folder = btn.dataset.folder;
|
|
996
|
+
const action = btn.dataset.action;
|
|
997
|
+
if (action === 'new-file') createFilePrompt(folder);
|
|
998
|
+
else if (action === 'rename') renameFolderPrompt(folder);
|
|
999
|
+
else if (action === 'delete') deleteFolderPrompt(folder);
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function getFolderNode(root, folderPath) {
|
|
1005
|
+
const parts = folderPath.split('/');
|
|
1006
|
+
let current = root;
|
|
1007
|
+
if (!current) return null;
|
|
1008
|
+
for (const part of parts) {
|
|
1009
|
+
current = (current.children || []).find((c) => c.name === part);
|
|
1010
|
+
if (!current) return null;
|
|
250
1011
|
}
|
|
1012
|
+
return current;
|
|
1013
|
+
}
|
|
251
1014
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const childrenHtml =
|
|
271
|
-
node.children && node.children.length
|
|
272
|
-
? node.children.map((child) => renderMemoryNode(child, currentPath)).join('')
|
|
273
|
-
: '';
|
|
274
|
-
|
|
275
|
-
const commentHtml = node.comment
|
|
276
|
-
? `<div class="tree-comment">${escapeHtml(node.comment)}</div>`
|
|
277
|
-
: '';
|
|
278
|
-
|
|
279
|
-
if (!node.children?.length && !node.files?.length) {
|
|
280
|
-
return '';
|
|
281
|
-
}
|
|
1015
|
+
function renderFileList(folderPath) {
|
|
1016
|
+
const node = getFolderNode(state.data.memories, folderPath);
|
|
1017
|
+
if (!node || !node.files.length) {
|
|
1018
|
+
return '<div class="empty-state" style="min-height:120px">No files in this folder.</div>';
|
|
1019
|
+
}
|
|
1020
|
+
const filesHtml = node.files
|
|
1021
|
+
.map(
|
|
1022
|
+
(file) => `
|
|
1023
|
+
<div class="file-row ${state.selectedMemoryFile?.folder === folderPath && state.selectedMemoryFile?.key === file.key ? 'active' : ''}"
|
|
1024
|
+
data-folder="${escapeHtml(folderPath)}" data-key="${escapeHtml(file.key)}">
|
|
1025
|
+
<span class="file-icon">📄</span>
|
|
1026
|
+
<span class="file-name">${escapeHtml(file.title || file.key)}</span>
|
|
1027
|
+
</div>
|
|
1028
|
+
`,
|
|
1029
|
+
)
|
|
1030
|
+
.join('');
|
|
1031
|
+
return `<div class="file-list">${filesHtml}</div>`;
|
|
1032
|
+
}
|
|
282
1033
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
1034
|
+
function renderMemoryEditor() {
|
|
1035
|
+
const editor = $('#memoryEditor');
|
|
1036
|
+
if (!editor) return;
|
|
1037
|
+
|
|
1038
|
+
const file = state.selectedMemoryFile;
|
|
1039
|
+
let composerHtml = '';
|
|
1040
|
+
if (file) {
|
|
1041
|
+
const editing = state.editingMemory || file.isNew;
|
|
1042
|
+
const tagsHtml = (Array.isArray(file.tags) ? file.tags : [])
|
|
1043
|
+
.map((t) => `<span class="tag">${escapeHtml(t)}</span>`)
|
|
1044
|
+
.join('');
|
|
1045
|
+
const tagsEditValue = Array.isArray(file.tags) ? file.tags.join(', ') : '';
|
|
1046
|
+
const bodyHtml = editing
|
|
1047
|
+
? `
|
|
1048
|
+
<label class="field-label">Title</label>
|
|
1049
|
+
<input type="text" class="composer-input" id="memoryTitle" value="${escapeHtml(
|
|
1050
|
+
file.title || '',
|
|
1051
|
+
)}" placeholder="Memory title" />
|
|
1052
|
+
<label class="field-label">Tags</label>
|
|
1053
|
+
<input type="text" class="composer-input" id="memoryTags" value="${escapeHtml(
|
|
1054
|
+
tagsEditValue,
|
|
1055
|
+
)}" placeholder="tag1, tag2" />
|
|
1056
|
+
<label class="field-label">Content</label>
|
|
1057
|
+
<textarea class="composer-textarea" id="memoryContent" placeholder="Write markdown content…">${escapeHtml(
|
|
1058
|
+
file.content || '',
|
|
1059
|
+
)}</textarea>
|
|
1060
|
+
`
|
|
1061
|
+
: `
|
|
1062
|
+
<div class="memory-meta">
|
|
1063
|
+
<div class="memory-title">${escapeHtml(file.title || file.key)}</div>
|
|
1064
|
+
<div class="tag-list">${tagsHtml}</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div class="memory-content md-content">${
|
|
1067
|
+
file.content ? renderMarkdown(file.content) : '<span class="muted">Empty file.</span>'
|
|
1068
|
+
}</div>
|
|
1069
|
+
`;
|
|
1070
|
+
const actionsHtml = editing
|
|
1071
|
+
? `
|
|
1072
|
+
<button class="btn btn-primary btn-sm" id="saveMemoryBtn" type="button">Save</button>
|
|
1073
|
+
<button class="btn btn-secondary btn-sm" id="cancelEditMemoryBtn" type="button">Cancel</button>
|
|
1074
|
+
${!file.isNew ? '<button class="btn btn-danger btn-sm" id="deleteMemoryBtn" type="button">Delete</button>' : ''}
|
|
1075
|
+
`
|
|
1076
|
+
: `
|
|
1077
|
+
<button class="btn btn-secondary btn-sm" id="editMemoryBtn" type="button">Edit</button>
|
|
1078
|
+
<button class="btn btn-danger btn-sm" id="deleteMemoryBtn" type="button">Delete</button>
|
|
1079
|
+
`;
|
|
1080
|
+
composerHtml = `
|
|
1081
|
+
<div class="composer-card memory-composer">
|
|
1082
|
+
<div class="composer-header">
|
|
1083
|
+
<h2 class="composer-title">${escapeHtml(file.key)}</h2>
|
|
1084
|
+
<div class="page-header-actions">
|
|
1085
|
+
${actionsHtml}
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
<div class="composer-status" id="composerStatus"></div>
|
|
1089
|
+
<div class="composer-body">
|
|
1090
|
+
${bodyHtml}
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
`;
|
|
1094
|
+
} else {
|
|
1095
|
+
composerHtml = `
|
|
1096
|
+
<div class="empty-state" style="min-height:180px">
|
|
1097
|
+
<div>
|
|
1098
|
+
<div style="font-size:18px;margin-bottom:8px">📝</div>
|
|
1099
|
+
<div>Select a file to view, or choose a folder and create a new file.</div>
|
|
288
1100
|
</div>
|
|
289
|
-
${commentHtml}
|
|
290
|
-
${filesHtml}
|
|
291
|
-
${childrenHtml}
|
|
292
1101
|
</div>
|
|
293
1102
|
`;
|
|
294
1103
|
}
|
|
295
1104
|
|
|
296
|
-
|
|
297
|
-
const folder = filePath.replace(new RegExp(`/${key}$`), '');
|
|
298
|
-
const encodedFolder = encodeURIComponent(folder);
|
|
299
|
-
const encodedKey = encodeURIComponent(key);
|
|
300
|
-
const preview = $('#memory-preview');
|
|
1105
|
+
editor.innerHTML = composerHtml;
|
|
301
1106
|
|
|
1107
|
+
if (file) {
|
|
1108
|
+
$('#editMemoryBtn')?.addEventListener('click', startEditMemory);
|
|
1109
|
+
$('#saveMemoryBtn')?.addEventListener('click', saveSelectedMemory);
|
|
1110
|
+
$('#cancelEditMemoryBtn')?.addEventListener('click', cancelEditMemory);
|
|
1111
|
+
$('#deleteMemoryBtn')?.addEventListener('click', deleteSelectedMemory);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function startEditMemory() {
|
|
1116
|
+
state.editingMemory = true;
|
|
1117
|
+
renderMemoryEditor();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function cancelEditMemory() {
|
|
1121
|
+
state.editingMemory = false;
|
|
1122
|
+
if (state.selectedMemoryFile?.isNew) {
|
|
1123
|
+
state.selectedMemoryFile = null;
|
|
1124
|
+
}
|
|
1125
|
+
renderMemoryEditor();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function selectFolder(folderPath) {
|
|
1129
|
+
state.selectedMemoryFolder = folderPath;
|
|
1130
|
+
state.selectedMemoryFile = null;
|
|
1131
|
+
renderFolderTree();
|
|
1132
|
+
renderMemoryEditor();
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function selectFile(folder, key) {
|
|
1136
|
+
try {
|
|
1137
|
+
const memory = await api(`/api/memory/${encodeURIComponent(folder)}/${encodeURIComponent(key)}`);
|
|
1138
|
+
state.selectedMemoryFile = { folder, key, ...memory };
|
|
1139
|
+
state.selectedMemoryFolder = folder;
|
|
1140
|
+
state.editingMemory = false;
|
|
1141
|
+
renderFolderTree();
|
|
1142
|
+
renderMemoryEditor();
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
alert(`Failed to load memory: ${err.message}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function createFolderPrompt(parentFolder) {
|
|
1149
|
+
const parent = parentFolder && state.memoryFolders.includes(parentFolder) ? parentFolder : 'memory';
|
|
1150
|
+
const name = prompt('New folder name:', '');
|
|
1151
|
+
if (!name) return;
|
|
1152
|
+
const folderPath = `${parent}/${name.trim()}`.replace(/\/+/g, '/');
|
|
1153
|
+
try {
|
|
1154
|
+
const result = await createMemoryFolder(folderPath);
|
|
1155
|
+
if (!result.ok) throw new Error(result.error || 'Failed to create folder');
|
|
1156
|
+
state.selectedMemoryFolder = folderPath;
|
|
1157
|
+
await loadMemories();
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
alert(`Create folder failed: ${err.message}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async function renameFolderPrompt(folderPath) {
|
|
1164
|
+
const newPath = prompt('Rename folder to:', folderPath);
|
|
1165
|
+
if (!newPath || newPath === folderPath) return;
|
|
1166
|
+
try {
|
|
1167
|
+
const result = await renameMemoryFolder(folderPath, newPath);
|
|
1168
|
+
if (!result.ok) throw new Error(result.error || 'Failed to rename folder');
|
|
1169
|
+
if (state.selectedMemoryFolder === folderPath) state.selectedMemoryFolder = newPath;
|
|
1170
|
+
await loadMemories();
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
alert(`Rename folder failed: ${err.message}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
async function deleteFolderPrompt(folderPath) {
|
|
1177
|
+
if (!confirm(`Delete folder "${folderPath}" and all its contents? This cannot be undone.`)) return;
|
|
1178
|
+
try {
|
|
1179
|
+
const result = await deleteMemoryFolder(folderPath, true);
|
|
1180
|
+
if (!result.ok) throw new Error(result.error || 'Failed to delete folder');
|
|
1181
|
+
if (state.selectedMemoryFolder === folderPath) {
|
|
1182
|
+
state.selectedMemoryFolder = null;
|
|
1183
|
+
state.selectedMemoryFile = null;
|
|
1184
|
+
}
|
|
1185
|
+
await loadMemories();
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
alert(`Delete folder failed: ${err.message}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
async function createFilePrompt(folderPath) {
|
|
1192
|
+
const key = prompt('New memory key:', '');
|
|
1193
|
+
if (!key) return;
|
|
1194
|
+
state.selectedMemoryFile = { folder: folderPath, key, title: '', tags: [], content: '', isNew: true };
|
|
1195
|
+
state.selectedMemoryFolder = folderPath;
|
|
1196
|
+
state.editingMemory = true;
|
|
1197
|
+
renderFolderTree();
|
|
1198
|
+
renderMemoryEditor();
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function parseTagsInput(value) {
|
|
1202
|
+
return String(value || '')
|
|
1203
|
+
.split(',')
|
|
1204
|
+
.map((t) => t.trim())
|
|
1205
|
+
.filter(Boolean);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
async function saveSelectedMemory() {
|
|
1209
|
+
const file = state.selectedMemoryFile;
|
|
1210
|
+
if (!file) return;
|
|
1211
|
+
const title = $('#memoryTitle').value;
|
|
1212
|
+
const tags = parseTagsInput($('#memoryTags').value);
|
|
1213
|
+
const content = $('#memoryContent').value;
|
|
1214
|
+
const status = $('#composerStatus');
|
|
1215
|
+
try {
|
|
1216
|
+
const result = await writeMemory(file.folder, file.key, { content, title, tags });
|
|
1217
|
+
if (!result.ok) throw new Error(result.error || 'Failed to save memory');
|
|
1218
|
+
setStatus(status, 'Memory saved.', 'success');
|
|
1219
|
+
state.editingMemory = false;
|
|
1220
|
+
state.selectedMemoryFile = { ...file, title, tags, content, isNew: false };
|
|
1221
|
+
await loadMemories();
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
setStatus(status, `Save failed: ${err.message}`, 'error');
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function deleteSelectedMemory() {
|
|
1228
|
+
const file = state.selectedMemoryFile;
|
|
1229
|
+
if (!file) return;
|
|
1230
|
+
if (!confirm(`Delete "${file.folder}/${file.key}"? This cannot be undone.`)) return;
|
|
1231
|
+
try {
|
|
1232
|
+
const result = await deleteMemoryFile(file.folder, file.key);
|
|
1233
|
+
if (!result.ok) throw new Error(result.error || 'Failed to delete memory');
|
|
1234
|
+
state.selectedMemoryFile = null;
|
|
1235
|
+
await loadMemories();
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
alert(`Delete memory failed: ${err.message}`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function deriveFoldersFromTree(tree) {
|
|
1242
|
+
const folders = [];
|
|
1243
|
+
function walk(node, prefix) {
|
|
1244
|
+
if (!node) return;
|
|
1245
|
+
const path = prefix ? `${prefix}/${node.name}` : node.name;
|
|
1246
|
+
if (prefix || node.name) folders.push(path);
|
|
1247
|
+
for (const child of node.children || []) walk(child, path);
|
|
1248
|
+
}
|
|
1249
|
+
if (tree && Array.isArray(tree.children)) {
|
|
1250
|
+
for (const root of tree.children) walk(root, '');
|
|
1251
|
+
}
|
|
1252
|
+
return folders.sort();
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function findFirstFolderWithFiles(node) {
|
|
1256
|
+
if (!node) return null;
|
|
1257
|
+
if (Array.isArray(node.files) && node.files.length > 0 && node.name) {
|
|
1258
|
+
return node.name;
|
|
1259
|
+
}
|
|
1260
|
+
for (const child of node.children || []) {
|
|
1261
|
+
const found = findFirstFolderWithFiles(child);
|
|
1262
|
+
if (found) return node.name ? `${node.name}/${found}` : found;
|
|
1263
|
+
}
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async function loadMemories() {
|
|
1268
|
+
state.memoriesError = null;
|
|
1269
|
+
try {
|
|
1270
|
+
const tree = await api('/api/memories');
|
|
1271
|
+
state.data.memories = tree;
|
|
1272
|
+
let folders = [];
|
|
302
1273
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1274
|
+
folders = await listMemoryFolders();
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
console.warn('Failed to fetch /api/folders, deriving from tree:', err);
|
|
1277
|
+
folders = deriveFoldersFromTree(tree);
|
|
1278
|
+
}
|
|
1279
|
+
state.memoryFolders = folders;
|
|
1280
|
+
if (state.selectedMemoryFolder && !folders.includes(state.selectedMemoryFolder)) {
|
|
1281
|
+
state.selectedMemoryFolder = null;
|
|
1282
|
+
state.selectedMemoryFile = null;
|
|
1283
|
+
}
|
|
1284
|
+
if (!state.selectedMemoryFolder && tree) {
|
|
1285
|
+
const autoFolder = findFirstFolderWithFiles(tree);
|
|
1286
|
+
if (autoFolder) state.selectedMemoryFolder = autoFolder;
|
|
1287
|
+
}
|
|
1288
|
+
if (state.currentView === 'memories') {
|
|
1289
|
+
renderFolderTree();
|
|
1290
|
+
renderMemoryEditor();
|
|
1291
|
+
}
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
state.memoriesError = String(err.message || err);
|
|
1294
|
+
console.error('loadMemories failed:', err);
|
|
1295
|
+
if (state.currentView === 'memories') {
|
|
1296
|
+
renderFolderTree();
|
|
1297
|
+
renderMemoryEditor();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// ---- Settings view ----
|
|
1303
|
+
|
|
1304
|
+
function renderSettingsView() {
|
|
1305
|
+
const ws = state.data.workspace || {};
|
|
1306
|
+
const autoVis = localStorage.getItem(LS_KEY_AUTO_VIS) === 'true';
|
|
1307
|
+
return `
|
|
1308
|
+
<section class="view view-active" data-view="settings">
|
|
1309
|
+
<div class="page-header">
|
|
1310
|
+
<h1 class="page-title">Settings</h1>
|
|
1311
|
+
</div>
|
|
1312
|
+
<div class="settings-grid">
|
|
1313
|
+
<div class="card">
|
|
1314
|
+
<div class="card-header">
|
|
1315
|
+
<h2 class="card-title">Paths</h2>
|
|
1316
|
+
</div>
|
|
1317
|
+
<div class="card-body">
|
|
1318
|
+
<div class="info-row">
|
|
1319
|
+
<span class="info-label">Workspace ID</span>
|
|
1320
|
+
<code class="info-value" id="settingsWorkspaceId">${escapeHtml(ws.id || '–')}</code>
|
|
1321
|
+
</div>
|
|
1322
|
+
<div class="info-row">
|
|
1323
|
+
<span class="info-label">Workspace path</span>
|
|
1324
|
+
<code class="info-value" id="settingsCwd">${escapeHtml(ws.cwd || '–')}</code>
|
|
1325
|
+
</div>
|
|
1326
|
+
<div class="info-row">
|
|
1327
|
+
<span class="info-label">Store root</span>
|
|
1328
|
+
<code class="info-value" id="settingsStoreRoot">${escapeHtml(ws.storePath || '–')}</code>
|
|
1329
|
+
</div>
|
|
1330
|
+
<div class="info-row">
|
|
1331
|
+
<span class="info-label">MCP config hint</span>
|
|
1332
|
+
<code class="info-value">~/.kimi-code/mcp.json</code>
|
|
311
1333
|
</div>
|
|
312
1334
|
</div>
|
|
313
1335
|
</div>
|
|
314
|
-
<div class="
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
1336
|
+
<div class="card">
|
|
1337
|
+
<div class="card-header">
|
|
1338
|
+
<h2 class="card-title">Environment</h2>
|
|
1339
|
+
</div>
|
|
1340
|
+
<div class="card-body">
|
|
1341
|
+
<label class="toggle-row">
|
|
1342
|
+
<span>Auto-open dashboard (KIMI_MEMORY_AUTO_VIS)</span>
|
|
1343
|
+
<input type="checkbox" id="autoVisToggle" ${autoVis ? 'checked' : ''} />
|
|
1344
|
+
</label>
|
|
1345
|
+
<p class="help-text">When enabled, the dashboard will open automatically on startup. This toggle stores a local preference; the actual environment variable must be set in your Kimi Code/MCP configuration.</p>
|
|
1346
|
+
</div>
|
|
320
1347
|
</div>
|
|
321
|
-
<div class="
|
|
322
|
-
|
|
323
|
-
|
|
1348
|
+
<div class="card">
|
|
1349
|
+
<div class="card-header">
|
|
1350
|
+
<h2 class="card-title">Links</h2>
|
|
1351
|
+
</div>
|
|
1352
|
+
<div class="card-body">
|
|
1353
|
+
<div class="link-list">
|
|
1354
|
+
<a class="btn btn-secondary" href="https://github.com/Zehee/kimi-code-memory-mcp-server" target="_blank" rel="noopener">GitHub</a>
|
|
1355
|
+
<a class="btn btn-secondary" href="https://www.npmjs.com/package/kimi-code-memory-mcp-server" target="_blank" rel="noopener">npm</a>
|
|
1356
|
+
</div>
|
|
1357
|
+
</div>
|
|
1358
|
+
</div>
|
|
1359
|
+
</div>
|
|
1360
|
+
</section>
|
|
1361
|
+
`;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function updateSettingsView() {
|
|
1365
|
+
if (state.currentView !== 'settings') return;
|
|
1366
|
+
const ws = state.data.workspace || {};
|
|
1367
|
+
$('#settingsWorkspaceId').textContent = ws.id || '–';
|
|
1368
|
+
$('#settingsCwd').textContent = ws.cwd || '–';
|
|
1369
|
+
$('#settingsStoreRoot').textContent = ws.storePath || '–';
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function bindSettingsView() {
|
|
1373
|
+
$('#autoVisToggle').addEventListener('change', (e) => {
|
|
1374
|
+
localStorage.setItem(LS_KEY_AUTO_VIS, String(e.target.checked));
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// ---- Routing / init ----
|
|
1379
|
+
|
|
1380
|
+
function loadDataForView(view, theme, key) {
|
|
1381
|
+
switch (view) {
|
|
1382
|
+
case 'workspace':
|
|
1383
|
+
case 'settings':
|
|
1384
|
+
if (!state.data.workspace) loadWorkspace();
|
|
1385
|
+
break;
|
|
1386
|
+
case 'themes':
|
|
1387
|
+
if (state.data.themes.length === 0) loadThemes();
|
|
1388
|
+
break;
|
|
1389
|
+
case 'theme-detail':
|
|
1390
|
+
loadThemeDetail(theme);
|
|
1391
|
+
break;
|
|
1392
|
+
case 'decisions':
|
|
1393
|
+
if (state.data.decisions.length === 0) loadDecisions();
|
|
1394
|
+
break;
|
|
1395
|
+
case 'memories':
|
|
1396
|
+
if (!state.data.memories) loadMemories();
|
|
1397
|
+
break;
|
|
1398
|
+
case 'searches':
|
|
1399
|
+
if (!state.data.searches) loadSearches();
|
|
1400
|
+
break;
|
|
1401
|
+
case 'search-detail':
|
|
1402
|
+
loadSearchDetail();
|
|
1403
|
+
break;
|
|
324
1404
|
}
|
|
1405
|
+
}
|
|
325
1406
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
tree.innerHTML = renderMemoryNode(state.memories);
|
|
333
|
-
|
|
334
|
-
tree.querySelectorAll('.tree-file').forEach((fileEl) => {
|
|
335
|
-
fileEl.addEventListener('click', () => {
|
|
336
|
-
tree.querySelectorAll('.tree-file').forEach((f) => f.classList.remove('active'));
|
|
337
|
-
fileEl.classList.add('active');
|
|
338
|
-
state.activeMemoryPath = fileEl.dataset.path;
|
|
339
|
-
loadMemoryContent(fileEl.dataset.path, fileEl.dataset.key);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
1407
|
+
function handleContentClick(e) {
|
|
1408
|
+
const deleteThemeBtn = e.target.closest('[data-action="delete-theme"]');
|
|
1409
|
+
if (deleteThemeBtn) {
|
|
1410
|
+
e.stopPropagation();
|
|
1411
|
+
deleteTheme(deleteThemeBtn.dataset.theme);
|
|
1412
|
+
return;
|
|
342
1413
|
}
|
|
343
1414
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1415
|
+
const deleteSearchBtn = e.target.closest('[data-action="delete-search"]');
|
|
1416
|
+
if (deleteSearchBtn) {
|
|
1417
|
+
e.stopPropagation();
|
|
1418
|
+
deleteSearchView(deleteSearchBtn.dataset.key);
|
|
1419
|
+
return;
|
|
347
1420
|
}
|
|
348
1421
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1422
|
+
const searchRow = e.target.closest('#searchesTable .search-row');
|
|
1423
|
+
if (searchRow) {
|
|
1424
|
+
const key = searchRow.dataset.key;
|
|
1425
|
+
setHash(`searches/${key}`);
|
|
1426
|
+
applyHash();
|
|
1427
|
+
return;
|
|
352
1428
|
}
|
|
353
1429
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1430
|
+
const themeRow = e.target.closest('#themesTable .theme-row');
|
|
1431
|
+
if (themeRow) {
|
|
1432
|
+
const theme = themeRow.dataset.theme;
|
|
1433
|
+
setHash(`themes/${theme}`);
|
|
1434
|
+
applyHash();
|
|
1435
|
+
return;
|
|
357
1436
|
}
|
|
358
1437
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (view === 'themes' && state.themes.length === 0) loadThemes();
|
|
367
|
-
if (view === 'decisions' && state.decisions.length === 0) loadDecisions();
|
|
368
|
-
if (view === 'memories' && !state.memories) loadMemories();
|
|
369
|
-
});
|
|
370
|
-
});
|
|
1438
|
+
const searchTimelineItem = e.target.closest('#searchTimeline .timeline-item');
|
|
1439
|
+
if (searchTimelineItem) {
|
|
1440
|
+
const sessionId = searchTimelineItem.dataset.session;
|
|
1441
|
+
const turnId = parseInt(searchTimelineItem.dataset.turn, 10);
|
|
1442
|
+
showRefinedTurnModal(sessionId, turnId);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
371
1445
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
1446
|
+
const themeTimelineItem = e.target.closest('#themeTimeline .timeline-item[data-session]');
|
|
1447
|
+
if (themeTimelineItem) {
|
|
1448
|
+
const sessionId = themeTimelineItem.dataset.session;
|
|
1449
|
+
const turnId = parseInt(themeTimelineItem.dataset.turn, 10);
|
|
1450
|
+
showRefinedTurnModal(sessionId, turnId);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
375
1454
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
$('.modal-backdrop').addEventListener('click', closeModal);
|
|
1455
|
+
function init() {
|
|
1456
|
+
state.sidebarCollapsed = getInitialCollapsed();
|
|
1457
|
+
updateCollapsedClass();
|
|
380
1458
|
|
|
381
|
-
|
|
382
|
-
if (e.key === 'Escape') closeModal();
|
|
383
|
-
});
|
|
1459
|
+
$('#content').addEventListener('click', handleContentClick);
|
|
384
1460
|
|
|
385
|
-
|
|
386
|
-
|
|
1461
|
+
window.addEventListener('hashchange', applyHash);
|
|
1462
|
+
window.addEventListener('popstate', applyHash);
|
|
387
1463
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (initialView === 'memories') loadMemories();
|
|
392
|
-
}
|
|
1464
|
+
applyHash();
|
|
1465
|
+
loadWorkspace();
|
|
1466
|
+
}
|
|
393
1467
|
|
|
394
|
-
|
|
395
|
-
})();
|
|
1468
|
+
init();
|