codetrap 0.1.7 → 0.1.9
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 +132 -98
- package/docs/installation.md +61 -63
- package/package.json +4 -3
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +2 -3
- package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +19 -17
- package/plugins/codetrap-agent/hooks.json +2 -2
- package/{skills → plugins/codetrap-agent/skills}/codetrap-add/SKILL.md +10 -4
- package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +14 -3
- package/plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md +52 -9
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +74 -6
- package/{skills → plugins/codetrap-agent/skills}/codetrap-search/SKILL.md +6 -5
- package/plugins/codetrap-agent/templates/AGENTS.codetrap-maintainer.md +15 -0
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +16 -5
- package/scripts/release-preflight.ts +15 -0
- package/scripts/search-policy-sweep.ts +131 -0
- package/src/commands/workflow.ts +172 -68
- package/src/db/embedding-queries.ts +230 -48
- package/src/db/queries.ts +0 -25
- package/src/db/repository.ts +32 -21
- package/src/db/schema.ts +80 -0
- package/src/index.ts +34 -4
- package/src/lib/codex-setup.ts +247 -0
- package/src/lib/command-requests.ts +112 -1
- package/src/lib/config.ts +57 -7
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +42 -12
- package/src/lib/embedder.ts +118 -3
- package/src/lib/embedding-health.ts +3 -1
- package/src/lib/embedding-job.ts +3 -0
- package/src/lib/embedding-management.ts +65 -0
- package/src/lib/embedding-runtime.ts +177 -0
- package/src/lib/output-json.ts +0 -2
- package/src/lib/scope-context.ts +12 -6
- package/src/lib/scope-migration.ts +2 -1
- package/src/lib/scope.ts +0 -2
- package/src/lib/search-eval.ts +38 -18
- package/src/lib/search-policy-sweep.ts +563 -0
- package/src/lib/search-policy.ts +0 -4
- package/src/lib/search-service.ts +14 -15
- package/src/lib/session-candidate-document.ts +175 -0
- package/src/lib/session-candidate-scope.ts +6 -0
- package/src/lib/session-capture.ts +298 -32
- package/src/lib/session-codec.ts +1 -8
- package/src/lib/session-operations.ts +83 -60
- package/src/lib/session-review.ts +327 -0
- package/src/lib/session-store.ts +87 -73
- package/src/lib/store.ts +74 -10
- package/src/lib/string-list.ts +3 -0
- package/src/lib/text-lines.ts +7 -0
- package/src/lib/trap-search-document.ts +2 -1
- package/src/lib/value-types.ts +3 -0
- package/src/web/client-review.ts +171 -0
- package/src/web/client-script.ts +426 -51
- package/src/web/client-shell.ts +414 -0
- package/src/web/client-text.ts +112 -0
- package/src/web/project-registry.ts +3 -5
- package/src/web/server.ts +117 -103
- package/src/web/static.ts +364 -19
- package/skills/codetrap-capture-external/SKILL.md +0 -62
- package/skills/codetrap-check/SKILL.md +0 -69
- package/src/lib/embedding-index.ts +0 -53
package/src/web/client-script.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { WEB_TEXT_JSON } from "./client-text";
|
|
2
|
+
import { WEB_REVIEW_CLIENT_SCRIPT } from "./client-review";
|
|
3
|
+
import { WEB_SHELL_CLIENT_SCRIPT } from "./client-shell";
|
|
2
4
|
|
|
3
5
|
export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
4
6
|
return ` const qs = new URLSearchParams(location.search);
|
|
@@ -6,6 +8,13 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
6
8
|
if (token) sessionStorage.setItem("codetrap-token", token);
|
|
7
9
|
const savedLocale = localStorage.getItem("codetrap-locale");
|
|
8
10
|
const initialLocale = savedLocale === "zh" ? "zh" : "en";
|
|
11
|
+
const savedSidebarCollapsed = localStorage.getItem("codetrap-sidebar-collapsed") === "true";
|
|
12
|
+
const savedQueueCollapsed = localStorage.getItem("codetrap-queue-collapsed") === "true";
|
|
13
|
+
const EMBEDDING_DEFAULTS = {
|
|
14
|
+
endpoint: "http://127.0.0.1:11434",
|
|
15
|
+
model: "qwen3-embedding:0.6b",
|
|
16
|
+
dimensions: "1024"
|
|
17
|
+
};
|
|
9
18
|
|
|
10
19
|
const TEXT = ${textJson};
|
|
11
20
|
|
|
@@ -14,6 +23,7 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
14
23
|
mainView: "review",
|
|
15
24
|
projects: [],
|
|
16
25
|
sessions: [],
|
|
26
|
+
candidateReview: null,
|
|
17
27
|
candidates: [],
|
|
18
28
|
traps: [],
|
|
19
29
|
trapKey: null,
|
|
@@ -24,16 +34,25 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
24
34
|
trapSort: "updated",
|
|
25
35
|
insightTraps: [],
|
|
26
36
|
insightFilters: { scope: "", status: "all" },
|
|
37
|
+
embeddingStatus: null,
|
|
38
|
+
embeddingSettings: null,
|
|
39
|
+
embeddingProviderDraft: "ollama",
|
|
40
|
+
embeddingOllama: { ...EMBEDDING_DEFAULTS },
|
|
41
|
+
embeddingReindexing: null,
|
|
27
42
|
projectRoot: null,
|
|
28
43
|
sessionId: null,
|
|
29
44
|
candidateId: null,
|
|
30
45
|
candidateView: "inbox",
|
|
46
|
+
candidateDirty: false,
|
|
47
|
+
sidebarCollapsed: savedSidebarCollapsed,
|
|
48
|
+
queueCollapsed: savedQueueCollapsed,
|
|
31
49
|
options: { categories: [], severities: [], scopes: [] },
|
|
32
50
|
conflicts: []
|
|
33
51
|
};
|
|
34
52
|
|
|
35
53
|
const el = (id) => document.getElementById(id);
|
|
36
|
-
|
|
54
|
+
${WEB_SHELL_CLIENT_SCRIPT}
|
|
55
|
+
${WEB_REVIEW_CLIENT_SCRIPT}
|
|
37
56
|
function t(key, params = {}) {
|
|
38
57
|
const text = TEXT[state.locale]?.[key] ?? TEXT.en[key] ?? key;
|
|
39
58
|
return Object.entries(params).reduce((value, [name, replacement]) =>
|
|
@@ -62,9 +81,11 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
62
81
|
document.querySelector("[data-main-view='review']").textContent = t("nav.review");
|
|
63
82
|
document.querySelector("[data-main-view='library']").textContent = t("nav.library");
|
|
64
83
|
document.querySelector("[data-main-view='insights']").textContent = t("nav.insights");
|
|
84
|
+
document.querySelector("[data-main-view='embeddings']").textContent = t("nav.embeddings");
|
|
65
85
|
document.querySelectorAll("[data-locale]").forEach((button) => {
|
|
66
86
|
button.classList.toggle("active", button.dataset.locale === state.locale);
|
|
67
87
|
});
|
|
88
|
+
renderSidebarToggle();
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
function setLocale(locale) {
|
|
@@ -113,23 +134,27 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
113
134
|
async function loadSessions() {
|
|
114
135
|
if (!state.projectRoot) {
|
|
115
136
|
state.sessions = [];
|
|
137
|
+
state.candidateReview = null;
|
|
116
138
|
state.candidates = [];
|
|
117
139
|
state.traps = [];
|
|
118
140
|
state.insightTraps = [];
|
|
141
|
+
state.embeddingStatus = null;
|
|
142
|
+
state.embeddingSettings = null;
|
|
119
143
|
renderSessions();
|
|
120
144
|
renderActiveView();
|
|
121
145
|
return;
|
|
122
146
|
}
|
|
123
147
|
const data = await api("/api/sessions?project=" + encodeURIComponent(state.projectRoot));
|
|
124
148
|
state.sessions = data.sessions;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
149
|
+
state.candidateReview = data.candidate_review || null;
|
|
150
|
+
state.sessionId = selectedReviewSessionId(state.sessions, state.sessionId);
|
|
128
151
|
renderSessions();
|
|
129
152
|
if (state.mainView === "library") {
|
|
130
153
|
await loadTraps();
|
|
131
154
|
} else if (state.mainView === "insights") {
|
|
132
155
|
await loadInsightTraps();
|
|
156
|
+
} else if (state.mainView === "embeddings") {
|
|
157
|
+
await loadEmbeddings();
|
|
133
158
|
} else {
|
|
134
159
|
await loadCandidates();
|
|
135
160
|
}
|
|
@@ -146,7 +171,12 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
146
171
|
}
|
|
147
172
|
const data = await api("/api/candidates?project=" + encodeURIComponent(state.projectRoot) + "&session=" + encodeURIComponent(state.sessionId));
|
|
148
173
|
state.candidates = data.candidates;
|
|
149
|
-
|
|
174
|
+
state.candidateId = reviewQueueModel({
|
|
175
|
+
candidates: state.candidates,
|
|
176
|
+
candidateView: state.candidateView,
|
|
177
|
+
candidateId: state.candidateId,
|
|
178
|
+
candidateReview: state.candidateReview
|
|
179
|
+
}).selectedCandidateId;
|
|
150
180
|
if (state.mainView === "review") {
|
|
151
181
|
renderCandidates();
|
|
152
182
|
renderDetail();
|
|
@@ -198,6 +228,26 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
198
228
|
}
|
|
199
229
|
}
|
|
200
230
|
|
|
231
|
+
async function loadEmbeddings() {
|
|
232
|
+
if (!state.projectRoot) {
|
|
233
|
+
state.embeddingStatus = null;
|
|
234
|
+
state.embeddingSettings = null;
|
|
235
|
+
if (state.mainView === "embeddings") {
|
|
236
|
+
renderEmbeddingsView();
|
|
237
|
+
renderEmbeddingsDetail();
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const data = await api("/api/embeddings?project=" + encodeURIComponent(state.projectRoot));
|
|
242
|
+
state.embeddingStatus = data;
|
|
243
|
+
state.embeddingSettings = data.settings || null;
|
|
244
|
+
syncEmbeddingDraftFromStatus(data);
|
|
245
|
+
if (state.mainView === "embeddings") {
|
|
246
|
+
renderEmbeddingsView();
|
|
247
|
+
renderEmbeddingsDetail();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
201
251
|
function renderMainViewButtons() {
|
|
202
252
|
document.querySelectorAll("[data-main-view]").forEach((button) => {
|
|
203
253
|
button.classList.toggle("active", button.dataset.mainView === state.mainView);
|
|
@@ -210,14 +260,23 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
210
260
|
el("queue-title").textContent = t("title.trapLibrary");
|
|
211
261
|
el("detail-title").textContent = t("title.trapDetail");
|
|
212
262
|
el("candidate-tabs").classList.add("hidden");
|
|
263
|
+
hideReviewSummary();
|
|
213
264
|
renderLibrary();
|
|
214
265
|
renderTrapDetail();
|
|
215
266
|
} else if (state.mainView === "insights") {
|
|
216
267
|
el("queue-title").textContent = t("title.growthInsights");
|
|
217
268
|
el("detail-title").textContent = t("title.insightDetail");
|
|
218
269
|
el("candidate-tabs").classList.add("hidden");
|
|
270
|
+
hideReviewSummary();
|
|
219
271
|
renderInsightsView();
|
|
220
272
|
renderInsightDetail();
|
|
273
|
+
} else if (state.mainView === "embeddings") {
|
|
274
|
+
el("queue-title").textContent = t("title.embeddings");
|
|
275
|
+
el("detail-title").textContent = t("title.embeddingDetail");
|
|
276
|
+
el("candidate-tabs").classList.add("hidden");
|
|
277
|
+
hideReviewSummary();
|
|
278
|
+
renderEmbeddingsView();
|
|
279
|
+
renderEmbeddingsDetail();
|
|
221
280
|
} else {
|
|
222
281
|
el("queue-title").textContent = t("title.candidateInbox");
|
|
223
282
|
el("detail-title").textContent = t("title.candidateDetail");
|
|
@@ -242,6 +301,8 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
242
301
|
state.trapKey = null;
|
|
243
302
|
state.trapDetails = {};
|
|
244
303
|
state.insightTraps = [];
|
|
304
|
+
state.embeddingStatus = null;
|
|
305
|
+
state.embeddingSettings = null;
|
|
245
306
|
renderProjects();
|
|
246
307
|
await loadSessions();
|
|
247
308
|
});
|
|
@@ -255,6 +316,7 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
255
316
|
<span class="row-title">\${escapeHtml(session.goal)}</span>
|
|
256
317
|
<span class="meta">
|
|
257
318
|
<span class="pill">\${escapeHtml(valueLabel(session.status))}</span>
|
|
319
|
+
<span class="pill \${session.pending_count ? "warn" : ""}">\${escapeHtml(t("pill.pending", { count: session.pending_count || 0 }))}</span>
|
|
258
320
|
<span class="pill">\${escapeHtml(t("pill.candidates", { count: session.candidate_count || 0 }))}</span>
|
|
259
321
|
<span class="pill accepted">\${escapeHtml(t("pill.accepted", { count: session.accepted_count || 0 }))}</span>
|
|
260
322
|
</span>
|
|
@@ -315,14 +377,21 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
315
377
|
|
|
316
378
|
function renderCandidates() {
|
|
317
379
|
if (state.mainView !== "review") return;
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
380
|
+
const model = reviewQueueModel({
|
|
381
|
+
candidates: state.candidates,
|
|
382
|
+
candidateView: state.candidateView,
|
|
383
|
+
candidateId: state.candidateId,
|
|
384
|
+
candidateReview: state.candidateReview
|
|
385
|
+
});
|
|
386
|
+
const pendingCount = model.pendingCount;
|
|
387
|
+
const reviewedCount = model.reviewedCount;
|
|
388
|
+
const sorted = model.visibleCandidates;
|
|
389
|
+
state.candidateId = model.selectedCandidateId;
|
|
322
390
|
const session = state.sessions.find((item) => item.id === state.sessionId);
|
|
323
391
|
el("queue-meta").textContent = session
|
|
324
392
|
? t("meta.sessionCounts", { goal: session.goal, pending: pendingCount, reviewed: reviewedCount })
|
|
325
393
|
: t("meta.noSession");
|
|
394
|
+
renderReviewSummary(model.summary);
|
|
326
395
|
renderCandidateViewTabs(pendingCount, reviewedCount);
|
|
327
396
|
el("candidates").innerHTML = sorted.length ? sorted.map((candidate) => \`
|
|
328
397
|
<div class="row \${candidate.id === state.candidateId ? "active" : ""} \${candidate.status} \${reviewCssClass(candidate)}">
|
|
@@ -596,6 +665,267 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
596
665
|
\`).join("") : '<div class="empty">' + escapeHtml(t("empty.noTraps")) + '</div>';
|
|
597
666
|
}
|
|
598
667
|
|
|
668
|
+
function syncEmbeddingDraftFromStatus(status) {
|
|
669
|
+
const settings = status?.settings || null;
|
|
670
|
+
const runtime = status?.runtime || null;
|
|
671
|
+
const provider = settings?.provider || runtime?.provider || "ollama";
|
|
672
|
+
state.embeddingProviderDraft = provider === "jina" ? "jina" : "ollama";
|
|
673
|
+
if (state.embeddingProviderDraft === "ollama") {
|
|
674
|
+
state.embeddingOllama = {
|
|
675
|
+
endpoint: settings?.endpoint || EMBEDDING_DEFAULTS.endpoint,
|
|
676
|
+
model: settings?.model || runtime?.model || EMBEDDING_DEFAULTS.model,
|
|
677
|
+
dimensions: String(settings?.dimensions || runtime?.dimensions || EMBEDDING_DEFAULTS.dimensions)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function renderEmbeddingsView() {
|
|
683
|
+
if (state.mainView !== "embeddings") return;
|
|
684
|
+
const status = state.embeddingStatus;
|
|
685
|
+
const runtime = status?.runtime || null;
|
|
686
|
+
const project = status?.project || null;
|
|
687
|
+
const global = status?.global || null;
|
|
688
|
+
const provider = runtime?.provider || state.embeddingProviderDraft || "";
|
|
689
|
+
const providerLabel = provider ? valueLabel(provider) : t("embedding.notConfigured");
|
|
690
|
+
el("queue-title").textContent = t("title.embeddings");
|
|
691
|
+
el("candidate-tabs").classList.add("hidden");
|
|
692
|
+
el("queue-meta").textContent = state.projectRoot && status
|
|
693
|
+
? t("meta.embeddingCounts", {
|
|
694
|
+
provider: providerLabel,
|
|
695
|
+
projectFresh: project?.fresh ?? 0,
|
|
696
|
+
projectTotal: project?.total ?? 0,
|
|
697
|
+
globalFresh: global?.fresh ?? 0,
|
|
698
|
+
globalTotal: global?.total ?? 0
|
|
699
|
+
})
|
|
700
|
+
: t(state.projectRoot ? "embedding.notConfigured" : "meta.noProject");
|
|
701
|
+
|
|
702
|
+
if (!state.projectRoot) {
|
|
703
|
+
el("candidates").innerHTML = '<div class="empty">' + escapeHtml(t("meta.selectProject")) + '</div>';
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
el("candidates").innerHTML = \`
|
|
708
|
+
<div class="summary-grid">
|
|
709
|
+
\${metric(t("metric.activeProvider"), providerLabel, runtimeStateLabel(runtime))}
|
|
710
|
+
\${metric(t("metric.activeProfile"), shortProfileId(runtime?.profile_id), runtime?.profile_id || t("embedding.noProfile"))}
|
|
711
|
+
\${metric(t("metric.projectFresh"), embeddingFreshValue(project), embeddingNeedsReindex(project))}
|
|
712
|
+
\${metric(t("metric.globalFresh"), embeddingFreshValue(global), embeddingNeedsReindex(global))}
|
|
713
|
+
</div>
|
|
714
|
+
<form class="settings-form" id="embedding-form">
|
|
715
|
+
<div class="status-line">
|
|
716
|
+
<span class="status-dot \${runtime?.available ? "available" : "unavailable"}" aria-hidden="true"></span>
|
|
717
|
+
<span class="pill \${runtime?.available ? "accepted" : "warn"}">\${escapeHtml(runtimeStateLabel(runtime))}</span>
|
|
718
|
+
\${runtime?.profile_id ? '<span class="pill scope">' + escapeHtml(t("embedding.activeProfile")) + '</span>' : ''}
|
|
719
|
+
</div>
|
|
720
|
+
<div class="segmented" id="embedding-provider-tabs" aria-label="\${escapeAttr(t("label.provider"))}">
|
|
721
|
+
<button type="button" data-embedding-provider="ollama" class="\${state.embeddingProviderDraft === "ollama" ? "active" : ""}">\${escapeHtml(valueLabel("ollama"))}</button>
|
|
722
|
+
<button type="button" data-embedding-provider="jina" class="\${state.embeddingProviderDraft === "jina" ? "active" : ""}">\${escapeHtml(valueLabel("jina"))}</button>
|
|
723
|
+
</div>
|
|
724
|
+
<div class="provider-fields \${state.embeddingProviderDraft === "ollama" ? "" : "hidden"}">
|
|
725
|
+
<div class="field"><label for="embedding-endpoint">\${escapeHtml(t("label.endpoint"))}</label><input id="embedding-endpoint" value="\${escapeAttr(state.embeddingOllama.endpoint)}" placeholder="\${escapeAttr(t("placeholder.endpoint"))}"></div>
|
|
726
|
+
<div class="field"><label for="embedding-model">\${escapeHtml(t("label.model"))}</label><input id="embedding-model" value="\${escapeAttr(state.embeddingOllama.model)}" placeholder="\${escapeAttr(t("placeholder.model"))}"></div>
|
|
727
|
+
<div class="field"><label for="embedding-dimensions">\${escapeHtml(t("label.dimensions"))}</label><input id="embedding-dimensions" type="number" min="1" step="1" value="\${escapeAttr(state.embeddingOllama.dimensions)}"></div>
|
|
728
|
+
</div>
|
|
729
|
+
<div class="warning \${state.embeddingProviderDraft === "jina" ? "" : "hidden"}">\${escapeHtml(t("hint.jinaEnv"))}</div>
|
|
730
|
+
<button type="submit" class="primary">\${escapeHtml(t("action.useProvider"))}</button>
|
|
731
|
+
</form>
|
|
732
|
+
<div class="section">
|
|
733
|
+
<div class="title">\${escapeHtml(t("title.reindex"))}</div>
|
|
734
|
+
<div class="subtle">\${escapeHtml(t("hint.reindexAfterSwitch"))}</div>
|
|
735
|
+
<div class="actions" style="padding:0;border-top:0;background:transparent">
|
|
736
|
+
<button type="button" id="embedding-reindex-project" \${state.embeddingReindexing ? "disabled" : ""}>\${escapeHtml(t("action.reindexProject"))}</button>
|
|
737
|
+
<button type="button" id="embedding-reindex-global" \${state.embeddingReindexing ? "disabled" : ""}>\${escapeHtml(t("action.reindexGlobal"))}</button>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
\`;
|
|
741
|
+
bindEmbeddingsControls();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function renderEmbeddingsDetail() {
|
|
745
|
+
if (state.mainView !== "embeddings") return;
|
|
746
|
+
const status = state.embeddingStatus;
|
|
747
|
+
const runtime = status?.runtime || null;
|
|
748
|
+
el("detail-title").textContent = t("title.embeddingDetail");
|
|
749
|
+
el("detail-meta").textContent = state.projectRoot
|
|
750
|
+
? t("meta.embeddingDetail", {
|
|
751
|
+
profile: runtime?.profile_id || t("embedding.noProfile"),
|
|
752
|
+
state: runtimeStateLabel(runtime)
|
|
753
|
+
})
|
|
754
|
+
: t("meta.selectProject");
|
|
755
|
+
|
|
756
|
+
if (!state.projectRoot) {
|
|
757
|
+
el("detail").innerHTML = '<div class="empty">' + escapeHtml(t("meta.selectProject")) + '</div>';
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (!status) {
|
|
761
|
+
el("detail").innerHTML = '<div class="empty">' + escapeHtml(t("empty.noData")) + '</div>';
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
el("detail").innerHTML = \`
|
|
766
|
+
<div class="scroll">
|
|
767
|
+
<div class="section">
|
|
768
|
+
<div class="title">\${escapeHtml(t("title.currentProfile"))}</div>
|
|
769
|
+
<div class="detail-kv">
|
|
770
|
+
\${kv(t("label.provider"), runtime?.provider ? valueLabel(runtime.provider) : t("embedding.notConfigured"))}
|
|
771
|
+
\${kv(t("label.model"), runtime?.model || "-")}
|
|
772
|
+
\${kv(t("label.dimensions"), runtime?.dimensions ?? "-")}
|
|
773
|
+
\${kv(t("label.profileId"), runtime?.profile_id || t("embedding.noProfile"))}
|
|
774
|
+
\${kv(t("label.available"), runtimeStateLabel(runtime))}
|
|
775
|
+
\${kv(t("label.setupAction"), runtime?.setup_action?.command || "-")}
|
|
776
|
+
</div>
|
|
777
|
+
\${runtime?.setup_action ? '<div class="warning">' + escapeHtml(runtime.setup_action.reason) + '</div>' : ''}
|
|
778
|
+
</div>
|
|
779
|
+
\${renderEmbeddingScopeDetail("project", status.project)}
|
|
780
|
+
\${renderEmbeddingScopeDetail("global", status.global)}
|
|
781
|
+
</div>
|
|
782
|
+
\`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function bindEmbeddingsControls() {
|
|
786
|
+
document.querySelectorAll("[data-embedding-provider]").forEach((button) => {
|
|
787
|
+
button.addEventListener("click", () => {
|
|
788
|
+
state.embeddingProviderDraft = button.dataset.embeddingProvider === "jina" ? "jina" : "ollama";
|
|
789
|
+
renderEmbeddingsView();
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
const endpoint = el("embedding-endpoint");
|
|
793
|
+
if (endpoint) endpoint.addEventListener("input", () => state.embeddingOllama.endpoint = endpoint.value);
|
|
794
|
+
const model = el("embedding-model");
|
|
795
|
+
if (model) model.addEventListener("input", () => state.embeddingOllama.model = model.value);
|
|
796
|
+
const dimensions = el("embedding-dimensions");
|
|
797
|
+
if (dimensions) dimensions.addEventListener("input", () => state.embeddingOllama.dimensions = dimensions.value);
|
|
798
|
+
const form = el("embedding-form");
|
|
799
|
+
if (form) {
|
|
800
|
+
form.addEventListener("submit", async (event) => {
|
|
801
|
+
event.preventDefault();
|
|
802
|
+
await useEmbeddingProvider();
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
const project = el("embedding-reindex-project");
|
|
806
|
+
if (project) project.addEventListener("click", () => reindexEmbeddings("project"));
|
|
807
|
+
const global = el("embedding-reindex-global");
|
|
808
|
+
if (global) global.addEventListener("click", () => reindexEmbeddings("global"));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function useEmbeddingProvider() {
|
|
812
|
+
if (!state.projectRoot) return;
|
|
813
|
+
const body = {
|
|
814
|
+
projectRoot: state.projectRoot,
|
|
815
|
+
provider: state.embeddingProviderDraft
|
|
816
|
+
};
|
|
817
|
+
if (state.embeddingProviderDraft === "ollama") {
|
|
818
|
+
const dimensions = Number.parseInt(state.embeddingOllama.dimensions, 10);
|
|
819
|
+
if (!Number.isInteger(dimensions) || dimensions <= 0) {
|
|
820
|
+
showStatus(t("status.invalidDimensions"), true);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
body.endpoint = state.embeddingOllama.endpoint || EMBEDDING_DEFAULTS.endpoint;
|
|
824
|
+
body.model = state.embeddingOllama.model || EMBEDDING_DEFAULTS.model;
|
|
825
|
+
body.dimensions = dimensions;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
const data = await api("/api/embeddings/use", {
|
|
829
|
+
method: "POST",
|
|
830
|
+
body: JSON.stringify(body)
|
|
831
|
+
});
|
|
832
|
+
state.embeddingSettings = data.settings || data.embeddings || null;
|
|
833
|
+
state.embeddingStatus = {
|
|
834
|
+
project_root: data.project_root,
|
|
835
|
+
settings: state.embeddingSettings,
|
|
836
|
+
...data.status
|
|
837
|
+
};
|
|
838
|
+
syncEmbeddingDraftFromStatus(state.embeddingStatus);
|
|
839
|
+
renderEmbeddingsView();
|
|
840
|
+
renderEmbeddingsDetail();
|
|
841
|
+
showStatus(t("status.embeddingProviderSaved"));
|
|
842
|
+
} catch (error) {
|
|
843
|
+
showStatus(error.message, true);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function reindexEmbeddings(scope) {
|
|
848
|
+
if (!state.projectRoot || state.embeddingReindexing) return;
|
|
849
|
+
state.embeddingReindexing = scope;
|
|
850
|
+
renderEmbeddingsView();
|
|
851
|
+
try {
|
|
852
|
+
const data = await api("/api/embeddings/reindex", {
|
|
853
|
+
method: "POST",
|
|
854
|
+
body: JSON.stringify({ projectRoot: state.projectRoot, scope })
|
|
855
|
+
});
|
|
856
|
+
state.embeddingStatus = {
|
|
857
|
+
project_root: data.project_root,
|
|
858
|
+
settings: state.embeddingSettings,
|
|
859
|
+
...data.status
|
|
860
|
+
};
|
|
861
|
+
renderEmbeddingsView();
|
|
862
|
+
renderEmbeddingsDetail();
|
|
863
|
+
showStatus(t("status.embeddingsReindexed", {
|
|
864
|
+
generated: data.result?.generated ?? 0,
|
|
865
|
+
skipped: data.result?.skipped ?? 0
|
|
866
|
+
}));
|
|
867
|
+
} catch (error) {
|
|
868
|
+
showStatus(error.message, true);
|
|
869
|
+
} finally {
|
|
870
|
+
state.embeddingReindexing = null;
|
|
871
|
+
renderEmbeddingsView();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function renderEmbeddingScopeDetail(scope, status) {
|
|
876
|
+
const title = scope === "project" ? t("title.projectEmbeddings") : t("title.globalEmbeddings");
|
|
877
|
+
if (!status) {
|
|
878
|
+
return \`<div class="section"><div class="title">\${escapeHtml(title)}</div><div class="empty">\${escapeHtml(t("empty.noData"))}</div></div>\`;
|
|
879
|
+
}
|
|
880
|
+
return \`<div class="section">
|
|
881
|
+
<div class="title">\${escapeHtml(title)}</div>
|
|
882
|
+
<div class="detail-kv">
|
|
883
|
+
\${kv(t("label.total"), status.total)}
|
|
884
|
+
\${kv(t("label.fresh"), status.fresh)}
|
|
885
|
+
\${kv(t("label.stale"), status.stale)}
|
|
886
|
+
\${kv(t("label.missing"), status.missing)}
|
|
887
|
+
</div>
|
|
888
|
+
<div class="title">\${escapeHtml(t("title.storedProfiles"))}</div>
|
|
889
|
+
<div class="profile-list">\${renderEmbeddingProfiles(status.profiles || [])}</div>
|
|
890
|
+
</div>\`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function renderEmbeddingProfiles(profiles) {
|
|
894
|
+
return profiles.length ? profiles.map((profile) => \`
|
|
895
|
+
<div class="profile-row">
|
|
896
|
+
<div class="row-title">\${escapeHtml(profile.id)}</div>
|
|
897
|
+
<div class="meta">
|
|
898
|
+
<span class="pill">\${escapeHtml(valueLabel(profile.provider))}</span>
|
|
899
|
+
<span class="pill">\${escapeHtml(profile.model)}</span>
|
|
900
|
+
<span class="pill">\${escapeHtml(String(profile.dimensions))}d</span>
|
|
901
|
+
<span class="pill">\${escapeHtml(t("label.count"))}: \${escapeHtml(profile.embedding_count)}</span>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="subtle">\${escapeHtml(profile.updated_at || "-")}</div>
|
|
904
|
+
</div>
|
|
905
|
+
\`).join("") : '<div class="empty">' + escapeHtml(t("empty.noProfiles")) + '</div>';
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function runtimeStateLabel(runtime) {
|
|
909
|
+
if (!runtime?.provider) return t("embedding.notConfigured");
|
|
910
|
+
return runtime.available ? t("embedding.available") : t("embedding.unavailable");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function embeddingFreshValue(status) {
|
|
914
|
+
if (!status) return "-";
|
|
915
|
+
return String(status.fresh || 0) + "/" + String(status.total || 0);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function embeddingNeedsReindex(status) {
|
|
919
|
+
if (!status || status.total === 0) return t("embedding.noProfile");
|
|
920
|
+
return status.fresh === status.total ? t("embedding.activeProfile") : t("embedding.profileNeedsReindex");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function shortProfileId(profileId) {
|
|
924
|
+
if (!profileId) return "-";
|
|
925
|
+
const parts = profileId.split(":");
|
|
926
|
+
return parts.length >= 4 ? parts[0] + " / " + parts[1] : profileId;
|
|
927
|
+
}
|
|
928
|
+
|
|
599
929
|
function metric(label, value, detail) {
|
|
600
930
|
return \`<div class="metric"><div class="metric-label">\${escapeHtml(label)}</div><div class="metric-value">\${escapeHtml(value)}</div><div class="subtle">\${escapeHtml(detail)}</div></div>\`;
|
|
601
931
|
}
|
|
@@ -735,20 +1065,29 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
735
1065
|
});
|
|
736
1066
|
}
|
|
737
1067
|
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1068
|
+
function renderReviewSummary(summary = visibleReviewSummary(state.candidateReview)) {
|
|
1069
|
+
const target = el("review-summary");
|
|
1070
|
+
if (!target) return;
|
|
1071
|
+
if (!summary) {
|
|
1072
|
+
target.classList.add("hidden");
|
|
1073
|
+
target.innerHTML = "";
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
target.classList.remove("hidden");
|
|
1077
|
+
target.innerHTML = \`
|
|
1078
|
+
<div class="review-banner">
|
|
1079
|
+
<strong>\${escapeHtml(t("reviewSummary.pending", { count: summary.pending_count }))}</strong>
|
|
1080
|
+
<span>\${escapeHtml(t("reviewSummary.sessions", { count: summary.pending_session_count }))}</span>
|
|
1081
|
+
<span>\${escapeHtml(t("reviewSummary.quality", { high: summary.high_quality_pending_count, edit: summary.needs_edit_count }))}</span>
|
|
1082
|
+
</div>
|
|
1083
|
+
\`;
|
|
746
1084
|
}
|
|
747
1085
|
|
|
748
|
-
function
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1086
|
+
function hideReviewSummary() {
|
|
1087
|
+
const target = el("review-summary");
|
|
1088
|
+
if (!target) return;
|
|
1089
|
+
target.classList.add("hidden");
|
|
1090
|
+
target.innerHTML = "";
|
|
752
1091
|
}
|
|
753
1092
|
|
|
754
1093
|
function renderTrapDetail() {
|
|
@@ -848,9 +1187,11 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
848
1187
|
const candidate = state.candidates.find((item) => item.id === state.candidateId);
|
|
849
1188
|
el("detail-meta").textContent = candidate ? candidate.id + " / " + valueLabel(candidate.status) : t("meta.selectCandidate");
|
|
850
1189
|
if (!candidate) {
|
|
1190
|
+
state.candidateDirty = false;
|
|
851
1191
|
el("detail").innerHTML = '<div class="empty">' + escapeHtml(t("empty.noCandidateSelected")) + '</div>';
|
|
852
1192
|
return;
|
|
853
1193
|
}
|
|
1194
|
+
state.candidateDirty = false;
|
|
854
1195
|
const disabled = candidate.status !== "proposed" ? "disabled" : "";
|
|
855
1196
|
el("detail").innerHTML = \`
|
|
856
1197
|
<div class="scroll">
|
|
@@ -887,6 +1228,7 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
887
1228
|
\${renderDetailActions(candidate, disabled)}
|
|
888
1229
|
\`;
|
|
889
1230
|
bindDetailActions(candidate);
|
|
1231
|
+
bindCandidateFormDirty(candidate);
|
|
890
1232
|
bindTrapJumpButtons();
|
|
891
1233
|
}
|
|
892
1234
|
|
|
@@ -938,9 +1280,29 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
938
1280
|
<button id="accept-anyway" \${disabled}>\${escapeHtml(t("action.acceptAnyway"))}</button>
|
|
939
1281
|
<input id="supersedes" placeholder="\${escapeAttr(t("placeholder.supersedesId"))}" style="width:150px" \${disabled}>
|
|
940
1282
|
<button id="supersede" \${disabled}>\${escapeHtml(t("action.supersede"))}</button>
|
|
1283
|
+
<span id="candidate-draft-state" class="action-hint">\${escapeHtml(t("hint.acceptUsesCurrentDraft"))}</span>
|
|
941
1284
|
</div>\`;
|
|
942
1285
|
}
|
|
943
1286
|
|
|
1287
|
+
function bindCandidateFormDirty(candidate) {
|
|
1288
|
+
const form = el("candidate-form");
|
|
1289
|
+
if (!form || candidate.status !== "proposed") return;
|
|
1290
|
+
const markDirty = () => {
|
|
1291
|
+
state.candidateDirty = true;
|
|
1292
|
+
renderCandidateDraftState();
|
|
1293
|
+
};
|
|
1294
|
+
form.addEventListener("input", markDirty);
|
|
1295
|
+
form.addEventListener("change", markDirty);
|
|
1296
|
+
renderCandidateDraftState();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function renderCandidateDraftState() {
|
|
1300
|
+
const draft = el("candidate-draft-state");
|
|
1301
|
+
if (!draft) return;
|
|
1302
|
+
draft.textContent = state.candidateDirty ? t("hint.unsavedDraftAccepted") : t("hint.acceptUsesCurrentDraft");
|
|
1303
|
+
draft.classList.toggle("dirty", state.candidateDirty);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
944
1306
|
function bindDetailActions(candidate) {
|
|
945
1307
|
document.querySelectorAll("[data-clean-deleted-candidates]").forEach((button) => {
|
|
946
1308
|
button.addEventListener("click", cleanupDeletedCandidates);
|
|
@@ -953,6 +1315,7 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
953
1315
|
method: "POST",
|
|
954
1316
|
body: JSON.stringify(candidatePayload(candidate.id))
|
|
955
1317
|
});
|
|
1318
|
+
state.candidateDirty = false;
|
|
956
1319
|
await syncAfterMutation(data.candidate.id);
|
|
957
1320
|
showStatus(t("status.candidateSaved"));
|
|
958
1321
|
} catch (error) {
|
|
@@ -983,12 +1346,21 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
983
1346
|
|
|
984
1347
|
async function acceptCandidate(extra) {
|
|
985
1348
|
try {
|
|
1349
|
+
const payload = el("candidate-form")
|
|
1350
|
+
? candidatePayload(state.candidateId, extra)
|
|
1351
|
+
: {
|
|
1352
|
+
projectRoot: state.projectRoot,
|
|
1353
|
+
sessionId: state.sessionId,
|
|
1354
|
+
candidateId: state.candidateId,
|
|
1355
|
+
...extra
|
|
1356
|
+
};
|
|
986
1357
|
const data = await api("/api/candidate/accept", {
|
|
987
1358
|
method: "POST",
|
|
988
|
-
body: JSON.stringify(
|
|
1359
|
+
body: JSON.stringify(payload)
|
|
989
1360
|
});
|
|
990
1361
|
await syncAfterMutation(data.candidate.id);
|
|
991
1362
|
state.conflicts = [];
|
|
1363
|
+
state.candidateDirty = false;
|
|
992
1364
|
showStatus(t("status.candidateAccepted"));
|
|
993
1365
|
} catch (error) {
|
|
994
1366
|
if (error.payload?.possible_conflicts) {
|
|
@@ -1003,25 +1375,30 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
1003
1375
|
}
|
|
1004
1376
|
}
|
|
1005
1377
|
|
|
1006
|
-
function candidatePayload(candidateId) {
|
|
1007
|
-
|
|
1008
|
-
return {
|
|
1378
|
+
function candidatePayload(candidateId, extra = {}) {
|
|
1379
|
+
return reviewCandidateMutationPayload({
|
|
1009
1380
|
projectRoot: state.projectRoot,
|
|
1010
1381
|
sessionId: state.sessionId,
|
|
1011
1382
|
candidateId,
|
|
1012
|
-
trap:
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1383
|
+
trap: reviewCandidateTrapDraft(candidateFormFields()),
|
|
1384
|
+
extra
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function candidateFormFields() {
|
|
1389
|
+
const form = new FormData(el("candidate-form"));
|
|
1390
|
+
return {
|
|
1391
|
+
title: form.get("title"),
|
|
1392
|
+
category: form.get("category"),
|
|
1393
|
+
scope: form.get("scope"),
|
|
1394
|
+
severity: form.get("severity"),
|
|
1395
|
+
tags: form.get("tags"),
|
|
1396
|
+
path_globs: form.get("path_globs"),
|
|
1397
|
+
module: form.get("module"),
|
|
1398
|
+
owner: form.get("owner"),
|
|
1399
|
+
context: form.get("context"),
|
|
1400
|
+
mistake: form.get("mistake"),
|
|
1401
|
+
fix: form.get("fix")
|
|
1025
1402
|
};
|
|
1026
1403
|
}
|
|
1027
1404
|
|
|
@@ -1046,6 +1423,12 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
1046
1423
|
}
|
|
1047
1424
|
|
|
1048
1425
|
el("refresh").addEventListener("click", refreshAll);
|
|
1426
|
+
el("sidebar-toggle").addEventListener("click", () => {
|
|
1427
|
+
setSidebarCollapsed(!state.sidebarCollapsed);
|
|
1428
|
+
});
|
|
1429
|
+
el("queue-toggle").addEventListener("click", () => {
|
|
1430
|
+
setQueueCollapsed(!state.queueCollapsed);
|
|
1431
|
+
});
|
|
1049
1432
|
document.querySelectorAll("[data-locale]").forEach((button) => {
|
|
1050
1433
|
button.addEventListener("click", () => setLocale(button.dataset.locale));
|
|
1051
1434
|
});
|
|
@@ -1059,6 +1442,8 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
1059
1442
|
await loadTraps();
|
|
1060
1443
|
} else if (state.mainView === "insights") {
|
|
1061
1444
|
await loadInsightTraps();
|
|
1445
|
+
} else if (state.mainView === "embeddings") {
|
|
1446
|
+
await loadEmbeddings();
|
|
1062
1447
|
} else {
|
|
1063
1448
|
await loadCandidates();
|
|
1064
1449
|
}
|
|
@@ -1086,6 +1471,8 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
1086
1471
|
state.trapKey = null;
|
|
1087
1472
|
state.trapDetails = {};
|
|
1088
1473
|
state.insightTraps = [];
|
|
1474
|
+
state.embeddingStatus = null;
|
|
1475
|
+
state.embeddingSettings = null;
|
|
1089
1476
|
el("project-path").value = "";
|
|
1090
1477
|
renderProjects();
|
|
1091
1478
|
await loadSessions();
|
|
@@ -1128,10 +1515,6 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
1128
1515
|
</div>\`).join("")}</div>\`;
|
|
1129
1516
|
}
|
|
1130
1517
|
|
|
1131
|
-
function statusRank(status) {
|
|
1132
|
-
return status === "proposed" ? 0 : status === "accepted" ? 1 : 2;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
1518
|
function reviewLabel(candidate) {
|
|
1136
1519
|
const review = candidate.review;
|
|
1137
1520
|
if (!review || review.status === "pending") return t("review.pending");
|
|
@@ -1147,15 +1530,6 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
1147
1530
|
return String(candidate.review?.status || candidate.status).replace(/_/g, "-");
|
|
1148
1531
|
}
|
|
1149
1532
|
|
|
1150
|
-
function splitList(value) {
|
|
1151
|
-
return String(value || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
function blankToNull(value) {
|
|
1155
|
-
const text = String(value || "").trim();
|
|
1156
|
-
return text ? text : null;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
1533
|
function escapeHtml(value) {
|
|
1160
1534
|
return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
|
1161
1535
|
}
|
|
@@ -1164,5 +1538,6 @@ export function webClientScript(textJson = WEB_TEXT_JSON): string {
|
|
|
1164
1538
|
return escapeHtml(value);
|
|
1165
1539
|
}
|
|
1166
1540
|
|
|
1541
|
+
initShellResizers();
|
|
1167
1542
|
refreshAll();`;
|
|
1168
1543
|
}
|