opencode-rag-plugin 1.17.3 → 1.18.1
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 +41 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/init-helpers.d.ts +5 -1
- package/dist/cli/commands/init-helpers.js +12 -19
- package/dist/cli/commands/init.js +42 -8
- package/dist/cli/commands/quirk.d.ts +10 -0
- package/dist/cli/commands/quirk.js +141 -0
- package/dist/cli/commands/setup.js +15 -6
- package/dist/cli/commands/ui.js +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/core/config.d.ts +30 -0
- package/dist/core/config.js +26 -2
- package/dist/core/interfaces.d.ts +25 -1
- package/dist/core/runtime-overrides.d.ts +7 -0
- package/dist/core/runtime-overrides.js +15 -0
- package/dist/describer/anthropic.d.ts +4 -0
- package/dist/describer/anthropic.js +9 -2
- package/dist/describer/describer.d.ts +4 -0
- package/dist/describer/describer.js +8 -0
- package/dist/describer/gemini.d.ts +3 -0
- package/dist/describer/gemini.js +8 -2
- package/dist/indexer/pipeline.js +40 -0
- package/dist/opencode/system-guidance.d.ts +34 -0
- package/dist/opencode/system-guidance.js +127 -0
- package/dist/opencode/tools.d.ts +38 -0
- package/dist/opencode/tools.js +127 -0
- package/dist/plugin.js +204 -31
- package/dist/quirks/auto-capture.d.ts +14 -0
- package/dist/quirks/auto-capture.js +112 -0
- package/dist/quirks/monitor.d.ts +5 -0
- package/dist/quirks/monitor.js +23 -0
- package/dist/quirks/prompts.d.ts +1 -0
- package/dist/quirks/prompts.js +13 -0
- package/dist/quirks/quirk-store.d.ts +27 -0
- package/dist/quirks/quirk-store.js +217 -0
- package/dist/quirks/types.d.ts +20 -0
- package/dist/quirks/types.js +2 -0
- package/dist/retriever/keyword-index.js +16 -0
- package/dist/vectorstore/lancedb.d.ts +21 -11
- package/dist/vectorstore/lancedb.js +174 -18
- package/dist/vectorstore/memory.d.ts +2 -0
- package/dist/vectorstore/memory.js +6 -0
- package/dist/web/api.d.ts +3 -1
- package/dist/web/api.js +50 -1
- package/dist/web/server.d.ts +2 -1
- package/dist/web/server.js +3 -2
- package/dist/web/ui/index.html +163 -0
- package/package.json +1 -1
package/dist/web/api.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
5
|
import { LanceDbStore } from "../vectorstore/lancedb.js";
|
|
6
6
|
import { KeywordIndex } from "../retriever/keyword-index.js";
|
|
7
|
+
import type { RagConfig } from "../core/config.js";
|
|
7
8
|
/** Internal shape for a JSON API response: an HTTP status code and a serialisable body. */
|
|
8
9
|
interface ApiResponse {
|
|
9
10
|
status: number;
|
|
@@ -23,9 +24,10 @@ interface ApiResponse {
|
|
|
23
24
|
* @param keywordIndex - The keyword-index instance for text search.
|
|
24
25
|
* @param storePath - Filesystem path to the store directory (used by eval endpoints).
|
|
25
26
|
* @param cwd - Optional workspace root for resolving file paths.
|
|
27
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
26
28
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
27
29
|
*/
|
|
28
|
-
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
30
|
+
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
29
31
|
/**
|
|
30
32
|
* Perform token-usage analysis for a single evaluation session.
|
|
31
33
|
*
|
package/dist/web/api.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { extname, resolve as resolvePathModule } from "node:path";
|
|
3
3
|
import { listSessions, getSession, deleteSession, compareSessions, validateSessionID } from "../eval/storage.js";
|
|
4
4
|
import { analyzeTokenUsage, compareTokenAnalyses, projectTokenSavings } from "../eval/token-analysis.js";
|
|
5
|
+
import { listQuirks, lintQuirks, removeQuirk } from "../quirks/quirk-store.js";
|
|
5
6
|
const FILE_MIME_TYPES = {
|
|
6
7
|
".png": "image/png",
|
|
7
8
|
".jpg": "image/jpeg",
|
|
@@ -11,6 +12,13 @@ const FILE_MIME_TYPES = {
|
|
|
11
12
|
".bmp": "image/bmp",
|
|
12
13
|
".svg": "image/svg+xml",
|
|
13
14
|
};
|
|
15
|
+
/** No-op embedder used by quirk endpoints that never need to embed text in the read-only UI context. */
|
|
16
|
+
const stubEmbedder = {
|
|
17
|
+
name: "stub",
|
|
18
|
+
embed: async () => {
|
|
19
|
+
throw new Error("Embedder is not available in the Web UI context");
|
|
20
|
+
},
|
|
21
|
+
};
|
|
14
22
|
/** Split a raw URL into its pathname and parsed query-string parameters. */
|
|
15
23
|
function parseQuery(url) {
|
|
16
24
|
const [path, queryString] = url.split("?");
|
|
@@ -43,13 +51,22 @@ function sendJson(res, response) {
|
|
|
43
51
|
* @param keywordIndex - The keyword-index instance for text search.
|
|
44
52
|
* @param storePath - Filesystem path to the store directory (used by eval endpoints).
|
|
45
53
|
* @param cwd - Optional workspace root for resolving file paths.
|
|
54
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
46
55
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
47
56
|
*/
|
|
48
|
-
export function createApiHandler(store, keywordIndex, storePath, cwd) {
|
|
57
|
+
export function createApiHandler(store, keywordIndex, storePath, cwd, cfg) {
|
|
49
58
|
return async (req, res) => {
|
|
50
59
|
const url = req.url ?? "/";
|
|
51
60
|
const method = req.method ?? "GET";
|
|
52
61
|
const { path, params } = parseQuery(url);
|
|
62
|
+
// Quirk store dependencies (embedder is a no-op stub for the read-only UI context).
|
|
63
|
+
const quirkDeps = {
|
|
64
|
+
embedder: stubEmbedder,
|
|
65
|
+
store,
|
|
66
|
+
keywordIndex,
|
|
67
|
+
cfg: cfg ?? {},
|
|
68
|
+
storePath,
|
|
69
|
+
};
|
|
53
70
|
// CORS preflight
|
|
54
71
|
if (method === "OPTIONS") {
|
|
55
72
|
res.writeHead(204, {
|
|
@@ -143,6 +160,22 @@ export function createApiHandler(store, keywordIndex, storePath, cwd) {
|
|
|
143
160
|
const body = await readBody(req);
|
|
144
161
|
response = handleEvalProjectSavings(body);
|
|
145
162
|
}
|
|
163
|
+
// Quirk memory endpoints
|
|
164
|
+
else if (path === "/api/quirks" && method === "GET") {
|
|
165
|
+
response = await handleQuirks(quirkDeps);
|
|
166
|
+
}
|
|
167
|
+
else if (path === "/api/quirks/lint" && method === "GET") {
|
|
168
|
+
response = await handleQuirkLint(quirkDeps);
|
|
169
|
+
}
|
|
170
|
+
else if (path.startsWith("/api/quirks/") && method === "DELETE") {
|
|
171
|
+
const id = path.slice("/api/quirks/".length);
|
|
172
|
+
if (!id) {
|
|
173
|
+
response = { status: 400, body: { error: "Missing quirk ID" } };
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
response = await handleQuirkDelete(quirkDeps, id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
146
179
|
else {
|
|
147
180
|
return false;
|
|
148
181
|
}
|
|
@@ -181,6 +214,22 @@ async function handleFiles(store) {
|
|
|
181
214
|
const files = await store.listFiles();
|
|
182
215
|
return { status: 200, body: files };
|
|
183
216
|
}
|
|
217
|
+
// ── Quirk Memory API ─────────────────────────────────────────────────
|
|
218
|
+
/** Respond with all stored quirks, sorted by last-observed time (most recent first). */
|
|
219
|
+
async function handleQuirks(deps) {
|
|
220
|
+
const quirks = await listQuirks(deps);
|
|
221
|
+
return { status: 200, body: { quirks } };
|
|
222
|
+
}
|
|
223
|
+
/** Respond with the health-check issues for the quirk store (confidence, staleness, duplicates). */
|
|
224
|
+
async function handleQuirkLint(deps) {
|
|
225
|
+
const issues = await lintQuirks(deps);
|
|
226
|
+
return { status: 200, body: { issues } };
|
|
227
|
+
}
|
|
228
|
+
/** Delete a single quirk by its ID from the store, index, and audit log. */
|
|
229
|
+
async function handleQuirkDelete(deps, id) {
|
|
230
|
+
await removeQuirk(deps, id);
|
|
231
|
+
return { status: 200, body: { deleted: true, id } };
|
|
232
|
+
}
|
|
184
233
|
/**
|
|
185
234
|
* Respond with a paginated, optionally filtered list of chunks.
|
|
186
235
|
*
|
package/dist/web/server.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface WebUiServer {
|
|
|
15
15
|
* @param port - TCP port to listen on.
|
|
16
16
|
* @param cwd - Optional workspace root used to resolve file paths for the file API.
|
|
17
17
|
* @param vectorDimension - Embedding vector dimension (default 384).
|
|
18
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
18
19
|
* @returns A {@link WebUiServer} handle for the running server.
|
|
19
20
|
*/
|
|
20
|
-
export declare function startWebUi(storePath: string, port: number, cwd?: string, vectorDimension?: number): Promise<WebUiServer>;
|
|
21
|
+
export declare function startWebUi(storePath: string, port: number, cwd?: string, vectorDimension?: number, cfg?: import("../core/config.js").RagConfig): Promise<WebUiServer>;
|
package/dist/web/server.js
CHANGED
|
@@ -53,13 +53,14 @@ function serveUiAsset(res, filePath) {
|
|
|
53
53
|
* @param port - TCP port to listen on.
|
|
54
54
|
* @param cwd - Optional workspace root used to resolve file paths for the file API.
|
|
55
55
|
* @param vectorDimension - Embedding vector dimension (default 384).
|
|
56
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
56
57
|
* @returns A {@link WebUiServer} handle for the running server.
|
|
57
58
|
*/
|
|
58
|
-
export async function startWebUi(storePath, port, cwd, vectorDimension = 384) {
|
|
59
|
+
export async function startWebUi(storePath, port, cwd, vectorDimension = 384, cfg) {
|
|
59
60
|
const store = new LanceDbStore(storePath, vectorDimension);
|
|
60
61
|
const keywordIndex = await KeywordIndex.load(storePath);
|
|
61
62
|
const html = getStaticHtml();
|
|
62
|
-
const apiHandler = createApiHandler(store, keywordIndex, storePath, cwd);
|
|
63
|
+
const apiHandler = createApiHandler(store, keywordIndex, storePath, cwd, cfg);
|
|
63
64
|
const server = createServer(async (req, res) => {
|
|
64
65
|
const url = req.url ?? "/";
|
|
65
66
|
if (url === "/" || url === "/index.html") {
|
package/dist/web/ui/index.html
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<button id="nav-chunks" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Chunks</button>
|
|
25
25
|
<button id="nav-files" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Files</button>
|
|
26
26
|
<button id="nav-evaluate" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Evaluate</button>
|
|
27
|
+
<button id="nav-quirks" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Quirks</button>
|
|
27
28
|
</nav>
|
|
28
29
|
<div class="flex-1"></div>
|
|
29
30
|
<div class="relative">
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
</div>
|
|
57
58
|
<div id="view-files" class="h-full overflow-y-auto scrollbar-thin"></div>
|
|
58
59
|
<div id="view-evaluate" class="hidden h-full flex flex-col"></div>
|
|
60
|
+
<div id="view-quirks" class="hidden h-full overflow-y-auto scrollbar-thin"></div>
|
|
59
61
|
</main>
|
|
60
62
|
</div>
|
|
61
63
|
|
|
@@ -85,6 +87,9 @@
|
|
|
85
87
|
async evalAnalysis(id) { const r = await fetch('/api/eval/sessions/' + encodeURIComponent(id) + '/analysis'); return r.json(); },
|
|
86
88
|
async evalTokenCompare(a, b) { const p = new URLSearchParams({a, b}); const r = await fetch('/api/eval/token-compare?' + p); return r.json(); },
|
|
87
89
|
async evalProjectSavings(params) { const r = await fetch('/api/eval/project-savings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(params) }); return r.json(); },
|
|
90
|
+
async quirks() { const r = await fetch('/api/quirks'); return r.json(); },
|
|
91
|
+
async quirkLint() { const r = await fetch('/api/quirks/lint'); return r.json(); },
|
|
92
|
+
async deleteQuirk(id) { const r = await fetch('/api/quirks/' + encodeURIComponent(id), { method: 'DELETE' }); return r.json(); },
|
|
88
93
|
};
|
|
89
94
|
|
|
90
95
|
const state = {
|
|
@@ -101,6 +106,8 @@
|
|
|
101
106
|
currentChunks: [],
|
|
102
107
|
evalSelectedSessions: new Set(),
|
|
103
108
|
evalSessionDetail: null,
|
|
109
|
+
|
|
110
|
+
quirkTypeFilter: null,
|
|
104
111
|
};
|
|
105
112
|
|
|
106
113
|
function $(sel) { return document.querySelector(sel); }
|
|
@@ -131,6 +138,21 @@
|
|
|
131
138
|
return `<span class="inline-block px-1.5 py-0.5 rounded text-xs font-mono ${langColor(lang)} bg-slate-800">${lang}</span>`;
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
function quirkTypeColor(quirkType) {
|
|
142
|
+
const map = {
|
|
143
|
+
gotcha: 'text-amber-400',
|
|
144
|
+
preference: 'text-emerald-400',
|
|
145
|
+
decision: 'text-sky-400',
|
|
146
|
+
'environment-constraint': 'text-rose-400',
|
|
147
|
+
};
|
|
148
|
+
return map[quirkType] || 'text-slate-400';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function quirkTypeBadge(quirkType) {
|
|
152
|
+
const label = quirkType || 'general';
|
|
153
|
+
return `<span class="inline-block px-1.5 py-0.5 rounded text-xs font-mono ${quirkTypeColor(quirkType)} bg-slate-800">${label}</span>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
134
156
|
function truncate(s, len=80) {
|
|
135
157
|
if (!s) return '';
|
|
136
158
|
return s.length > len ? s.slice(0, len) + '...' : s;
|
|
@@ -794,6 +816,8 @@
|
|
|
794
816
|
|
|
795
817
|
$('#nav-evaluate')?.addEventListener('click', () => { showView('evaluate'); renderEvaluate(); });
|
|
796
818
|
|
|
819
|
+
$('#nav-quirks')?.addEventListener('click', () => { showView('quirks'); renderQuirks(); });
|
|
820
|
+
|
|
797
821
|
// ── Evaluate View ──────────────────────────────────────────────────
|
|
798
822
|
|
|
799
823
|
function formatTokens(n) {
|
|
@@ -1468,6 +1492,145 @@
|
|
|
1468
1492
|
}
|
|
1469
1493
|
}
|
|
1470
1494
|
|
|
1495
|
+
// ── Quirks View ────────────────────────────────────────────────────
|
|
1496
|
+
|
|
1497
|
+
function confidenceColor(c) {
|
|
1498
|
+
if (c >= 0.75) return 'text-green-400';
|
|
1499
|
+
if (c >= 0.5) return 'text-amber-400';
|
|
1500
|
+
return 'text-red-400';
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function formatQuirkDate(iso) {
|
|
1504
|
+
if (!iso) return '-';
|
|
1505
|
+
const d = new Date(iso);
|
|
1506
|
+
if (isNaN(d.getTime())) return '-';
|
|
1507
|
+
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
async function renderQuirks() {
|
|
1511
|
+
const el = $('#view-quirks');
|
|
1512
|
+
if (!el) return;
|
|
1513
|
+
el.innerHTML = '<div class="text-center py-10 text-slate-500">Loading quirks...</div>';
|
|
1514
|
+
|
|
1515
|
+
let data;
|
|
1516
|
+
try {
|
|
1517
|
+
data = await API.quirks();
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
el.innerHTML = `<div class="text-center py-10 text-red-400">Failed to load quirks: ${escapeHtml(String(err))}</div>`;
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const quirks = data.quirks || [];
|
|
1524
|
+
const types = [...new Set(quirks.map(q => q.quirkType || 'general'))].sort();
|
|
1525
|
+
const filter = state.quirkTypeFilter;
|
|
1526
|
+
|
|
1527
|
+
const filtered = filter ? quirks.filter(q => (q.quirkType || 'general') === filter) : quirks;
|
|
1528
|
+
|
|
1529
|
+
if (quirks.length === 0) {
|
|
1530
|
+
el.innerHTML = `
|
|
1531
|
+
<div class="text-center flex-1 flex items-center justify-center py-20">
|
|
1532
|
+
<div>
|
|
1533
|
+
<div class="text-4xl mb-3">💡</div>
|
|
1534
|
+
<h2 class="text-lg font-semibold text-slate-400 mb-2">No quirks stored yet</h2>
|
|
1535
|
+
<p class="text-sm text-slate-500 max-w-md mx-auto">
|
|
1536
|
+
Quirks are experiential memories (gotchas, preferences, decisions, environment constraints)
|
|
1537
|
+
recorded by the agent via the <code>add_quirk</code> tool. They will appear here once captured.
|
|
1538
|
+
</p>
|
|
1539
|
+
</div>
|
|
1540
|
+
</div>`;
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
el.innerHTML = `
|
|
1545
|
+
<div class="flex items-center gap-3 mb-4">
|
|
1546
|
+
<h2 class="text-lg font-semibold text-white">Quirks</h2>
|
|
1547
|
+
<span class="text-sm text-slate-400">${quirks.length} total${filter ? `, ${filtered.length} shown` : ''}</span>
|
|
1548
|
+
<div class="flex-1"></div>
|
|
1549
|
+
<div class="flex items-center gap-2">
|
|
1550
|
+
<button class="quirk-filter px-2 py-1 rounded text-xs ${!filter ? 'bg-brand-600 text-white' : 'bg-slate-700 text-slate-300 hover:bg-slate-600'}" data-type="">All</button>
|
|
1551
|
+
${types.map(t => `
|
|
1552
|
+
<button class="quirk-filter px-2 py-1 rounded text-xs ${filter === t ? 'bg-brand-600 text-white' : 'bg-slate-700 text-slate-300 hover:bg-slate-600'}" data-type="${escapeHtml(t)}">${escapeHtml(t)}</button>
|
|
1553
|
+
`).join('')}
|
|
1554
|
+
</div>
|
|
1555
|
+
<button id="quirk-lint-btn" class="px-3 py-1 bg-slate-700 rounded text-sm hover:bg-slate-600">Lint</button>
|
|
1556
|
+
</div>
|
|
1557
|
+
<div id="quirk-lint-area" class="hidden mb-4"></div>
|
|
1558
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
|
1559
|
+
${filtered.map(q => `
|
|
1560
|
+
<div class="bg-slate-900 rounded-lg border border-slate-700 p-3 flex flex-col" data-id="${escapeHtml(q.id)}">
|
|
1561
|
+
<div class="flex items-center gap-2 mb-2">
|
|
1562
|
+
${quirkTypeBadge(q.quirkType)}
|
|
1563
|
+
<span class="text-xs text-slate-500">confidence <span class="${confidenceColor(q.confidence)}">${((q.confidence ?? 1) * 100).toFixed(0)}%</span></span>
|
|
1564
|
+
<span class="text-xs text-slate-600 ml-auto">${formatQuirkDate(q.lastObserved)}</span>
|
|
1565
|
+
</div>
|
|
1566
|
+
<p class="text-sm text-slate-200 whitespace-pre-wrap flex-1">${escapeHtml(q.content)}</p>
|
|
1567
|
+
${q.tags && q.tags.length ? `
|
|
1568
|
+
<div class="flex flex-wrap gap-1 mt-2">
|
|
1569
|
+
${q.tags.map(t => `<span class="inline-block px-1.5 py-0.5 rounded text-xs bg-slate-800 text-slate-400">#${escapeHtml(t)}</span>`).join('')}
|
|
1570
|
+
</div>` : ''}
|
|
1571
|
+
${q.sourceRef ? `<div class="text-xs text-slate-600 mt-2 font-mono">src: ${escapeHtml(q.sourceRef)}</div>` : ''}
|
|
1572
|
+
<div class="flex items-center justify-between mt-2 pt-2 border-t border-slate-800">
|
|
1573
|
+
<span class="text-xs text-slate-600 font-mono">${escapeHtml(q.id)}</span>
|
|
1574
|
+
<button class="quirk-delete text-xs text-slate-600 hover:text-red-400" data-id="${escapeHtml(q.id)}">Delete</button>
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>
|
|
1577
|
+
`).join('')}
|
|
1578
|
+
</div>
|
|
1579
|
+
`;
|
|
1580
|
+
|
|
1581
|
+
$$('.quirk-filter').forEach(btn => {
|
|
1582
|
+
btn.addEventListener('click', () => {
|
|
1583
|
+
state.quirkTypeFilter = btn.dataset.type || null;
|
|
1584
|
+
renderQuirks();
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
$$('.quirk-delete').forEach(btn => {
|
|
1589
|
+
btn.addEventListener('click', async (e) => {
|
|
1590
|
+
e.stopPropagation();
|
|
1591
|
+
const id = btn.dataset.id;
|
|
1592
|
+
if (!confirm('Delete this quirk? This cannot be undone.')) return;
|
|
1593
|
+
btn.textContent = 'Deleting...';
|
|
1594
|
+
try {
|
|
1595
|
+
await API.deleteQuirk(id);
|
|
1596
|
+
renderQuirks();
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
alert('Failed to delete quirk: ' + String(err));
|
|
1599
|
+
renderQuirks();
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
$('#quirk-lint-btn')?.addEventListener('click', async () => {
|
|
1605
|
+
const area = $('#quirk-lint-area');
|
|
1606
|
+
if (!area) return;
|
|
1607
|
+
if (!area.classList.contains('hidden')) {
|
|
1608
|
+
area.classList.add('hidden');
|
|
1609
|
+
area.innerHTML = '';
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
area.classList.remove('hidden');
|
|
1613
|
+
area.innerHTML = '<div class="text-center py-3 text-slate-500">Running lint...</div>';
|
|
1614
|
+
try {
|
|
1615
|
+
const res = await API.quirkLint();
|
|
1616
|
+
const issues = res.issues || [];
|
|
1617
|
+
if (issues.length === 0) {
|
|
1618
|
+
area.innerHTML = `<div class="bg-green-900/20 border border-green-800 rounded-lg p-3 text-sm text-green-300">All quirks look healthy. No issues found.</div>`;
|
|
1619
|
+
} else {
|
|
1620
|
+
area.innerHTML = `
|
|
1621
|
+
<div class="bg-slate-900 rounded-lg border border-amber-800 p-3">
|
|
1622
|
+
<div class="text-sm font-semibold text-amber-300 mb-2">${issues.length} issue(s) found</div>
|
|
1623
|
+
<ul class="space-y-1 text-xs text-slate-300 list-disc list-inside">
|
|
1624
|
+
${issues.map(i => `<li>${escapeHtml(i)}</li>`).join('')}
|
|
1625
|
+
</ul>
|
|
1626
|
+
</div>`;
|
|
1627
|
+
}
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
area.innerHTML = `<div class="bg-red-900/20 border border-red-800 rounded-lg p-3 text-sm text-red-300">Lint failed: ${escapeHtml(String(err))}</div>`;
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1471
1634
|
// Init
|
|
1472
1635
|
renderDashboard();
|
|
1473
1636
|
</script>
|