devsmind-mcp 2.4.0 → 4.0.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 +317 -234
- package/dist/cli/activity.d.ts +9 -0
- package/dist/cli/activity.js +69 -0
- package/dist/cli/activity.js.map +1 -0
- package/dist/cli/describe.d.ts +31 -0
- package/dist/cli/describe.js +201 -0
- package/dist/cli/describe.js.map +1 -0
- package/dist/cli/diff.d.ts +14 -0
- package/dist/cli/diff.js +161 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/embed.d.ts +6 -0
- package/dist/cli/embed.js +109 -0
- package/dist/cli/embed.js.map +1 -0
- package/dist/cli/extract-agent.d.ts +66 -0
- package/dist/cli/extract-agent.js +192 -0
- package/dist/cli/extract-agent.js.map +1 -0
- package/dist/cli/feedback.d.ts +11 -0
- package/dist/cli/feedback.js +90 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/index.js +139 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +30 -0
- package/dist/cli/init.js +89 -18
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/integrations/memory-topics.d.ts +55 -0
- package/dist/cli/integrations/memory-topics.js +318 -0
- package/dist/cli/integrations/memory-topics.js.map +1 -0
- package/dist/cli/integrations/memory.d.ts +17 -6
- package/dist/cli/integrations/memory.js +52 -139
- package/dist/cli/integrations/memory.js.map +1 -1
- package/dist/cli/integrations/prompt.d.ts +8 -0
- package/dist/cli/integrations/prompt.js +24 -3
- package/dist/cli/integrations/prompt.js.map +1 -1
- package/dist/cli/integrations/registry.d.ts +26 -9
- package/dist/cli/integrations/registry.js +61 -30
- package/dist/cli/integrations/registry.js.map +1 -1
- package/dist/cli/llm-client.d.ts +116 -0
- package/dist/cli/llm-client.js +322 -0
- package/dist/cli/llm-client.js.map +1 -0
- package/dist/cli/rule.d.ts +22 -2
- package/dist/cli/rule.js +110 -34
- package/dist/cli/rule.js.map +1 -1
- package/dist/cli/runner.d.ts +21 -0
- package/dist/cli/runner.js +94 -276
- package/dist/cli/runner.js.map +1 -1
- package/dist/cli/sync-progress.d.ts +9 -0
- package/dist/cli/sync-progress.js +41 -0
- package/dist/cli/sync-progress.js.map +1 -0
- package/dist/cli/sync.js +5 -2
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/view.js +3 -2
- package/dist/cli/view.js.map +1 -1
- package/dist/cli/workflow.js +26 -20
- package/dist/cli/workflow.js.map +1 -1
- package/dist/db/activity-graph.d.ts +55 -0
- package/dist/db/activity-graph.js +314 -0
- package/dist/db/activity-graph.js.map +1 -0
- package/dist/db/activity.d.ts +221 -0
- package/dist/db/activity.js +348 -0
- package/dist/db/activity.js.map +1 -0
- package/dist/db/analyze.js +18 -6
- package/dist/db/analyze.js.map +1 -1
- package/dist/db/database.d.ts +654 -102
- package/dist/db/database.js +1911 -558
- package/dist/db/database.js.map +1 -1
- package/dist/db/edges.d.ts +24 -0
- package/dist/db/edges.js +84 -0
- package/dist/db/edges.js.map +1 -1
- package/dist/db/embedder.d.ts +39 -0
- package/dist/db/embedder.js +0 -0
- package/dist/db/embedder.js.map +1 -0
- package/dist/db/feedback.d.ts +128 -0
- package/dist/db/feedback.js +182 -0
- package/dist/db/feedback.js.map +1 -0
- package/dist/db/file-diff.d.ts +32 -0
- package/dist/db/file-diff.js +110 -0
- package/dist/db/file-diff.js.map +1 -0
- package/dist/db/grep.d.ts +78 -0
- package/dist/db/grep.js +475 -0
- package/dist/db/grep.js.map +1 -0
- package/dist/db/index-build.d.ts +75 -0
- package/dist/db/index-build.js +177 -0
- package/dist/db/index-build.js.map +1 -0
- package/dist/db/message-revert.d.ts +63 -0
- package/dist/db/message-revert.js +258 -0
- package/dist/db/message-revert.js.map +1 -0
- package/dist/db/revert.d.ts +31 -0
- package/dist/db/revert.js +108 -0
- package/dist/db/revert.js.map +1 -0
- package/dist/db/schema.d.ts +49 -4
- package/dist/db/schema.js +125 -73
- package/dist/db/schema.js.map +1 -1
- package/dist/db/search-index.d.ts +65 -0
- package/dist/db/search-index.js +74 -0
- package/dist/db/search-index.js.map +1 -0
- package/dist/db/staging.d.ts +91 -5
- package/dist/db/staging.js +148 -22
- package/dist/db/staging.js.map +1 -1
- package/dist/mcp/server.d.ts +29 -7
- package/dist/mcp/server.js +2637 -878
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/vendor/3d-force-graph.min.js +5 -0
- package/dist/mcp/vendor/force-graph.min.js +5 -0
- package/dist/mcp/vendor/model/model_int8.onnx +0 -0
- package/dist/mcp/vendor/model/vocab.txt +30522 -0
- package/dist/mcp/vendor/three.min.js +7 -0
- package/dist/mcp/view.css +419 -0
- package/dist/mcp/view.html +161 -0
- package/dist/mcp/view.js +245 -0
- package/dist/mcp/view_chat.js +382 -0
- package/dist/mcp/view_graph.js +576 -0
- package/dist/mcp/visualizer.d.ts +25 -2
- package/dist/mcp/visualizer.js +31 -4
- package/dist/mcp/visualizer.js.map +1 -1
- package/dist/utils/ast.d.ts +89 -0
- package/dist/utils/ast.js +378 -10
- package/dist/utils/ast.js.map +1 -1
- package/dist/utils/diff.d.ts +44 -0
- package/dist/utils/diff.js +78 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/edit.d.ts +6 -0
- package/dist/utils/edit.js +47 -14
- package/dist/utils/edit.js.map +1 -1
- package/dist/utils/scanner.d.ts +6 -4
- package/dist/utils/scanner.js +18 -8
- package/dist/utils/scanner.js.map +1 -1
- package/dist/utils/tokenize.d.ts +45 -0
- package/dist/utils/tokenize.js +129 -0
- package/dist/utils/tokenize.js.map +1 -0
- package/dist/utils/version.d.ts +14 -0
- package/dist/utils/version.js +61 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +18 -5
- package/dist/mcp/visualizer_2d.html +0 -635
- package/dist/mcp/visualizer_3d.html +0 -613
package/dist/mcp/view.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shell for the DevsMind view app: tab switching, the ?path= param every fetch carries,
|
|
3
|
+
* a fetch helper, HTML escaping, a reusable floating-filter toggle, and the diff-line renderer
|
|
4
|
+
* shared by both view_chat.js (message diffs) and view_graph.js (per-node history diffs).
|
|
5
|
+
*
|
|
6
|
+
* view_chat.js and view_graph.js each register an init function here; the shell calls the
|
|
7
|
+
* relevant one lazily, the first time a tab is opened, so the Graph section's /api/graph-data
|
|
8
|
+
* fetch never happens if the user never opens it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
12
|
+
const pathParam = urlParams.get('path') || '';
|
|
13
|
+
|
|
14
|
+
function withPath(url) {
|
|
15
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
16
|
+
return pathParam ? `${url}${sep}path=${encodeURIComponent(pathParam)}` : url;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchJSON(url, opts) {
|
|
20
|
+
const res = await fetch(withPath(url), opts);
|
|
21
|
+
const data = await res.json().catch(() => ({}));
|
|
22
|
+
if (!res.ok) throw new Error(data.error || `request failed (${res.status})`);
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function escapeHtml(text) {
|
|
27
|
+
if (text === null || text === undefined) return '';
|
|
28
|
+
return String(text)
|
|
29
|
+
.replace(/&/g, '&')
|
|
30
|
+
.replace(/</g, '<')
|
|
31
|
+
.replace(/>/g, '>')
|
|
32
|
+
.replace(/"/g, '"')
|
|
33
|
+
.replace(/'/g, ''');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatDate(iso) {
|
|
37
|
+
try {
|
|
38
|
+
return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
39
|
+
} catch { return iso || ''; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isSameDay(isoA, isoB) {
|
|
43
|
+
const a = new Date(isoA), b = new Date(isoB);
|
|
44
|
+
return a.toDateString() === b.toDateString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 'YYYY-MM-DD' in local time — the value a native <input type="date"> expects/produces. */
|
|
48
|
+
function dateKeyOf(isoOrDate) {
|
|
49
|
+
const d = isoOrDate instanceof Date ? isoOrDate : new Date(isoOrDate);
|
|
50
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function todayKey() { return dateKeyOf(new Date()); }
|
|
54
|
+
|
|
55
|
+
function addDaysKey(key, deltaDays) {
|
|
56
|
+
const d = new Date(`${key}T00:00:00`);
|
|
57
|
+
d.setDate(d.getDate() + deltaDays);
|
|
58
|
+
return dateKeyOf(d);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True if `iso`'s local date falls within [fromKey, toKey] — either bound may be null (open). */
|
|
62
|
+
function dateInRange(iso, fromKey, toKey) {
|
|
63
|
+
const key = dateKeyOf(iso);
|
|
64
|
+
if (fromKey && key < fromKey) return false;
|
|
65
|
+
if (toKey && key > toKey) return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Diff rendering (unified + side-by-side) ────────────────────────────────
|
|
70
|
+
// Every diff shown anywhere in the app (file hunks, individual edits, node-history diffs)
|
|
71
|
+
// renders through renderDiffBlock. There is exactly ONE toggle for this — the topbar's
|
|
72
|
+
// "Side by side" button — which flips every diff currently on the page at once. Per-block
|
|
73
|
+
// toggles were tried first and had a real bug: blocks whose id happened to collide (legacy
|
|
74
|
+
// edits recorded before each one had its own id) shared one DOM node, so only the first
|
|
75
|
+
// block's button actually did anything. A single page-wide control has no ids to collide on.
|
|
76
|
+
// The one exception is the full-file side panel, which is permanently locked to side-by-side
|
|
77
|
+
// (see openFullFilePanel) and sits outside this page-wide toggle entirely.
|
|
78
|
+
|
|
79
|
+
let diffViewMode = loadLocal('diff-view-mode', 'unified');
|
|
80
|
+
let diffBlockSeq = 0;
|
|
81
|
+
|
|
82
|
+
/** Red/green/context rows with an old#/new# gutter — top-to-bottom, like `git diff`. */
|
|
83
|
+
function renderUnifiedDiff(lines) {
|
|
84
|
+
if (!lines || !lines.length) return '<div class="empty-hint">No changes to show.</div>';
|
|
85
|
+
const marker = { add: '+', del: '-', ctx: '' };
|
|
86
|
+
return `<div class="diff-lines">${lines.map(l => `
|
|
87
|
+
<div class="diff-line ${l.type}">
|
|
88
|
+
<span class="line-no old">${l.old_line ?? ''}</span>
|
|
89
|
+
<span class="line-no new">${l.new_line ?? ''}</span>
|
|
90
|
+
<span class="line-marker">${marker[l.type]}</span>
|
|
91
|
+
<span class="line-text">${escapeHtml(l.text)}</span>
|
|
92
|
+
</div>
|
|
93
|
+
`).join('')}</div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Before (left) vs after (right), like a PR's split view. Deletion/addition runs that fall
|
|
97
|
+
* in the same change group share a row so they line up; unequal-length runs pad with blanks. */
|
|
98
|
+
function renderSideBySideDiff(lines) {
|
|
99
|
+
if (!lines || !lines.length) return '<div class="empty-hint">No changes to show.</div>';
|
|
100
|
+
const rows = [];
|
|
101
|
+
for (let i = 0; i < lines.length;) {
|
|
102
|
+
const l = lines[i];
|
|
103
|
+
if (l.type === 'ctx') { rows.push([l, l]); i++; continue; }
|
|
104
|
+
const dels = [];
|
|
105
|
+
while (i < lines.length && lines[i].type === 'del') dels.push(lines[i++]);
|
|
106
|
+
const adds = [];
|
|
107
|
+
while (i < lines.length && lines[i].type === 'add') adds.push(lines[i++]);
|
|
108
|
+
const max = Math.max(dels.length, adds.length);
|
|
109
|
+
for (let k = 0; k < max; k++) rows.push([dels[k] || null, adds[k] || null]);
|
|
110
|
+
}
|
|
111
|
+
const cell = (l, lineNoKey) => l
|
|
112
|
+
? `<span class="line-no">${l[lineNoKey] ?? ''}</span><span class="line-text">${escapeHtml(l.text)}</span>`
|
|
113
|
+
: `<span class="line-no"></span><span class="line-text"></span>`;
|
|
114
|
+
return `<div class="diff-sxs">${rows.map(([left, right]) => `
|
|
115
|
+
<div class="diff-sxs-row">
|
|
116
|
+
<div class="diff-sxs-cell ${left ? left.type : 'blank'}">${cell(left, 'old_line')}</div>
|
|
117
|
+
<div class="diff-sxs-cell ${right ? right.type : 'blank'}">${cell(right, 'new_line')}</div>
|
|
118
|
+
</div>
|
|
119
|
+
`).join('')}</div>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderDiffByMode(lines, mode) {
|
|
123
|
+
return mode === 'side-by-side' ? renderSideBySideDiff(lines) : renderUnifiedDiff(lines);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Diff block, no toggle of its own — mode comes from the page-wide `diffViewMode` (or
|
|
127
|
+
* `opts.mode` when `opts.locked` fixes this specific block regardless of the page-wide
|
|
128
|
+
* setting, e.g. the full-file panel). `opts.id` lets a caller address it later. The lines are
|
|
129
|
+
* kept on the block as JSON so the global toggle can re-render it with no refetch. */
|
|
130
|
+
function renderDiffBlock(lines, opts) {
|
|
131
|
+
opts = opts || {};
|
|
132
|
+
const id = opts.id || `diffblk-${++diffBlockSeq}`;
|
|
133
|
+
const mode = opts.locked ? (opts.mode || 'side-by-side') : diffViewMode;
|
|
134
|
+
return `<div class="diff-block" id="${id}" data-diff-lines='${escapeHtml(JSON.stringify(lines || []))}' data-diff-mode="${mode}"${opts.locked ? ' data-diff-locked="1"' : ''}>
|
|
135
|
+
<div class="diff-block-body">${renderDiffByMode(lines, mode)}</div>
|
|
136
|
+
</div>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Flips every diff block currently on the page (except locked ones, e.g. the full-file panel)
|
|
140
|
+
* between unified and side-by-side in one shot, and remembers the choice for diffs opened later. */
|
|
141
|
+
function setDiffViewMode(mode) {
|
|
142
|
+
diffViewMode = mode;
|
|
143
|
+
saveLocal('diff-view-mode', mode);
|
|
144
|
+
document.querySelectorAll('.diff-block:not([data-diff-locked])').forEach(block => {
|
|
145
|
+
const lines = JSON.parse(block.dataset.diffLines || '[]');
|
|
146
|
+
block.dataset.diffMode = mode;
|
|
147
|
+
block.querySelector('.diff-block-body').innerHTML = renderDiffByMode(lines, mode);
|
|
148
|
+
});
|
|
149
|
+
const btn = document.getElementById('btn-diff-mode');
|
|
150
|
+
if (btn) btn.textContent = mode === 'side-by-side' ? '≡ Unified' : '⇄ Side by side';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
document.getElementById('btn-diff-mode').textContent = diffViewMode === 'side-by-side' ? '≡ Unified' : '⇄ Side by side';
|
|
154
|
+
document.getElementById('btn-diff-mode').addEventListener('click', () => {
|
|
155
|
+
setDiffViewMode(diffViewMode === 'side-by-side' ? 'unified' : 'side-by-side');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
function toggleFilterPanel(id) {
|
|
159
|
+
document.getElementById(id).classList.toggle('hidden');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Full-file side panel ─────────────────────────────────────────────────
|
|
163
|
+
// A single shared panel (half the page width) for "view full file" — opened from Chat's
|
|
164
|
+
// per-file diff blocks. "Changes only" (the collapsed hunks) stays the inline default; this is
|
|
165
|
+
// only ever a deliberate, separate look at the whole reconstructed file. Always side-by-side,
|
|
166
|
+
// with no unified toggle — the wide, whole-file split view is the point of opening this panel;
|
|
167
|
+
// a unified rendering of an entire file belongs in "changes only", not here. Locked, so it also
|
|
168
|
+
// sits outside the page-wide diff-mode toggle.
|
|
169
|
+
|
|
170
|
+
function openFullFilePanel(filePath, diffLines) {
|
|
171
|
+
document.getElementById('full-file-path').textContent = filePath;
|
|
172
|
+
document.getElementById('full-file-body').innerHTML = renderDiffBlock(diffLines, { id: 'full-file-diff-block', mode: 'side-by-side', locked: true });
|
|
173
|
+
document.getElementById('full-file-backdrop').classList.remove('hidden');
|
|
174
|
+
document.getElementById('full-file-panel').classList.remove('hidden');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function closeFullFilePanel() {
|
|
178
|
+
document.getElementById('full-file-backdrop').classList.add('hidden');
|
|
179
|
+
document.getElementById('full-file-panel').classList.add('hidden');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
document.getElementById('full-file-close').addEventListener('click', closeFullFilePanel);
|
|
183
|
+
document.getElementById('full-file-backdrop').addEventListener('click', closeFullFilePanel);
|
|
184
|
+
|
|
185
|
+
// ── Filter persistence ──────────────────────────────────────────────────────
|
|
186
|
+
// Filter selections survive a reload/tab-switch via localStorage, scoped per-brain (?path=) so
|
|
187
|
+
// two different projects opened in the same browser don't stomp on each other's saved filters.
|
|
188
|
+
|
|
189
|
+
function saveLocal(key, value) {
|
|
190
|
+
try { localStorage.setItem(`devsmind:${pathParam}:${key}`, JSON.stringify(value)); } catch { /* private mode, quota, etc — filters just won't persist */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function loadLocal(key, fallback) {
|
|
194
|
+
try {
|
|
195
|
+
const raw = localStorage.getItem(`devsmind:${pathParam}:${key}`);
|
|
196
|
+
return raw === null ? fallback : JSON.parse(raw);
|
|
197
|
+
} catch { return fallback; }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Close an open floating filter panel on an outside click.
|
|
201
|
+
document.addEventListener('click', (e) => {
|
|
202
|
+
for (const panel of document.querySelectorAll('.filter-panel:not(.hidden)')) {
|
|
203
|
+
if (!panel.contains(e.target) && !e.target.closest('[id$="-filter-toggle"]')) {
|
|
204
|
+
panel.classList.add('hidden');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── Tabs ──────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
const sections = { chat: document.getElementById('section-chat'), graph: document.getElementById('section-graph') };
|
|
212
|
+
const tabs = { chat: document.getElementById('tab-chat'), graph: document.getElementById('tab-graph') };
|
|
213
|
+
let activeTab = 'chat';
|
|
214
|
+
const tabInitializers = {};
|
|
215
|
+
const tabInitialized = { chat: false, graph: false };
|
|
216
|
+
|
|
217
|
+
/** view_chat.js / view_graph.js register their one-time init here. */
|
|
218
|
+
function registerTab(name, initFn) {
|
|
219
|
+
tabInitializers[name] = initFn;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function switchTab(name) {
|
|
223
|
+
if (!sections[name]) return;
|
|
224
|
+
activeTab = name;
|
|
225
|
+
for (const key of Object.keys(sections)) {
|
|
226
|
+
sections[key].classList.toggle('hidden', key !== name);
|
|
227
|
+
tabs[key].classList.toggle('active', key === name);
|
|
228
|
+
tabs[key].setAttribute('aria-selected', key === name ? 'true' : 'false');
|
|
229
|
+
}
|
|
230
|
+
if (!tabInitialized[name] && tabInitializers[name]) {
|
|
231
|
+
tabInitialized[name] = true;
|
|
232
|
+
tabInitializers[name]();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
tabs.chat.addEventListener('click', () => switchTab('chat'));
|
|
237
|
+
tabs.graph.addEventListener('click', () => switchTab('graph'));
|
|
238
|
+
|
|
239
|
+
document.getElementById('btn-reload').addEventListener('click', () => {
|
|
240
|
+
tabInitialized[activeTab] = false;
|
|
241
|
+
switchTab(activeTab);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Kick off the default (Chat) tab once the other two scripts have registered their init fns.
|
|
245
|
+
window.addEventListener('DOMContentLoaded', () => switchTab('chat'));
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat section — a read-only history of `commit_changes` messages, grouped into the sessions
|
|
3
|
+
* `start_session` created. No input box: this shows what the AI already did, chat-bubble style,
|
|
4
|
+
* not a place to talk to it. Relies on view.js for fetchJSON/escapeHtml/renderDiffBlock/withPath/
|
|
5
|
+
* openFullFilePanel.
|
|
6
|
+
*
|
|
7
|
+
* Revert works at three granularities, all backed by the same exact-match safety guard:
|
|
8
|
+
* - whole message (/api/message-revert, /api/message-unrevert)
|
|
9
|
+
* - one file within a message (/api/message-file-revert, /api/message-file-unrevert)
|
|
10
|
+
* - one single edit (/api/message-edit-revert, /api/message-edit-unrevert)
|
|
11
|
+
*
|
|
12
|
+
* A file that took more than one edit in a message shows its INDIVIDUAL changes by default —
|
|
13
|
+
* that's the granularity revert/un-revert act on, so it's what should be in front of the user.
|
|
14
|
+
* The combined, hunk-collapsed view of the whole file's diff is one click away behind "Show
|
|
15
|
+
* combined file diff" for whoever wants the PR-style single-glance version instead.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
let chatSessions = [];
|
|
19
|
+
let chatDeveloper = null;
|
|
20
|
+
let currentSessionId = null;
|
|
21
|
+
const savedChatRange = loadLocal('chat-date-range', null);
|
|
22
|
+
let chatDateFrom = savedChatRange ? savedChatRange.from : todayKey();
|
|
23
|
+
let chatDateTo = savedChatRange ? savedChatRange.to : todayKey();
|
|
24
|
+
|
|
25
|
+
function saveChatRange() {
|
|
26
|
+
saveLocal('chat-date-range', { from: chatDateFrom, to: chatDateTo });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Which quick chip (if any) the current from/to values match — null if it's a custom range. */
|
|
30
|
+
function chatQuickChipFor(from, to) {
|
|
31
|
+
if (!from && !to) return 'clear';
|
|
32
|
+
const today = todayKey();
|
|
33
|
+
if (from === today && to === today) return 'today';
|
|
34
|
+
if (from === addDaysKey(today, -7) && to === today) return '7d';
|
|
35
|
+
if (from === addDaysKey(today, -30) && to === today) return '30d';
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function initChat() {
|
|
40
|
+
document.getElementById('chat-filter-toggle').addEventListener('click', () => toggleFilterPanel('chat-filter-panel'));
|
|
41
|
+
|
|
42
|
+
const fromEl = document.getElementById('chat-date-from');
|
|
43
|
+
const toEl = document.getElementById('chat-date-to');
|
|
44
|
+
fromEl.value = chatDateFrom || '';
|
|
45
|
+
toEl.value = chatDateTo || '';
|
|
46
|
+
fromEl.addEventListener('change', () => { chatDateFrom = fromEl.value || null; saveChatRange(); setActiveChatQuickChip(chatQuickChipFor(chatDateFrom, chatDateTo)); renderSessionList(); });
|
|
47
|
+
toEl.addEventListener('change', () => { chatDateTo = toEl.value || null; saveChatRange(); setActiveChatQuickChip(chatQuickChipFor(chatDateFrom, chatDateTo)); renderSessionList(); });
|
|
48
|
+
|
|
49
|
+
document.querySelectorAll('[data-chat-quick]').forEach(btn => {
|
|
50
|
+
btn.addEventListener('click', () => {
|
|
51
|
+
const quick = btn.dataset.chatQuick;
|
|
52
|
+
if (quick === 'clear') { chatDateFrom = null; chatDateTo = null; }
|
|
53
|
+
else if (quick === 'today') { chatDateFrom = todayKey(); chatDateTo = todayKey(); }
|
|
54
|
+
else { chatDateFrom = addDaysKey(todayKey(), -(quick === '7d' ? 7 : 30)); chatDateTo = todayKey(); }
|
|
55
|
+
fromEl.value = chatDateFrom || '';
|
|
56
|
+
toEl.value = chatDateTo || '';
|
|
57
|
+
saveChatRange();
|
|
58
|
+
setActiveChatQuickChip(quick);
|
|
59
|
+
renderSessionList();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
setActiveChatQuickChip(chatQuickChipFor(chatDateFrom, chatDateTo));
|
|
63
|
+
|
|
64
|
+
loadActivity();
|
|
65
|
+
}
|
|
66
|
+
registerTab('chat', initChat);
|
|
67
|
+
|
|
68
|
+
function setActiveChatQuickChip(quick) {
|
|
69
|
+
document.querySelectorAll('[data-chat-quick]').forEach(b => b.classList.toggle('active', b.dataset.chatQuick === quick));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function loadActivity() {
|
|
73
|
+
const listEl = document.getElementById('session-list');
|
|
74
|
+
listEl.innerHTML = '<div class="empty-hint">Loading…</div>';
|
|
75
|
+
try {
|
|
76
|
+
const data = await fetchJSON('/api/activity');
|
|
77
|
+
chatSessions = data.sessions || [];
|
|
78
|
+
chatDeveloper = data.developer;
|
|
79
|
+
document.getElementById('project-name-label').textContent = chatDeveloper ? `— ${chatDeveloper}` : '';
|
|
80
|
+
renderSessionList();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
listEl.innerHTML = `<div class="empty-hint">Failed to load: ${escapeHtml(err.message)}</div>`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sessionInRange(session, fromKey, toKey) {
|
|
87
|
+
if (!fromKey && !toKey) return true;
|
|
88
|
+
return session.messages.some(m => dateInRange(m.created_at, fromKey, toKey));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sessionLabel(session) {
|
|
92
|
+
if (session.label) return session.label;
|
|
93
|
+
const first = session.messages[0];
|
|
94
|
+
if (!first) return '(empty session)';
|
|
95
|
+
return first.request || first.summary || '(untitled change)';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderSessionList() {
|
|
99
|
+
const listEl = document.getElementById('session-list');
|
|
100
|
+
const filtered = chatSessions
|
|
101
|
+
.filter(s => sessionInRange(s, chatDateFrom, chatDateTo))
|
|
102
|
+
.slice()
|
|
103
|
+
.sort((a, b) => b.last_active.localeCompare(a.last_active));
|
|
104
|
+
|
|
105
|
+
if (!filtered.length) {
|
|
106
|
+
listEl.innerHTML = `<div class="empty-hint">No sessions in this range. Sessions are created by <code>start_session</code> — an AI agent must call DevsMind for one to show up here.</div>`;
|
|
107
|
+
document.getElementById('chat-empty').classList.remove('hidden');
|
|
108
|
+
document.getElementById('transcript').classList.add('hidden');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
listEl.innerHTML = filtered.map(s => `
|
|
113
|
+
<div class="session-item ${s.id === currentSessionId ? 'active' : ''}" data-session="${escapeHtml(s.id)}">
|
|
114
|
+
<div class="session-item-label">${escapeHtml(sessionLabel(s))}</div>
|
|
115
|
+
<div class="session-item-meta">${s.messages.length} message${s.messages.length === 1 ? '' : 's'} · ${formatDate(s.last_active)}</div>
|
|
116
|
+
</div>
|
|
117
|
+
`).join('');
|
|
118
|
+
|
|
119
|
+
listEl.querySelectorAll('.session-item').forEach(el => {
|
|
120
|
+
el.addEventListener('click', () => selectSession(el.dataset.session));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Nothing selected yet (or the selection fell out of the filtered set) — default to the newest.
|
|
124
|
+
if (!currentSessionId || !filtered.some(s => s.id === currentSessionId)) {
|
|
125
|
+
selectSession(filtered[0].id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function selectSession(id) {
|
|
130
|
+
currentSessionId = id;
|
|
131
|
+
document.querySelectorAll('.session-item').forEach(el => el.classList.toggle('active', el.dataset.session === id));
|
|
132
|
+
const session = chatSessions.find(s => s.id === id);
|
|
133
|
+
document.getElementById('chat-empty').classList.add('hidden');
|
|
134
|
+
const transcriptEl = document.getElementById('transcript');
|
|
135
|
+
transcriptEl.classList.remove('hidden');
|
|
136
|
+
if (!session) { transcriptEl.innerHTML = ''; return; }
|
|
137
|
+
transcriptEl.innerHTML = session.messages.map(renderMessagePair).join('');
|
|
138
|
+
transcriptEl.querySelectorAll('.bubble-ai').forEach(el => {
|
|
139
|
+
el.addEventListener('click', () => toggleMessageDiff(el.dataset.message));
|
|
140
|
+
});
|
|
141
|
+
transcriptEl.querySelectorAll('[data-action="revert"]').forEach(el => {
|
|
142
|
+
el.addEventListener('click', (e) => { e.stopPropagation(); doRevert(el.dataset.message); });
|
|
143
|
+
});
|
|
144
|
+
transcriptEl.querySelectorAll('[data-action="unrevert"]').forEach(el => {
|
|
145
|
+
el.addEventListener('click', (e) => { e.stopPropagation(); doUnrevert(el.dataset.message); });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderMessagePair(m) {
|
|
150
|
+
const fullyReverted = m.status === 'reverted';
|
|
151
|
+
const partial = m.status === 'partial';
|
|
152
|
+
const requestText = m.request || '(no request text captured)';
|
|
153
|
+
let action = '';
|
|
154
|
+
if (fullyReverted || partial) {
|
|
155
|
+
action = m.can_unrevert
|
|
156
|
+
? `<button class="btn" data-action="unrevert" data-message="${escapeHtml(m.id)}">↩ Un-revert${partial ? ` message (${m.reverted_edit_count})` : ''}</button>`
|
|
157
|
+
: `<span class="bubble-meta">Restore an earlier message first to un-revert this one</span>`;
|
|
158
|
+
if (!fullyReverted) {
|
|
159
|
+
action += ` <button class="btn danger" data-action="revert" data-message="${escapeHtml(m.id)}">⎌ Revert rest${m.later_applied_count ? ` (+${m.later_applied_count} after)` : ''}</button>`;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
action = `<button class="btn danger" data-action="revert" data-message="${escapeHtml(m.id)}">⎌ Revert${m.later_applied_count ? ` (+${m.later_applied_count} after)` : ''}</button>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const statusLabel = fullyReverted ? 'reverted' : partial ? `partially reverted (${m.reverted_edit_count}/${m.edit_count})` : 'applied';
|
|
166
|
+
|
|
167
|
+
return `
|
|
168
|
+
<div class="msg-pair ${fullyReverted ? 'reverted' : ''} ${partial ? 'partial' : ''}" data-message-pair="${escapeHtml(m.id)}">
|
|
169
|
+
<div class="bubble bubble-user">${escapeHtml(requestText)}</div>
|
|
170
|
+
<div class="bubble-meta">${formatDate(m.created_at)}</div>
|
|
171
|
+
<div class="bubble bubble-ai" data-message="${escapeHtml(m.id)}">
|
|
172
|
+
${escapeHtml(m.summary || '(no summary)')}
|
|
173
|
+
<div class="bubble-meta">${m.edit_count} change${m.edit_count === 1 ? '' : 's'} · ${statusLabel} · click to see diff</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="msg-actions">${action}</div>
|
|
176
|
+
<div class="file-diff-container hidden" id="diff-${escapeHtml(m.id)}"></div>
|
|
177
|
+
</div>
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function toggleMessageDiff(messageId) {
|
|
182
|
+
const container = document.getElementById(`diff-${messageId}`);
|
|
183
|
+
if (!container) return;
|
|
184
|
+
if (!container.classList.contains('hidden')) {
|
|
185
|
+
container.classList.add('hidden');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
container.classList.remove('hidden');
|
|
189
|
+
if (container.dataset.loaded) return;
|
|
190
|
+
await loadMessageDiffInto(container, messageId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Fetches + renders one message's per-file diff blocks, wiring up every button inside them. */
|
|
194
|
+
async function loadMessageDiffInto(container, messageId) {
|
|
195
|
+
container.innerHTML = '<div class="empty-hint">Loading diff…</div>';
|
|
196
|
+
try {
|
|
197
|
+
const data = await fetchJSON(`/api/message-file-diff?message_id=${encodeURIComponent(messageId)}`);
|
|
198
|
+
container.dataset.loaded = '1';
|
|
199
|
+
container.innerHTML = data.files.map((f, idx) => renderFileDiffBlock(f, idx, messageId)).join('');
|
|
200
|
+
|
|
201
|
+
container.querySelectorAll('[data-file-toggle]').forEach(head => {
|
|
202
|
+
head.addEventListener('click', (e) => {
|
|
203
|
+
if (e.target.closest('.file-diff-actions')) return;
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
const body = container.querySelector(`#${CSS.escape(head.dataset.fileToggle)}`);
|
|
206
|
+
if (!body) return;
|
|
207
|
+
const collapsed = body.classList.toggle('hidden');
|
|
208
|
+
head.classList.toggle('expanded', !collapsed);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
container.querySelectorAll('[data-open-full]').forEach(btn => {
|
|
212
|
+
btn.addEventListener('click', (e) => {
|
|
213
|
+
e.stopPropagation();
|
|
214
|
+
openFullFilePanel(btn.dataset.openFull, JSON.parse(btn.dataset.fullHunks));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
container.querySelectorAll('[data-toggle-edits]').forEach(btn => {
|
|
218
|
+
btn.addEventListener('click', (e) => {
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
const list = container.querySelector(`[data-edits-list="${btn.dataset.toggleEdits}"]`);
|
|
221
|
+
list.classList.toggle('hidden');
|
|
222
|
+
btn.textContent = list.classList.contains('hidden')
|
|
223
|
+
? btn.dataset.labelClosed
|
|
224
|
+
: btn.dataset.labelOpen;
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
container.querySelectorAll('[data-file-revert]').forEach(btn => {
|
|
228
|
+
btn.addEventListener('click', (e) => { e.stopPropagation(); doFileRevert(messageId, btn.dataset.fileRevert); });
|
|
229
|
+
});
|
|
230
|
+
container.querySelectorAll('[data-file-unrevert]').forEach(btn => {
|
|
231
|
+
btn.addEventListener('click', (e) => { e.stopPropagation(); doFileUnrevert(messageId, btn.dataset.fileUnrevert); });
|
|
232
|
+
});
|
|
233
|
+
container.querySelectorAll('[data-edit-revert]').forEach(btn => {
|
|
234
|
+
btn.addEventListener('click', (e) => { e.stopPropagation(); doEditRevert(messageId, btn.dataset.editRevert); });
|
|
235
|
+
});
|
|
236
|
+
container.querySelectorAll('[data-edit-unrevert]').forEach(btn => {
|
|
237
|
+
btn.addEventListener('click', (e) => { e.stopPropagation(); doEditUnrevert(messageId, btn.dataset.editUnrevert); });
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
container.innerHTML = `<div class="empty-hint">Failed to load diff: ${escapeHtml(err.message)}</div>`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderFileDiffBlock(file, idx, messageId) {
|
|
245
|
+
const blockId = `${messageId}-${idx}`;
|
|
246
|
+
const bodyId = `${blockId}-body`;
|
|
247
|
+
|
|
248
|
+
if (file.drifted) {
|
|
249
|
+
const body = `
|
|
250
|
+
<div class="drift-note">⚠ ${escapeHtml(file.drift_reason || 'file changed since — showing per-change diffs')}</div>
|
|
251
|
+
${(file.per_node || []).map(n => `
|
|
252
|
+
<div class="file-diff-head"><span class="path">${escapeHtml(n.node_id)}</span></div>
|
|
253
|
+
${renderDiffBlock(n.lines)}
|
|
254
|
+
`).join('')}
|
|
255
|
+
`;
|
|
256
|
+
return `
|
|
257
|
+
<div class="file-diff-block">
|
|
258
|
+
<div class="file-diff-head file-collapse-toggle" data-file-toggle="${bodyId}">
|
|
259
|
+
<span class="collapse-chevron">▸</span>
|
|
260
|
+
<span class="path">${escapeHtml(file.file_path)}</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="file-diff-body hidden" id="${bodyId}">${body}</div>
|
|
263
|
+
</div>
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const edits = file.edits || [];
|
|
268
|
+
const appliedCount = edits.filter(e => !e.reverted).length;
|
|
269
|
+
const revertedCount = edits.filter(e => e.reverted).length;
|
|
270
|
+
const fileActions = [
|
|
271
|
+
appliedCount > 0 ? `<button class="btn danger small" data-file-revert="${escapeHtml(file.file_path)}">⎌ Revert this file (${appliedCount})</button>` : '',
|
|
272
|
+
revertedCount > 0 ? `<button class="btn small" data-file-unrevert="${escapeHtml(file.file_path)}">↩ Un-revert this file (${revertedCount})</button>` : ''
|
|
273
|
+
].filter(Boolean).join(' ');
|
|
274
|
+
|
|
275
|
+
const head = `
|
|
276
|
+
<div class="file-diff-head file-collapse-toggle" data-file-toggle="${bodyId}">
|
|
277
|
+
<span class="collapse-chevron">▸</span>
|
|
278
|
+
<span class="path">${escapeHtml(file.file_path)}</span>
|
|
279
|
+
<span class="file-diff-meta">${edits.length ? `${edits.length} change${edits.length === 1 ? '' : 's'}` : ''}</span>
|
|
280
|
+
<span class="file-diff-actions">
|
|
281
|
+
${fileActions}
|
|
282
|
+
<button class="btn-icon small" data-open-full="${escapeHtml(file.file_path)}" data-full-hunks='${escapeHtml(JSON.stringify(file.full_hunks))}'>📄 View full file</button>
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
`;
|
|
286
|
+
|
|
287
|
+
// One edit -> the individual and combined views are identical, so just show the one diff.
|
|
288
|
+
if (edits.length <= 1) {
|
|
289
|
+
return `<div class="file-diff-block">${head}<div class="file-diff-body hidden" id="${bodyId}">${renderDiffBlock(file.hunks, { id: `${blockId}-only` })}</div></div>`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// More than one edit -> default to the INDIVIDUAL changes (that's the granularity revert/
|
|
293
|
+
// un-revert act on); the combined, hunk-collapsed file view is a click away behind a toggle.
|
|
294
|
+
const editRows = edits.map(e => `
|
|
295
|
+
<div class="single-edit-row">
|
|
296
|
+
<div class="single-edit-meta">
|
|
297
|
+
<span class="mono">${escapeHtml(e.node_id)}</span>
|
|
298
|
+
<span class="bubble-meta">${formatDate(e.at)} · ${e.reverted ? 'reverted' : 'applied'}</span>
|
|
299
|
+
${e.reverted
|
|
300
|
+
? `<button class="btn-icon small" data-edit-unrevert="${escapeHtml(e.id)}">↩ Un-revert this change</button>`
|
|
301
|
+
: `<button class="btn-icon small" data-edit-revert="${escapeHtml(e.id)}">⎌ Revert this change</button>`}
|
|
302
|
+
</div>
|
|
303
|
+
${renderDiffBlock(e.lines, { id: `${blockId}-edit-${escapeHtml(e.id)}` })}
|
|
304
|
+
</div>
|
|
305
|
+
`).join('');
|
|
306
|
+
|
|
307
|
+
const combinedId = `${blockId}-combined`;
|
|
308
|
+
const body = `
|
|
309
|
+
<div class="single-edits-list">${editRows}</div>
|
|
310
|
+
<button class="btn-icon small link-btn" data-toggle-edits="${combinedId}" data-label-closed="▸ Show combined file diff (${edits.length} changes)" data-label-open="▾ Hide combined file diff">▸ Show combined file diff (${edits.length} changes)</button>
|
|
311
|
+
<div class="hidden combined-file-diff" data-edits-list="${combinedId}">${renderDiffBlock(file.hunks, { id: `${combinedId}-block` })}</div>
|
|
312
|
+
`;
|
|
313
|
+
return `
|
|
314
|
+
<div class="file-diff-block">
|
|
315
|
+
${head}
|
|
316
|
+
<div class="file-diff-body hidden" id="${bodyId}">${body}</div>
|
|
317
|
+
</div>
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function postWithToken(url, body) {
|
|
322
|
+
const res = await fetch(withPath(url), {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: { 'Content-Type': 'application/json', 'X-Devsmind-Token': DEVSMIND_TOKEN },
|
|
325
|
+
body: JSON.stringify(body)
|
|
326
|
+
});
|
|
327
|
+
const data = await res.json().catch(() => ({}));
|
|
328
|
+
return { ok: res.ok, status: res.status, data };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** After any revert/un-revert action: refresh the transcript (status/buttons) and reload the
|
|
332
|
+
* diff panel for the message just acted on, so the user doesn't lose their place. */
|
|
333
|
+
async function refreshChatAfterAction(messageId) {
|
|
334
|
+
const data = await fetchJSON('/api/activity');
|
|
335
|
+
chatSessions = data.sessions || [];
|
|
336
|
+
if (currentSessionId) selectSession(currentSessionId);
|
|
337
|
+
const container = document.getElementById(`diff-${messageId}`);
|
|
338
|
+
if (container) {
|
|
339
|
+
container.classList.remove('hidden');
|
|
340
|
+
delete container.dataset.loaded;
|
|
341
|
+
await loadMessageDiffInto(container, messageId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function doRevert(messageId) {
|
|
346
|
+
if (!confirm('Revert this message? This restores the code to before it, and reverts any later messages built on top of it too.')) return;
|
|
347
|
+
const r = await postWithToken('/api/message-revert', { message_id: messageId });
|
|
348
|
+
if (!r.data.ok && !r.ok) { alert(`Revert failed: ${r.data.blocked_at ? r.data.blocked_at.reason : r.data.error || 'unknown error'}`); }
|
|
349
|
+
await loadActivity();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function doUnrevert(messageId) {
|
|
353
|
+
const r = await postWithToken('/api/message-unrevert', { message_id: messageId });
|
|
354
|
+
if (!r.ok) { alert(`Un-revert failed: ${r.data.error || 'unknown error'}`); }
|
|
355
|
+
await loadActivity();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function doFileRevert(messageId, filePath) {
|
|
359
|
+
if (!confirm(`Revert every change this message made to ${filePath}? The rest of this message stays applied.`)) return;
|
|
360
|
+
const r = await postWithToken('/api/message-file-revert', { message_id: messageId, file_path: filePath });
|
|
361
|
+
if (!r.ok) { alert(`Revert failed: ${r.data.error || 'unknown error'}`); }
|
|
362
|
+
await refreshChatAfterAction(messageId);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function doFileUnrevert(messageId, filePath) {
|
|
366
|
+
const r = await postWithToken('/api/message-file-unrevert', { message_id: messageId, file_path: filePath });
|
|
367
|
+
if (!r.ok) { alert(`Un-revert failed: ${r.data.error || 'unknown error'}`); }
|
|
368
|
+
await refreshChatAfterAction(messageId);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function doEditRevert(messageId, editId) {
|
|
372
|
+
if (!confirm('Revert just this one change?')) return;
|
|
373
|
+
const r = await postWithToken('/api/message-edit-revert', { message_id: messageId, edit_id: editId });
|
|
374
|
+
if (!r.ok) { alert(`Revert failed: ${r.data.error || 'unknown error'}`); }
|
|
375
|
+
await refreshChatAfterAction(messageId);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function doEditUnrevert(messageId, editId) {
|
|
379
|
+
const r = await postWithToken('/api/message-edit-unrevert', { message_id: messageId, edit_id: editId });
|
|
380
|
+
if (!r.ok) { alert(`Un-revert failed: ${r.data.error || 'unknown error'}`); }
|
|
381
|
+
await refreshChatAfterAction(messageId);
|
|
382
|
+
}
|