codetrap 0.1.6 → 0.1.8
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 +159 -51
- package/docs/installation.md +113 -29
- package/package.json +4 -3
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +1 -2
- 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.md +31 -5
- package/scripts/search-policy-sweep.ts +131 -0
- package/src/commands/workflow.ts +186 -68
- package/src/db/connection.ts +6 -6
- 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 +32 -7
- package/src/lib/command-requests.ts +134 -1
- package/src/lib/config.ts +57 -7
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +96 -6
- package/src/lib/embed-output.ts +26 -0
- 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 +17 -11
- package/src/lib/scope-migration.ts +2 -1
- package/src/lib/scope.ts +4 -6
- package/src/lib/search-eval.ts +136 -23
- 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 +111 -51
- package/src/lib/session-review.ts +327 -0
- package/src/lib/session-store.ts +177 -55
- package/src/lib/store.ts +79 -11
- 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 +1543 -0
- package/src/web/client-shell.ts +414 -0
- package/src/web/client-text.ts +447 -0
- package/src/web/project-registry.ts +3 -5
- package/src/web/server.ts +184 -111
- package/src/web/static.ts +581 -484
- 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/static.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { webClientScript } from "./client-script";
|
|
2
|
+
|
|
1
3
|
export const WEB_INDEX_HTML = `<!doctype html>
|
|
2
4
|
<html lang="en">
|
|
3
5
|
<head>
|
|
@@ -7,23 +9,26 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
7
9
|
<style>
|
|
8
10
|
:root {
|
|
9
11
|
color-scheme: light;
|
|
10
|
-
--bg: #
|
|
11
|
-
--panel: #
|
|
12
|
-
--panel-2: #
|
|
12
|
+
--bg: #f3f6f2;
|
|
13
|
+
--panel: #f8faf7;
|
|
14
|
+
--panel-2: #fcfdfb;
|
|
13
15
|
--surface: #ffffff;
|
|
14
|
-
--surface-hover: #
|
|
15
|
-
--line: #
|
|
16
|
-
--line-soft: #
|
|
17
|
-
--text: #
|
|
18
|
-
--muted: #
|
|
19
|
-
--faint: #
|
|
16
|
+
--surface-hover: #edf3ef;
|
|
17
|
+
--line: #d6dfd9;
|
|
18
|
+
--line-soft: #e5ebe6;
|
|
19
|
+
--text: #20231f;
|
|
20
|
+
--muted: #657069;
|
|
21
|
+
--faint: #8b968e;
|
|
20
22
|
--accent: #0f766e;
|
|
21
23
|
--accent-soft: #d9f1eb;
|
|
22
24
|
--accent-strong: #064e46;
|
|
25
|
+
--ink: #1f2937;
|
|
26
|
+
--violet: #4f46e5;
|
|
27
|
+
--violet-soft: #e6e8ff;
|
|
23
28
|
--danger: #b42318;
|
|
24
29
|
--warn: #9a6700;
|
|
25
30
|
--ok: #18794e;
|
|
26
|
-
--shadow: rgba(
|
|
31
|
+
--shadow: rgba(28, 39, 32, 0.08);
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
* { box-sizing: border-box; }
|
|
@@ -31,8 +36,8 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
31
36
|
body {
|
|
32
37
|
margin: 0;
|
|
33
38
|
background:
|
|
34
|
-
|
|
35
|
-
linear-gradient(180deg, #
|
|
39
|
+
linear-gradient(120deg, rgba(15, 118, 110, 0.08), transparent 34%),
|
|
40
|
+
linear-gradient(180deg, #fbfcf8 0%, var(--bg) 48%, #eef3ef 100%);
|
|
36
41
|
color: var(--text);
|
|
37
42
|
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
|
|
38
43
|
letter-spacing: 0;
|
|
@@ -56,7 +61,7 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
button:hover { background: var(--surface-hover); border-color: #c9c1b4; }
|
|
59
|
-
button.primary { background:
|
|
64
|
+
button.primary { background: var(--ink); color: #fffdf8; border-color: var(--ink); }
|
|
60
65
|
button.danger { border-color: color-mix(in srgb, var(--danger), var(--line) 35%); color: var(--danger); }
|
|
61
66
|
button.ghost { background: transparent; }
|
|
62
67
|
button:disabled { color: var(--faint); border-color: var(--line); cursor: not-allowed; opacity: 0.62; }
|
|
@@ -112,9 +117,10 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
112
117
|
.shell {
|
|
113
118
|
height: 100%;
|
|
114
119
|
display: grid;
|
|
115
|
-
grid-template-columns: minmax(250px, 0.82fr) minmax(
|
|
120
|
+
grid-template-columns: minmax(250px, 0.82fr) 8px minmax(460px, 1.48fr) 8px minmax(320px, 1fr);
|
|
116
121
|
gap: 0;
|
|
117
122
|
overflow: hidden;
|
|
123
|
+
position: relative;
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
.rail, .queue, .detail {
|
|
@@ -124,9 +130,136 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
124
130
|
display: flex;
|
|
125
131
|
flex-direction: column;
|
|
126
132
|
backdrop-filter: blur(12px);
|
|
133
|
+
transition: box-shadow 140ms ease, transform 140ms ease, opacity 140ms ease;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.detail { background: var(--panel-2); }
|
|
137
|
+
.queue { border-right: 0; }
|
|
138
|
+
|
|
139
|
+
.shell.rail-collapsed .rail,
|
|
140
|
+
.shell.rail-collapsed [data-splitter="left"] {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.shell.queue-collapsed .queue,
|
|
145
|
+
.shell.queue-collapsed [data-splitter="right"] {
|
|
146
|
+
display: none;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.shell.rail-collapsed {
|
|
150
|
+
grid-template-columns: minmax(460px, 1.48fr) 8px minmax(320px, 1fr);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.shell.queue-collapsed {
|
|
154
|
+
grid-template-columns: minmax(250px, 0.82fr) 8px minmax(460px, 1fr);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.shell.rail-collapsed.queue-collapsed {
|
|
158
|
+
grid-template-columns: minmax(460px, 1fr);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.shell.rail-collapsed.rail-peeking .rail,
|
|
162
|
+
.shell.queue-collapsed.queue-peeking .queue {
|
|
163
|
+
display: flex;
|
|
164
|
+
position: absolute;
|
|
165
|
+
top: 0;
|
|
166
|
+
bottom: 0;
|
|
167
|
+
z-index: 11;
|
|
168
|
+
border: 1px solid var(--line-soft);
|
|
169
|
+
background: color-mix(in srgb, var(--panel), transparent 4%);
|
|
170
|
+
box-shadow: 0 18px 54px rgba(31, 43, 36, 0.18);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.shell.rail-collapsed.rail-peeking .rail {
|
|
174
|
+
left: 0;
|
|
175
|
+
width: min(330px, calc(100% - 72px));
|
|
176
|
+
animation: rail-peek-in 140ms ease-out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.shell.queue-collapsed.queue-peeking .queue {
|
|
180
|
+
right: 0;
|
|
181
|
+
width: min(390px, calc(100% - 72px));
|
|
182
|
+
animation: queue-peek-in 140ms ease-out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@keyframes rail-peek-in {
|
|
186
|
+
from { opacity: 0.72; transform: translateX(-18px); }
|
|
187
|
+
to { opacity: 1; transform: translateX(0); }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@keyframes queue-peek-in {
|
|
191
|
+
from { opacity: 0.72; transform: translateX(18px); }
|
|
192
|
+
to { opacity: 1; transform: translateX(0); }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.edge-reveal {
|
|
196
|
+
position: absolute;
|
|
197
|
+
top: 0;
|
|
198
|
+
bottom: 0;
|
|
199
|
+
z-index: 9;
|
|
200
|
+
width: 18px;
|
|
201
|
+
display: none;
|
|
202
|
+
pointer-events: none;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.edge-reveal-left { left: 0; }
|
|
206
|
+
.edge-reveal-right { right: 0; }
|
|
207
|
+
|
|
208
|
+
.shell.rail-collapsed .edge-reveal-left,
|
|
209
|
+
.shell.queue-collapsed .edge-reveal-right {
|
|
210
|
+
display: block;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.edge-reveal::after {
|
|
214
|
+
content: "";
|
|
215
|
+
position: absolute;
|
|
216
|
+
top: 14px;
|
|
217
|
+
bottom: 14px;
|
|
218
|
+
width: 2px;
|
|
219
|
+
border-radius: 999px;
|
|
220
|
+
background: color-mix(in srgb, var(--accent), transparent 45%);
|
|
221
|
+
opacity: 0;
|
|
222
|
+
transition: opacity 120ms ease;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.edge-reveal-left::after { left: 3px; }
|
|
226
|
+
.edge-reveal-right::after { right: 3px; }
|
|
227
|
+
.shell.rail-peeking .edge-reveal-left::after,
|
|
228
|
+
.shell.queue-peeking .edge-reveal-right::after,
|
|
229
|
+
.shell.rail-collapse-target [data-splitter="left"]::before,
|
|
230
|
+
.shell.queue-collapse-target [data-splitter="right"]::before {
|
|
231
|
+
opacity: 1;
|
|
232
|
+
background: color-mix(in srgb, var(--accent), transparent 70%);
|
|
233
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent), transparent 40%);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.splitter {
|
|
237
|
+
min-height: 0;
|
|
238
|
+
position: relative;
|
|
239
|
+
background:
|
|
240
|
+
linear-gradient(90deg, transparent 0, transparent 3px, var(--line-soft) 3px, var(--line-soft) 4px, transparent 4px);
|
|
241
|
+
cursor: col-resize;
|
|
242
|
+
touch-action: none;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.splitter::before {
|
|
246
|
+
content: "";
|
|
247
|
+
position: absolute;
|
|
248
|
+
inset: 0 2px;
|
|
249
|
+
border-radius: 999px;
|
|
250
|
+
background: transparent;
|
|
251
|
+
transition: background 120ms ease, box-shadow 120ms ease;
|
|
127
252
|
}
|
|
128
253
|
|
|
129
|
-
.
|
|
254
|
+
.splitter:hover::before,
|
|
255
|
+
.splitter:focus-visible::before,
|
|
256
|
+
.splitter.dragging::before {
|
|
257
|
+
background: color-mix(in srgb, var(--accent), transparent 82%);
|
|
258
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent), transparent 54%);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.splitter:focus-visible { outline: none; }
|
|
262
|
+
body.resizing-panes { cursor: col-resize; user-select: none; }
|
|
130
263
|
|
|
131
264
|
.bar {
|
|
132
265
|
min-height: 56px;
|
|
@@ -138,6 +271,123 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
138
271
|
gap: 10px;
|
|
139
272
|
}
|
|
140
273
|
|
|
274
|
+
.bar-title-group {
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
gap: 10px;
|
|
278
|
+
min-width: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.icon-button {
|
|
282
|
+
width: 34px;
|
|
283
|
+
min-width: 34px;
|
|
284
|
+
height: 34px;
|
|
285
|
+
min-height: 34px;
|
|
286
|
+
display: inline-grid;
|
|
287
|
+
place-items: center;
|
|
288
|
+
padding: 0;
|
|
289
|
+
border-radius: 8px;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.shell-toggle {
|
|
294
|
+
position: absolute;
|
|
295
|
+
top: 12px;
|
|
296
|
+
z-index: 12;
|
|
297
|
+
width: 36px;
|
|
298
|
+
min-width: 36px;
|
|
299
|
+
height: 36px;
|
|
300
|
+
min-height: 36px;
|
|
301
|
+
border-radius: 10px;
|
|
302
|
+
background: rgba(255, 255, 255, 0.82);
|
|
303
|
+
border-color: color-mix(in srgb, var(--line), var(--accent) 16%);
|
|
304
|
+
box-shadow: 0 1px 2px var(--shadow), 0 10px 28px rgba(31, 43, 36, 0.08);
|
|
305
|
+
backdrop-filter: blur(14px);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.shell-toggle-left { left: 12px; }
|
|
309
|
+
.shell-toggle-right { right: 12px; }
|
|
310
|
+
|
|
311
|
+
.shell-toggle:hover,
|
|
312
|
+
.shell-toggle.active {
|
|
313
|
+
background: #fffdf8;
|
|
314
|
+
border-color: color-mix(in srgb, var(--accent), var(--line) 34%);
|
|
315
|
+
box-shadow: 0 1px 2px var(--shadow), 0 12px 30px rgba(15, 118, 110, 0.12);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.rail > .bar {
|
|
319
|
+
align-items: flex-start;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
padding-left: 58px;
|
|
322
|
+
}
|
|
323
|
+
.queue > .bar { padding-right: 58px; }
|
|
324
|
+
.shell.rail-collapsed .detail > .bar { padding-left: 58px; }
|
|
325
|
+
.shell.queue-collapsed .detail > .bar { padding-right: 58px; }
|
|
326
|
+
|
|
327
|
+
.icon-button:hover,
|
|
328
|
+
.icon-button.active {
|
|
329
|
+
color: var(--text);
|
|
330
|
+
border-color: color-mix(in srgb, var(--accent), var(--line) 45%);
|
|
331
|
+
background: #fffdf8;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.sidebar-toggle-icon {
|
|
335
|
+
width: 18px;
|
|
336
|
+
height: 16px;
|
|
337
|
+
position: relative;
|
|
338
|
+
border: 2px solid currentColor;
|
|
339
|
+
border-radius: 5px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.sidebar-toggle-icon::before {
|
|
343
|
+
content: "";
|
|
344
|
+
position: absolute;
|
|
345
|
+
top: 0;
|
|
346
|
+
bottom: 0;
|
|
347
|
+
left: 6px;
|
|
348
|
+
width: 2px;
|
|
349
|
+
background: currentColor;
|
|
350
|
+
opacity: 0.72;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.sidebar-toggle.active .sidebar-toggle-icon::before {
|
|
354
|
+
opacity: 0.18;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.queue-toggle .sidebar-toggle-icon::before {
|
|
358
|
+
left: auto;
|
|
359
|
+
right: 6px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.queue-actions {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
justify-content: flex-end;
|
|
366
|
+
gap: 8px;
|
|
367
|
+
flex-wrap: wrap;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.rail-actions {
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
justify-content: flex-end;
|
|
374
|
+
gap: 8px;
|
|
375
|
+
flex-wrap: wrap;
|
|
376
|
+
flex: 1;
|
|
377
|
+
min-width: 0;
|
|
378
|
+
width: 100%;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.main-nav {
|
|
382
|
+
display: grid;
|
|
383
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
384
|
+
width: min(100%, 240px);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.main-nav button {
|
|
388
|
+
width: 100%;
|
|
389
|
+
}
|
|
390
|
+
|
|
141
391
|
.title {
|
|
142
392
|
font-weight: 650;
|
|
143
393
|
text-transform: none;
|
|
@@ -175,6 +425,26 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
175
425
|
.row.accepted { border-color: color-mix(in srgb, var(--ok), var(--line) 55%); }
|
|
176
426
|
.row.accepted-missing { border-color: color-mix(in srgb, var(--warn), var(--line) 40%); }
|
|
177
427
|
.row.rejected { border-color: color-mix(in srgb, var(--danger), var(--line) 55%); opacity: 0.72; }
|
|
428
|
+
.row-main {
|
|
429
|
+
width: 100%;
|
|
430
|
+
min-height: 0;
|
|
431
|
+
padding: 0;
|
|
432
|
+
border: 0;
|
|
433
|
+
border-radius: 0;
|
|
434
|
+
background: transparent;
|
|
435
|
+
box-shadow: none;
|
|
436
|
+
text-align: left;
|
|
437
|
+
display: grid;
|
|
438
|
+
gap: 5px;
|
|
439
|
+
color: inherit;
|
|
440
|
+
}
|
|
441
|
+
.row-main:hover { background: transparent; border-color: transparent; }
|
|
442
|
+
.row-action {
|
|
443
|
+
justify-self: start;
|
|
444
|
+
min-height: 28px;
|
|
445
|
+
font-size: 12px;
|
|
446
|
+
box-shadow: none;
|
|
447
|
+
}
|
|
178
448
|
.row-title { overflow-wrap: anywhere; }
|
|
179
449
|
.meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
180
450
|
|
|
@@ -195,6 +465,9 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
195
465
|
.pill.accepted-missing { color: var(--warn); border-color: color-mix(in srgb, var(--warn), var(--line) 55%); }
|
|
196
466
|
.pill.rejected { color: var(--danger); border-color: color-mix(in srgb, var(--danger), var(--line) 55%); }
|
|
197
467
|
.pill.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn), var(--line) 55%); }
|
|
468
|
+
.pill.scope { color: var(--violet); background: var(--violet-soft); border-color: color-mix(in srgb, var(--violet), var(--line) 55%); }
|
|
469
|
+
.pill.critical { color: var(--danger); border-color: color-mix(in srgb, var(--danger), var(--line) 42%); }
|
|
470
|
+
.pill.error { color: var(--warn); border-color: color-mix(in srgb, var(--warn), var(--line) 42%); }
|
|
198
471
|
|
|
199
472
|
.detail-body {
|
|
200
473
|
display: grid;
|
|
@@ -213,6 +486,211 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
213
486
|
.field.full { grid-column: 1 / -1; }
|
|
214
487
|
label { color: var(--muted); font-size: 11px; text-transform: uppercase; }
|
|
215
488
|
|
|
489
|
+
.library-tools {
|
|
490
|
+
display: grid;
|
|
491
|
+
gap: 10px;
|
|
492
|
+
padding: 12px;
|
|
493
|
+
border-bottom: 1px solid var(--line-soft);
|
|
494
|
+
background: rgba(255, 255, 255, 0.54);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.filter-grid {
|
|
498
|
+
display: grid;
|
|
499
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
500
|
+
gap: 8px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.filter-grid .wide { grid-column: 1 / -1; }
|
|
504
|
+
|
|
505
|
+
.summary-grid {
|
|
506
|
+
display: grid;
|
|
507
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
508
|
+
gap: 8px;
|
|
509
|
+
padding: 12px;
|
|
510
|
+
border-bottom: 1px solid var(--line-soft);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.settings-form {
|
|
514
|
+
display: grid;
|
|
515
|
+
gap: 10px;
|
|
516
|
+
padding: 12px;
|
|
517
|
+
border-bottom: 1px solid var(--line-soft);
|
|
518
|
+
background: rgba(255, 255, 255, 0.54);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.settings-form .segmented {
|
|
522
|
+
justify-self: start;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.provider-fields {
|
|
526
|
+
display: grid;
|
|
527
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
528
|
+
gap: 8px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.status-line {
|
|
532
|
+
display: flex;
|
|
533
|
+
flex-wrap: wrap;
|
|
534
|
+
align-items: center;
|
|
535
|
+
gap: 8px;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.status-dot {
|
|
539
|
+
width: 9px;
|
|
540
|
+
height: 9px;
|
|
541
|
+
border-radius: 999px;
|
|
542
|
+
background: var(--faint);
|
|
543
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--faint), transparent 78%);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.status-dot.available {
|
|
547
|
+
background: var(--ok);
|
|
548
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok), transparent 80%);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.status-dot.unavailable {
|
|
552
|
+
background: var(--warn);
|
|
553
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn), transparent 80%);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.profile-list {
|
|
557
|
+
display: grid;
|
|
558
|
+
gap: 8px;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.profile-row {
|
|
562
|
+
border: 1px solid var(--line);
|
|
563
|
+
border-radius: 8px;
|
|
564
|
+
padding: 9px;
|
|
565
|
+
background: rgba(255, 255, 255, 0.68);
|
|
566
|
+
display: grid;
|
|
567
|
+
gap: 6px;
|
|
568
|
+
overflow-wrap: anywhere;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.metric {
|
|
572
|
+
border: 1px solid var(--line);
|
|
573
|
+
border-radius: 8px;
|
|
574
|
+
padding: 10px;
|
|
575
|
+
background: rgba(255, 255, 255, 0.72);
|
|
576
|
+
min-height: 74px;
|
|
577
|
+
display: grid;
|
|
578
|
+
align-content: space-between;
|
|
579
|
+
gap: 6px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.metric-value {
|
|
583
|
+
font-size: 21px;
|
|
584
|
+
line-height: 1;
|
|
585
|
+
font-weight: 720;
|
|
586
|
+
color: var(--text);
|
|
587
|
+
overflow-wrap: anywhere;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.metric-label {
|
|
591
|
+
color: var(--muted);
|
|
592
|
+
font-size: 11px;
|
|
593
|
+
text-transform: uppercase;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.insight-grid {
|
|
597
|
+
display: grid;
|
|
598
|
+
gap: 10px;
|
|
599
|
+
padding: 12px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.insight-block {
|
|
603
|
+
border-top: 1px solid var(--line-soft);
|
|
604
|
+
padding-top: 10px;
|
|
605
|
+
display: grid;
|
|
606
|
+
gap: 8px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.rank-list {
|
|
610
|
+
display: grid;
|
|
611
|
+
gap: 7px;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.rank-row {
|
|
615
|
+
display: grid;
|
|
616
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
617
|
+
gap: 8px;
|
|
618
|
+
align-items: center;
|
|
619
|
+
font-size: 13px;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.rank-label { overflow-wrap: anywhere; }
|
|
623
|
+
.rank-count { color: var(--muted); font-size: 12px; }
|
|
624
|
+
|
|
625
|
+
.bar-track {
|
|
626
|
+
grid-column: 1 / -1;
|
|
627
|
+
height: 5px;
|
|
628
|
+
border-radius: 999px;
|
|
629
|
+
background: var(--line-soft);
|
|
630
|
+
overflow: hidden;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.bar-fill {
|
|
634
|
+
height: 100%;
|
|
635
|
+
border-radius: inherit;
|
|
636
|
+
background: var(--accent);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.trap-rows {
|
|
640
|
+
display: grid;
|
|
641
|
+
gap: 10px;
|
|
642
|
+
padding: 12px;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.text-block {
|
|
646
|
+
display: grid;
|
|
647
|
+
gap: 6px;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.text-block .content {
|
|
651
|
+
white-space: pre-wrap;
|
|
652
|
+
line-height: 1.48;
|
|
653
|
+
overflow-wrap: anywhere;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.code-block {
|
|
657
|
+
margin: 0;
|
|
658
|
+
border: 1px solid var(--line);
|
|
659
|
+
border-radius: 8px;
|
|
660
|
+
padding: 10px;
|
|
661
|
+
background: #17201d;
|
|
662
|
+
color: #eef6f0;
|
|
663
|
+
overflow: auto;
|
|
664
|
+
line-height: 1.45;
|
|
665
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
666
|
+
font-size: 12px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.detail-kv {
|
|
670
|
+
display: grid;
|
|
671
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
672
|
+
gap: 8px;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.kv {
|
|
676
|
+
border: 1px solid var(--line);
|
|
677
|
+
border-radius: 8px;
|
|
678
|
+
padding: 9px;
|
|
679
|
+
background: rgba(255, 255, 255, 0.62);
|
|
680
|
+
overflow-wrap: anywhere;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.kv-label {
|
|
684
|
+
color: var(--muted);
|
|
685
|
+
font-size: 11px;
|
|
686
|
+
text-transform: uppercase;
|
|
687
|
+
margin-bottom: 4px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.kv-value { font-size: 13px; }
|
|
691
|
+
|
|
692
|
+
.hidden { display: none !important; }
|
|
693
|
+
|
|
216
694
|
.section {
|
|
217
695
|
border-top: 1px solid var(--line-soft);
|
|
218
696
|
padding: 12px;
|
|
@@ -231,15 +709,45 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
231
709
|
.warning { border-color: color-mix(in srgb, var(--warn), var(--line) 50%); color: var(--warn); }
|
|
232
710
|
.conflict { border-color: color-mix(in srgb, var(--danger), var(--line) 45%); }
|
|
233
711
|
.review-note { border-color: color-mix(in srgb, var(--accent), var(--line) 55%); }
|
|
712
|
+
|
|
713
|
+
.review-summary {
|
|
714
|
+
padding: 10px 12px 0;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.review-banner {
|
|
718
|
+
display: flex;
|
|
719
|
+
flex-wrap: wrap;
|
|
720
|
+
align-items: center;
|
|
721
|
+
gap: 8px;
|
|
722
|
+
border: 1px solid color-mix(in srgb, var(--warn), var(--line) 55%);
|
|
723
|
+
border-radius: 8px;
|
|
724
|
+
padding: 9px 10px;
|
|
725
|
+
background: color-mix(in srgb, #fff7d6, var(--surface) 35%);
|
|
726
|
+
color: #5f4200;
|
|
727
|
+
font-size: 12px;
|
|
728
|
+
}
|
|
729
|
+
|
|
234
730
|
.actions {
|
|
235
731
|
padding: 12px;
|
|
236
732
|
border-top: 1px solid var(--line-soft);
|
|
237
733
|
display: flex;
|
|
238
734
|
gap: 8px;
|
|
239
735
|
flex-wrap: wrap;
|
|
736
|
+
align-items: center;
|
|
240
737
|
background: rgba(255, 255, 255, 0.018);
|
|
241
738
|
}
|
|
242
739
|
|
|
740
|
+
.action-hint {
|
|
741
|
+
color: var(--muted);
|
|
742
|
+
font-size: 12px;
|
|
743
|
+
line-height: 1.3;
|
|
744
|
+
min-width: 180px;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.action-hint.dirty {
|
|
748
|
+
color: #8a5b00;
|
|
749
|
+
}
|
|
750
|
+
|
|
243
751
|
.empty {
|
|
244
752
|
padding: 28px 18px;
|
|
245
753
|
color: var(--muted);
|
|
@@ -266,511 +774,100 @@ export const WEB_INDEX_HTML = `<!doctype html>
|
|
|
266
774
|
|
|
267
775
|
@media (max-width: 1060px) {
|
|
268
776
|
.shell { grid-template-columns: 1fr; overflow: auto; }
|
|
269
|
-
.
|
|
777
|
+
.splitter { display: none; }
|
|
778
|
+
.sidebar-toggle { display: none; }
|
|
779
|
+
.rail { min-height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
780
|
+
.queue, .detail { min-height: 520px; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
@media (max-width: 520px) {
|
|
784
|
+
.bar { align-items: flex-start; flex-direction: column; }
|
|
785
|
+
.rail-actions { justify-content: flex-start; }
|
|
786
|
+
.filter-grid, .summary-grid, .detail-kv, .provider-fields { grid-template-columns: 1fr; }
|
|
787
|
+
.project-form { grid-template-columns: 1fr auto; }
|
|
270
788
|
}
|
|
271
789
|
</style>
|
|
272
790
|
</head>
|
|
273
791
|
<body>
|
|
274
792
|
<main class="shell">
|
|
793
|
+
<button type="button" class="icon-button sidebar-toggle shell-toggle shell-toggle-left" id="sidebar-toggle" aria-pressed="false" aria-label="Hide sidebar" title="Hide sidebar">
|
|
794
|
+
<span class="sidebar-toggle-icon" aria-hidden="true"></span>
|
|
795
|
+
</button>
|
|
796
|
+
<button type="button" class="icon-button sidebar-toggle queue-toggle shell-toggle shell-toggle-right" id="queue-toggle" aria-pressed="false" aria-label="Hide queue pane" title="Hide queue pane">
|
|
797
|
+
<span class="sidebar-toggle-icon" aria-hidden="true"></span>
|
|
798
|
+
</button>
|
|
799
|
+
<div class="edge-reveal edge-reveal-left" aria-hidden="true"></div>
|
|
800
|
+
<div class="edge-reveal edge-reveal-right" aria-hidden="true"></div>
|
|
275
801
|
<aside class="rail">
|
|
276
802
|
<div class="bar">
|
|
277
803
|
<div>
|
|
278
804
|
<div class="title">codetrap</div>
|
|
279
|
-
<div class="subtle">review console</div>
|
|
805
|
+
<div class="subtle" id="app-subtitle">review console</div>
|
|
806
|
+
</div>
|
|
807
|
+
<div class="rail-actions">
|
|
808
|
+
<div class="segmented main-nav" aria-label="Main view">
|
|
809
|
+
<button type="button" class="active" data-main-view="review">Review</button>
|
|
810
|
+
<button type="button" data-main-view="library">Library</button>
|
|
811
|
+
<button type="button" data-main-view="insights">Insights</button>
|
|
812
|
+
<button type="button" data-main-view="embeddings">Embeddings</button>
|
|
813
|
+
</div>
|
|
814
|
+
<div class="segmented" aria-label="Language">
|
|
815
|
+
<button type="button" data-locale="en">EN</button>
|
|
816
|
+
<button type="button" data-locale="zh">中文</button>
|
|
817
|
+
</div>
|
|
818
|
+
<button class="ghost" id="refresh" title="Refresh">Refresh</button>
|
|
280
819
|
</div>
|
|
281
|
-
<button class="ghost" id="refresh" title="Refresh">Refresh</button>
|
|
282
820
|
</div>
|
|
283
821
|
<form class="project-form" id="project-form">
|
|
284
822
|
<input id="project-path" placeholder="/path/to/project">
|
|
285
|
-
<button type="submit">Add</button>
|
|
823
|
+
<button type="submit" id="project-add">Add</button>
|
|
286
824
|
</form>
|
|
287
825
|
<div class="scroll">
|
|
288
826
|
<div class="stack" id="projects"></div>
|
|
289
827
|
<div class="section">
|
|
290
|
-
<div class="title">sessions</div>
|
|
828
|
+
<div class="title" id="sessions-title">sessions</div>
|
|
291
829
|
<div id="sessions" class="stack" style="padding:0"></div>
|
|
292
830
|
</div>
|
|
293
831
|
</div>
|
|
294
832
|
</aside>
|
|
295
833
|
|
|
296
|
-
<
|
|
297
|
-
<div class="bar">
|
|
298
|
-
<div>
|
|
299
|
-
<div class="title">candidate inbox</div>
|
|
300
|
-
<div class="subtle" id="queue-meta">no project selected</div>
|
|
301
|
-
</div>
|
|
302
|
-
<div class="segmented" aria-label="Candidate view">
|
|
303
|
-
<button type="button" class="active" data-candidate-view="inbox">Inbox</button>
|
|
304
|
-
<button type="button" data-candidate-view="reviewed">Reviewed</button>
|
|
305
|
-
</div>
|
|
306
|
-
</div>
|
|
307
|
-
<div class="scroll">
|
|
308
|
-
<div class="stack" id="candidates"></div>
|
|
309
|
-
</div>
|
|
310
|
-
</section>
|
|
834
|
+
<div class="splitter" data-splitter="left" role="separator" aria-orientation="vertical" aria-label="Resize project and detail panes" tabindex="0"></div>
|
|
311
835
|
|
|
312
836
|
<section class="detail">
|
|
313
837
|
<div class="bar">
|
|
314
838
|
<div>
|
|
315
|
-
<div class="title">candidate detail</div>
|
|
839
|
+
<div class="title" id="detail-title">candidate detail</div>
|
|
316
840
|
<div class="subtle" id="detail-meta">select a candidate</div>
|
|
317
841
|
</div>
|
|
318
842
|
</div>
|
|
319
843
|
<div class="detail-body" id="detail"></div>
|
|
320
844
|
</section>
|
|
321
|
-
</main>
|
|
322
|
-
<div class="status" id="status"></div>
|
|
323
845
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
candidates: [],
|
|
333
|
-
projectRoot: null,
|
|
334
|
-
sessionId: null,
|
|
335
|
-
candidateId: null,
|
|
336
|
-
candidateView: "inbox",
|
|
337
|
-
options: { categories: [], severities: [], scopes: [] },
|
|
338
|
-
conflicts: []
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const el = (id) => document.getElementById(id);
|
|
342
|
-
|
|
343
|
-
async function api(path, options = {}) {
|
|
344
|
-
const headers = { "X-Codetrap-Token": token, ...(options.headers || {}) };
|
|
345
|
-
if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
346
|
-
const res = await fetch(path, { ...options, headers });
|
|
347
|
-
const text = await res.text();
|
|
348
|
-
const data = text ? JSON.parse(text) : null;
|
|
349
|
-
if (!res.ok) {
|
|
350
|
-
const err = new Error(data?.error || res.statusText);
|
|
351
|
-
err.payload = data;
|
|
352
|
-
throw err;
|
|
353
|
-
}
|
|
354
|
-
return data;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function showStatus(message, isError = false) {
|
|
358
|
-
const box = el("status");
|
|
359
|
-
box.textContent = message;
|
|
360
|
-
box.className = "status show" + (isError ? " error" : "");
|
|
361
|
-
clearTimeout(showStatus.timer);
|
|
362
|
-
showStatus.timer = setTimeout(() => box.className = "status", 3200);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async function bootstrap() {
|
|
366
|
-
const data = await api("/api/bootstrap");
|
|
367
|
-
state.projects = data.projects;
|
|
368
|
-
state.projectRoot = data.current_project_root || data.projects[0]?.root || null;
|
|
369
|
-
state.options = data.options;
|
|
370
|
-
renderProjects();
|
|
371
|
-
await loadSessions();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function loadSessions() {
|
|
375
|
-
if (!state.projectRoot) {
|
|
376
|
-
state.sessions = [];
|
|
377
|
-
state.candidates = [];
|
|
378
|
-
renderSessions();
|
|
379
|
-
renderCandidates();
|
|
380
|
-
renderDetail();
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
const data = await api("/api/sessions?project=" + encodeURIComponent(state.projectRoot));
|
|
384
|
-
state.sessions = data.sessions;
|
|
385
|
-
if (!state.sessionId || !state.sessions.some((s) => s.id === state.sessionId)) {
|
|
386
|
-
state.sessionId = state.sessions[0]?.id || null;
|
|
387
|
-
}
|
|
388
|
-
renderSessions();
|
|
389
|
-
await loadCandidates();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async function loadCandidates() {
|
|
393
|
-
if (!state.projectRoot || !state.sessionId) {
|
|
394
|
-
state.candidates = [];
|
|
395
|
-
renderCandidates();
|
|
396
|
-
renderDetail();
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
const data = await api("/api/candidates?project=" + encodeURIComponent(state.projectRoot) + "&session=" + encodeURIComponent(state.sessionId));
|
|
400
|
-
state.candidates = data.candidates;
|
|
401
|
-
selectVisibleCandidate();
|
|
402
|
-
renderCandidates();
|
|
403
|
-
renderDetail();
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function renderProjects() {
|
|
407
|
-
el("projects").innerHTML = state.projects.length ? state.projects.map((project) => \`
|
|
408
|
-
<button class="row \${project.root === state.projectRoot ? "active" : ""}" data-project="\${escapeAttr(project.root)}">
|
|
409
|
-
<span class="row-title">\${escapeHtml(project.name)}</span>
|
|
410
|
-
<span class="subtle">\${escapeHtml(project.root)}</span>
|
|
411
|
-
</button>
|
|
412
|
-
\`).join("") : '<div class="empty">No projects</div>';
|
|
413
|
-
document.querySelectorAll("[data-project]").forEach((button) => {
|
|
414
|
-
button.addEventListener("click", async () => {
|
|
415
|
-
state.projectRoot = button.dataset.project;
|
|
416
|
-
state.sessionId = null;
|
|
417
|
-
state.candidateId = null;
|
|
418
|
-
renderProjects();
|
|
419
|
-
await loadSessions();
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function renderSessions() {
|
|
425
|
-
el("sessions").innerHTML = state.sessions.length ? state.sessions.map((session) => \`
|
|
426
|
-
<button class="row \${session.id === state.sessionId ? "active" : ""}" data-session="\${escapeAttr(session.id)}">
|
|
427
|
-
<span class="row-title">\${escapeHtml(session.goal)}</span>
|
|
428
|
-
<span class="meta">
|
|
429
|
-
<span class="pill">\${escapeHtml(session.status)}</span>
|
|
430
|
-
<span class="pill">\${session.candidate_count || 0} candidates</span>
|
|
431
|
-
<span class="pill accepted">\${session.accepted_count || 0} accepted</span>
|
|
432
|
-
</span>
|
|
433
|
-
</button>
|
|
434
|
-
\`).join("") : '<div class="empty">No sessions</div>';
|
|
435
|
-
document.querySelectorAll("[data-session]").forEach((button) => {
|
|
436
|
-
button.addEventListener("click", async () => {
|
|
437
|
-
state.sessionId = button.dataset.session;
|
|
438
|
-
state.candidateId = null;
|
|
439
|
-
renderSessions();
|
|
440
|
-
await loadCandidates();
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function renderCandidates() {
|
|
446
|
-
const pendingCount = state.candidates.filter((candidate) => candidate.status === "proposed").length;
|
|
447
|
-
const reviewedCount = state.candidates.length - pendingCount;
|
|
448
|
-
const sorted = sortedVisibleCandidates();
|
|
449
|
-
selectVisibleCandidate(sorted);
|
|
450
|
-
const session = state.sessions.find((item) => item.id === state.sessionId);
|
|
451
|
-
el("queue-meta").textContent = session ? session.goal + " / " + pendingCount + " pending, " + reviewedCount + " reviewed" : "no session selected";
|
|
452
|
-
renderCandidateViewTabs(pendingCount, reviewedCount);
|
|
453
|
-
el("candidates").innerHTML = sorted.length ? sorted.map((candidate) => \`
|
|
454
|
-
<button class="row \${candidate.id === state.candidateId ? "active" : ""} \${candidate.status} \${reviewCssClass(candidate)}" data-candidate="\${escapeAttr(candidate.id)}">
|
|
455
|
-
<span class="row-title">\${escapeHtml(candidate.trap.title)}</span>
|
|
456
|
-
<span class="meta">
|
|
457
|
-
<span class="pill \${candidate.status} \${reviewCssClass(candidate)}">\${escapeHtml(reviewLabel(candidate))}</span>
|
|
458
|
-
<span class="pill">q \${Number(candidate.quality_score).toFixed(2)}</span>
|
|
459
|
-
\${candidate.quality.warnings.length ? '<span class="pill warn">' + candidate.quality.warnings.length + ' warnings</span>' : ''}
|
|
460
|
-
</span>
|
|
461
|
-
</button>
|
|
462
|
-
\`).join("") : '<div class="empty">' + (state.candidateView === "inbox" ? "No pending candidates" : "No reviewed candidates") + '</div>';
|
|
463
|
-
document.querySelectorAll("[data-candidate]").forEach((button) => {
|
|
464
|
-
button.addEventListener("click", () => {
|
|
465
|
-
state.candidateId = button.dataset.candidate;
|
|
466
|
-
state.conflicts = [];
|
|
467
|
-
renderCandidates();
|
|
468
|
-
renderDetail();
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function renderCandidateViewTabs(pendingCount, reviewedCount) {
|
|
474
|
-
document.querySelectorAll("[data-candidate-view]").forEach((button) => {
|
|
475
|
-
const view = button.dataset.candidateView;
|
|
476
|
-
const count = view === "inbox" ? pendingCount : reviewedCount;
|
|
477
|
-
button.classList.toggle("active", view === state.candidateView);
|
|
478
|
-
button.textContent = (view === "inbox" ? "Inbox" : "Reviewed") + " " + count;
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function sortedVisibleCandidates() {
|
|
483
|
-
return state.candidates
|
|
484
|
-
.filter(candidateVisible)
|
|
485
|
-
.sort((a, b) => statusRank(a.status) - statusRank(b.status) || b.quality_score - a.quality_score);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function candidateVisible(candidate) {
|
|
489
|
-
return state.candidateView === "inbox" ? candidate.status === "proposed" : candidate.status !== "proposed";
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function selectVisibleCandidate(candidates = sortedVisibleCandidates()) {
|
|
493
|
-
if (!candidates.some((candidate) => candidate.id === state.candidateId)) {
|
|
494
|
-
state.candidateId = candidates[0]?.id || null;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function renderDetail() {
|
|
499
|
-
const candidate = state.candidates.find((item) => item.id === state.candidateId);
|
|
500
|
-
el("detail-meta").textContent = candidate ? candidate.id + " / " + candidate.status : "select a candidate";
|
|
501
|
-
if (!candidate) {
|
|
502
|
-
el("detail").innerHTML = '<div class="empty">No candidate selected</div>';
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
const disabled = candidate.status !== "proposed" ? "disabled" : "";
|
|
506
|
-
el("detail").innerHTML = \`
|
|
507
|
-
<div class="scroll">
|
|
508
|
-
\${renderReviewNotice(candidate)}
|
|
509
|
-
<form class="section" id="candidate-form">
|
|
510
|
-
<div class="form-grid">
|
|
511
|
-
\${field("title", "Title", candidate.trap.title, disabled)}
|
|
512
|
-
\${selectField("category", "Category", candidate.trap.category, state.options.categories, disabled)}
|
|
513
|
-
\${selectField("scope", "Scope", candidate.trap.scope, state.options.scopes, disabled)}
|
|
514
|
-
\${selectField("severity", "Severity", candidate.trap.severity || "warning", state.options.severities, disabled)}
|
|
515
|
-
\${field("tags", "Tags", (candidate.trap.tags || []).join(", "), disabled)}
|
|
516
|
-
\${field("path_globs", "Path globs", (candidate.trap.path_globs || []).join(", "), disabled)}
|
|
517
|
-
\${field("module", "Module", candidate.trap.module || "", disabled)}
|
|
518
|
-
\${field("owner", "Owner", candidate.trap.owner || "", disabled)}
|
|
519
|
-
\${textarea("context", "Context", candidate.trap.context, disabled)}
|
|
520
|
-
\${textarea("mistake", "Mistake", candidate.trap.mistake, disabled)}
|
|
521
|
-
\${textarea("fix", "Fix", candidate.trap.fix, disabled)}
|
|
522
|
-
</div>
|
|
523
|
-
</form>
|
|
524
|
-
<div class="section">
|
|
525
|
-
<div class="meta">
|
|
526
|
-
<span class="pill">quality \${Number(candidate.quality_score).toFixed(2)}</span>
|
|
527
|
-
<span class="pill">conflict \${escapeHtml(candidate.quality.conflict_status)}</span>
|
|
528
|
-
<span class="pill">action \${escapeHtml(candidate.quality.suggested_action)}</span>
|
|
529
|
-
</div>
|
|
530
|
-
\${candidate.quality.warnings.map((warning) => '<div class="warning">' + escapeHtml(warning) + '</div>').join("")}
|
|
531
|
-
</div>
|
|
532
|
-
<div class="section">
|
|
533
|
-
<div class="title">evidence</div>
|
|
534
|
-
\${candidate.evidence.length ? candidate.evidence.map(renderEvidence).join("") : '<div class="empty">No evidence</div>'}
|
|
846
|
+
<div class="splitter" data-splitter="right" role="separator" aria-orientation="vertical" aria-label="Resize detail and queue panes" tabindex="0"></div>
|
|
847
|
+
|
|
848
|
+
<section class="queue">
|
|
849
|
+
<div class="bar">
|
|
850
|
+
<div class="bar-title-group">
|
|
851
|
+
<div>
|
|
852
|
+
<div class="title" id="queue-title">candidate inbox</div>
|
|
853
|
+
<div class="subtle" id="queue-meta">no project selected</div>
|
|
535
854
|
</div>
|
|
536
|
-
\${renderConflicts()}
|
|
537
855
|
</div>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
function renderReviewNotice(candidate) {
|
|
544
|
-
const review = candidate.review;
|
|
545
|
-
if (!review || review.status === "pending") return "";
|
|
546
|
-
if (review.status === "accepted_missing") {
|
|
547
|
-
return \`<div class="section"><div class="warning">\${escapeHtml(review.label)}</div></div>\`;
|
|
548
|
-
}
|
|
549
|
-
if (review.status === "accepted") {
|
|
550
|
-
return \`<div class="section"><div class="evidence review-note">
|
|
551
|
-
<div class="meta">
|
|
552
|
-
<span class="pill accepted">\${escapeHtml(review.label)}</span>
|
|
553
|
-
<span class="pill">\${escapeHtml(review.trap_status)}</span>
|
|
856
|
+
<div class="queue-actions">
|
|
857
|
+
<div class="segmented" id="candidate-tabs" aria-label="Candidate view">
|
|
858
|
+
<button type="button" class="active" data-candidate-view="inbox">Inbox</button>
|
|
859
|
+
<button type="button" data-candidate-view="reviewed">Reviewed</button>
|
|
554
860
|
</div>
|
|
555
|
-
<div class="subtle">\${escapeHtml(review.trap_title)}</div>
|
|
556
|
-
</div></div>\`;
|
|
557
|
-
}
|
|
558
|
-
if (review.status === "rejected") {
|
|
559
|
-
return \`<div class="section"><div class="evidence">
|
|
560
|
-
<div class="meta"><span class="pill rejected">\${escapeHtml(review.label)}</span></div>
|
|
561
|
-
\${review.rejection_reason ? '<div class="subtle">' + escapeHtml(review.rejection_reason) + '</div>' : ''}
|
|
562
|
-
</div></div>\`;
|
|
563
|
-
}
|
|
564
|
-
return "";
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function renderDetailActions(candidate, disabled) {
|
|
568
|
-
if (candidate.status !== "proposed") {
|
|
569
|
-
return \`<div class="actions"><span class="pill \${reviewCssClass(candidate)}">\${escapeHtml(reviewLabel(candidate))}</span></div>\`;
|
|
570
|
-
}
|
|
571
|
-
return \`<div class="actions">
|
|
572
|
-
<button id="save" class="primary" \${disabled}>Save</button>
|
|
573
|
-
<button id="accept" \${disabled}>Accept</button>
|
|
574
|
-
<button id="reject" class="danger" \${disabled}>Reject</button>
|
|
575
|
-
<button id="accept-anyway" \${disabled}>Accept anyway</button>
|
|
576
|
-
<input id="supersedes" placeholder="supersedes id" style="width:150px" \${disabled}>
|
|
577
|
-
<button id="supersede" \${disabled}>Supersede</button>
|
|
578
|
-
</div>\`;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function bindDetailActions(candidate) {
|
|
582
|
-
const save = el("save");
|
|
583
|
-
if (!save) return;
|
|
584
|
-
save.addEventListener("click", async () => {
|
|
585
|
-
try {
|
|
586
|
-
const data = await api("/api/candidate/save", {
|
|
587
|
-
method: "POST",
|
|
588
|
-
body: JSON.stringify(candidatePayload(candidate.id))
|
|
589
|
-
});
|
|
590
|
-
await syncAfterMutation(data.candidate.id);
|
|
591
|
-
showStatus("Candidate saved");
|
|
592
|
-
} catch (error) {
|
|
593
|
-
showStatus(error.message, true);
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
el("accept").addEventListener("click", () => acceptCandidate({}));
|
|
597
|
-
el("accept-anyway").addEventListener("click", () => acceptCandidate({ acceptAnyway: true }));
|
|
598
|
-
el("supersede").addEventListener("click", () => {
|
|
599
|
-
const value = Number.parseInt(el("supersedes").value, 10);
|
|
600
|
-
if (Number.isNaN(value)) return showStatus("Supersedes id is required", true);
|
|
601
|
-
acceptCandidate({ supersedesId: value });
|
|
602
|
-
});
|
|
603
|
-
el("reject").addEventListener("click", async () => {
|
|
604
|
-
const reason = prompt("Reject reason") || "";
|
|
605
|
-
try {
|
|
606
|
-
const data = await api("/api/candidate/reject", {
|
|
607
|
-
method: "POST",
|
|
608
|
-
body: JSON.stringify({ projectRoot: state.projectRoot, sessionId: state.sessionId, candidateId: candidate.id, reason })
|
|
609
|
-
});
|
|
610
|
-
await syncAfterMutation(data.candidate.id);
|
|
611
|
-
showStatus("Candidate rejected");
|
|
612
|
-
} catch (error) {
|
|
613
|
-
showStatus(error.message, true);
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
async function acceptCandidate(extra) {
|
|
619
|
-
try {
|
|
620
|
-
const data = await api("/api/candidate/accept", {
|
|
621
|
-
method: "POST",
|
|
622
|
-
body: JSON.stringify({ projectRoot: state.projectRoot, sessionId: state.sessionId, candidateId: state.candidateId, ...extra })
|
|
623
|
-
});
|
|
624
|
-
await syncAfterMutation(data.candidate.id);
|
|
625
|
-
state.conflicts = [];
|
|
626
|
-
showStatus("Candidate accepted");
|
|
627
|
-
} catch (error) {
|
|
628
|
-
if (error.payload?.possible_conflicts) {
|
|
629
|
-
state.conflicts = error.payload.possible_conflicts;
|
|
630
|
-
showStatus("Possible conflict found", true);
|
|
631
|
-
await loadCandidates();
|
|
632
|
-
state.conflicts = error.payload.possible_conflicts;
|
|
633
|
-
renderDetail();
|
|
634
|
-
} else {
|
|
635
|
-
showStatus(error.message, true);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function candidatePayload(candidateId) {
|
|
641
|
-
const form = new FormData(el("candidate-form"));
|
|
642
|
-
return {
|
|
643
|
-
projectRoot: state.projectRoot,
|
|
644
|
-
sessionId: state.sessionId,
|
|
645
|
-
candidateId,
|
|
646
|
-
trap: {
|
|
647
|
-
title: String(form.get("title") || ""),
|
|
648
|
-
category: String(form.get("category") || ""),
|
|
649
|
-
scope: String(form.get("scope") || ""),
|
|
650
|
-
severity: String(form.get("severity") || ""),
|
|
651
|
-
tags: splitList(form.get("tags")),
|
|
652
|
-
path_globs: splitList(form.get("path_globs")),
|
|
653
|
-
module: blankToNull(form.get("module")),
|
|
654
|
-
owner: blankToNull(form.get("owner")),
|
|
655
|
-
context: String(form.get("context") || ""),
|
|
656
|
-
mistake: String(form.get("mistake") || ""),
|
|
657
|
-
fix: String(form.get("fix") || "")
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function replaceCandidate(candidate) {
|
|
663
|
-
state.candidates = state.candidates.map((item) => item.id === candidate.id ? candidate : item);
|
|
664
|
-
renderCandidates();
|
|
665
|
-
renderDetail();
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
async function syncAfterMutation(candidateId) {
|
|
669
|
-
state.candidateId = candidateId;
|
|
670
|
-
await loadSessions();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
async function refreshAll() {
|
|
674
|
-
try {
|
|
675
|
-
await bootstrap();
|
|
676
|
-
showStatus("Refreshed");
|
|
677
|
-
} catch (error) {
|
|
678
|
-
showStatus(error.message, true);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
el("refresh").addEventListener("click", refreshAll);
|
|
683
|
-
document.querySelectorAll("[data-candidate-view]").forEach((button) => {
|
|
684
|
-
button.addEventListener("click", () => {
|
|
685
|
-
state.candidateView = button.dataset.candidateView;
|
|
686
|
-
state.candidateId = null;
|
|
687
|
-
state.conflicts = [];
|
|
688
|
-
renderCandidates();
|
|
689
|
-
renderDetail();
|
|
690
|
-
});
|
|
691
|
-
});
|
|
692
|
-
el("project-form").addEventListener("submit", async (event) => {
|
|
693
|
-
event.preventDefault();
|
|
694
|
-
try {
|
|
695
|
-
const path = el("project-path").value.trim();
|
|
696
|
-
if (!path) return;
|
|
697
|
-
const data = await api("/api/projects", { method: "POST", body: JSON.stringify({ path }) });
|
|
698
|
-
state.projects = data.projects;
|
|
699
|
-
state.projectRoot = data.project.root;
|
|
700
|
-
state.sessionId = null;
|
|
701
|
-
state.candidateId = null;
|
|
702
|
-
el("project-path").value = "";
|
|
703
|
-
renderProjects();
|
|
704
|
-
await loadSessions();
|
|
705
|
-
} catch (error) {
|
|
706
|
-
showStatus(error.message, true);
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
function field(name, label, value, disabled) {
|
|
711
|
-
return \`<div class="field"><label for="\${name}">\${label}</label><input id="\${name}" name="\${name}" value="\${escapeAttr(value || "")}" \${disabled}></div>\`;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function textarea(name, label, value, disabled) {
|
|
715
|
-
return \`<div class="field full"><label for="\${name}">\${label}</label><textarea id="\${name}" name="\${name}" \${disabled}>\${escapeHtml(value || "")}</textarea></div>\`;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function selectField(name, label, value, options, disabled) {
|
|
719
|
-
return \`<div class="field"><label for="\${name}">\${label}</label><select id="\${name}" name="\${name}" \${disabled}>\${options.map((option) => \`<option value="\${escapeAttr(option)}" \${option === value ? "selected" : ""}>\${escapeHtml(option)}</option>\`).join("")}</select></div>\`;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function renderEvidence(evidence) {
|
|
723
|
-
return \`<div class="evidence">
|
|
724
|
-
<div class="meta">
|
|
725
|
-
<span class="pill">\${escapeHtml(evidence.source_type)}</span>
|
|
726
|
-
\${evidence.source_ref ? '<span class="pill">' + escapeHtml(evidence.source_ref) + '</span>' : ''}
|
|
727
861
|
</div>
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
<div class="conflict">
|
|
737
|
-
<div class="meta"><span class="pill danger">#\${conflict.trap_id}</span><span class="pill">\${escapeHtml(conflict.scope)}</span><span class="pill warn">\${escapeHtml(conflict.reason)}</span></div>
|
|
738
|
-
<strong>\${escapeHtml(conflict.title)}</strong>
|
|
739
|
-
<div class="subtle">\${escapeHtml(conflict.context)}</div>
|
|
740
|
-
<div>\${escapeHtml(conflict.fix)}</div>
|
|
741
|
-
</div>\`).join("")}</div>\`;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function statusRank(status) {
|
|
745
|
-
return status === "proposed" ? 0 : status === "accepted" ? 1 : 2;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function reviewLabel(candidate) {
|
|
749
|
-
return candidate.review?.label || candidate.status;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function reviewCssClass(candidate) {
|
|
753
|
-
return String(candidate.review?.status || candidate.status).replace(/_/g, "-");
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function splitList(value) {
|
|
757
|
-
return String(value || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function blankToNull(value) {
|
|
761
|
-
const text = String(value || "").trim();
|
|
762
|
-
return text ? text : null;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function escapeHtml(value) {
|
|
766
|
-
return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function escapeAttr(value) {
|
|
770
|
-
return escapeHtml(value);
|
|
771
|
-
}
|
|
862
|
+
</div>
|
|
863
|
+
<div class="review-summary hidden" id="review-summary"></div>
|
|
864
|
+
<div class="scroll">
|
|
865
|
+
<div class="stack" id="candidates"></div>
|
|
866
|
+
</div>
|
|
867
|
+
</section>
|
|
868
|
+
</main>
|
|
869
|
+
<div class="status" id="status"></div>
|
|
772
870
|
|
|
773
|
-
|
|
774
|
-
</script>
|
|
871
|
+
<script>${webClientScript()}</script>
|
|
775
872
|
</body>
|
|
776
873
|
</html>`;
|