dev-memory-cli 0.18.2
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/LICENSE +21 -0
- package/README.md +362 -0
- package/bin/dev-memory.js +590 -0
- package/hooks/README.md +84 -0
- package/hooks/codex-hooks.json +32 -0
- package/hooks/hooks.json +60 -0
- package/lib/assets/tidy_review.html +889 -0
- package/lib/dev_memory_branch.py +853 -0
- package/lib/dev_memory_capture.py +1343 -0
- package/lib/dev_memory_common.py +1934 -0
- package/lib/dev_memory_context.py +129 -0
- package/lib/dev_memory_graduate.py +282 -0
- package/lib/dev_memory_setup.py +256 -0
- package/lib/dev_memory_tidy.py +1622 -0
- package/lib/ui-app.html +1052 -0
- package/lib/ui-server.js +330 -0
- package/package.json +52 -0
- package/scripts/hooks/_common.py +456 -0
- package/scripts/hooks/pre_compact.py +21 -0
- package/scripts/hooks/session_end.py +35 -0
- package/scripts/hooks/session_start.py +51 -0
- package/scripts/hooks/stop.py +35 -0
- package/suite-manifest.json +26 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>dev-memory tidy review</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--fg: #1f2328;
|
|
10
|
+
--fg-muted: #59636e;
|
|
11
|
+
--fg-subtle: #8b949e;
|
|
12
|
+
--bg: #f6f8fa;
|
|
13
|
+
--bg-card: #ffffff;
|
|
14
|
+
--bg-card-soft: #fbfcfd;
|
|
15
|
+
--bg-hover: #f0f3f6;
|
|
16
|
+
--border: #d1d9e0;
|
|
17
|
+
--border-subtle: rgba(209, 217, 224, 0.55);
|
|
18
|
+
--accent: #0969da;
|
|
19
|
+
--accept: #1a7f37;
|
|
20
|
+
--accept-bg: #dafbe1;
|
|
21
|
+
--reject: #c4271f;
|
|
22
|
+
--reject-bg: #ffe9e9;
|
|
23
|
+
--custom: #6639ba;
|
|
24
|
+
--custom-bg: #ece4f7;
|
|
25
|
+
--warn-bg: #fff8c5;
|
|
26
|
+
--warn-fg: #9a6700;
|
|
27
|
+
--shadow: 0 1px 3px rgba(31,35,40,0.06);
|
|
28
|
+
--radius: 8px;
|
|
29
|
+
/* Priority palette */
|
|
30
|
+
--p0-fg: #ffffff; --p0-bg: #cf222e;
|
|
31
|
+
--p1-fg: #ffffff; --p1-bg: #d97757;
|
|
32
|
+
--p2-fg: #6e5400; --p2-bg: #f7d54d;
|
|
33
|
+
--p3-fg: #ffffff; --p3-bg: #2c8eef;
|
|
34
|
+
--p4-fg: #4a4a4a; --p4-bg: #d0d4d9;
|
|
35
|
+
}
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root {
|
|
38
|
+
--fg: #e6edf3;
|
|
39
|
+
--fg-muted: #8b949e;
|
|
40
|
+
--fg-subtle: #6e7681;
|
|
41
|
+
--bg: #0d1117;
|
|
42
|
+
--bg-card: #161b22;
|
|
43
|
+
--bg-card-soft: #131820;
|
|
44
|
+
--bg-hover: #1c2128;
|
|
45
|
+
--border: #30363d;
|
|
46
|
+
--border-subtle: rgba(48, 54, 61, 0.5);
|
|
47
|
+
--accent: #58a6ff;
|
|
48
|
+
--accept: #3fb950;
|
|
49
|
+
--accept-bg: #033d1a;
|
|
50
|
+
--reject: #ff7b72;
|
|
51
|
+
--reject-bg: #4d1717;
|
|
52
|
+
--custom: #c8a8ff;
|
|
53
|
+
--custom-bg: #2a1d4d;
|
|
54
|
+
--warn-bg: #4d3800;
|
|
55
|
+
--warn-fg: #eac54f;
|
|
56
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
57
|
+
--p0-bg: #ff6b6b; --p0-fg: #1a0606;
|
|
58
|
+
--p1-bg: #ffa657; --p1-fg: #1a0a02;
|
|
59
|
+
--p2-bg: #d2a8ff; --p2-fg: #1a0030;
|
|
60
|
+
--p3-bg: #58a6ff; --p3-fg: #02112a;
|
|
61
|
+
--p4-bg: #4a4f55; --p4-fg: #d0d4d9;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
* { box-sizing: border-box; }
|
|
66
|
+
html, body { margin: 0; }
|
|
67
|
+
body {
|
|
68
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", system-ui, sans-serif;
|
|
69
|
+
color: var(--fg);
|
|
70
|
+
background: var(--bg);
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
line-height: 1.5;
|
|
73
|
+
padding: 18px 22px 80px;
|
|
74
|
+
}
|
|
75
|
+
.mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
|
|
76
|
+
|
|
77
|
+
.head {
|
|
78
|
+
display: flex; flex-wrap: wrap; align-items: baseline; gap: 14px;
|
|
79
|
+
margin-bottom: 4px;
|
|
80
|
+
}
|
|
81
|
+
h1 { font-size: 19px; font-weight: 600; margin: 0; }
|
|
82
|
+
.scope {
|
|
83
|
+
color: var(--fg-muted); font-size: 12px;
|
|
84
|
+
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.toolbar {
|
|
88
|
+
position: sticky; top: 0; z-index: 10;
|
|
89
|
+
background: var(--bg);
|
|
90
|
+
padding: 10px 0;
|
|
91
|
+
margin: 8px 0 16px;
|
|
92
|
+
border-bottom: 1px solid var(--border);
|
|
93
|
+
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
|
94
|
+
}
|
|
95
|
+
.btn {
|
|
96
|
+
padding: 6px 12px;
|
|
97
|
+
border: 1px solid var(--border);
|
|
98
|
+
background: var(--bg-card);
|
|
99
|
+
border-radius: 6px;
|
|
100
|
+
color: var(--fg);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-size: 12.5px;
|
|
103
|
+
font-family: inherit;
|
|
104
|
+
transition: all 0.1s;
|
|
105
|
+
}
|
|
106
|
+
.btn:hover { background: var(--bg-hover); }
|
|
107
|
+
.btn.primary {
|
|
108
|
+
background: var(--accent); color: #fff; border-color: var(--accent);
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
}
|
|
111
|
+
.btn.primary:hover { filter: brightness(0.95); }
|
|
112
|
+
.summary {
|
|
113
|
+
margin-left: auto;
|
|
114
|
+
color: var(--fg-muted);
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
font-family: ui-monospace, monospace;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.grid {
|
|
120
|
+
display: grid;
|
|
121
|
+
gap: 14px;
|
|
122
|
+
grid-template-columns: 1fr;
|
|
123
|
+
}
|
|
124
|
+
@media (min-width: 1200px) { .grid { grid-template-columns: 1fr 1fr; } }
|
|
125
|
+
@media (min-width: 1800px) { .grid { grid-template-columns: 1fr 1fr 1fr; } }
|
|
126
|
+
|
|
127
|
+
.card {
|
|
128
|
+
background: var(--bg-card);
|
|
129
|
+
border: 1px solid var(--border-subtle);
|
|
130
|
+
border-radius: var(--radius);
|
|
131
|
+
box-shadow: var(--shadow);
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.15s;
|
|
136
|
+
}
|
|
137
|
+
.card.rejected { opacity: 0.55; border-style: dashed; }
|
|
138
|
+
.card.accepted { border-color: var(--accept); }
|
|
139
|
+
.card.custom { border-color: var(--custom); }
|
|
140
|
+
|
|
141
|
+
.card > header {
|
|
142
|
+
padding: 12px 14px 8px;
|
|
143
|
+
display: flex; align-items: flex-start; justify-content: space-between;
|
|
144
|
+
gap: 12px;
|
|
145
|
+
}
|
|
146
|
+
.card .title { font-weight: 600; font-size: 14px; line-height: 1.4; }
|
|
147
|
+
.card .pid {
|
|
148
|
+
display: inline-block; margin-right: 6px;
|
|
149
|
+
color: var(--fg-subtle); font-size: 11px;
|
|
150
|
+
font-family: ui-monospace, monospace;
|
|
151
|
+
}
|
|
152
|
+
.toggle { display: flex; gap: 4px; flex-shrink: 0; }
|
|
153
|
+
.toggle button {
|
|
154
|
+
border: 1px solid var(--border);
|
|
155
|
+
background: var(--bg-card);
|
|
156
|
+
border-radius: 5px;
|
|
157
|
+
padding: 4px 10px;
|
|
158
|
+
font-size: 11px;
|
|
159
|
+
font-family: inherit;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
color: var(--fg-muted);
|
|
162
|
+
transition: all 0.1s;
|
|
163
|
+
}
|
|
164
|
+
.toggle button:hover { background: var(--bg-hover); color: var(--fg); }
|
|
165
|
+
.toggle button.on.accept { background: var(--accept-bg); color: var(--accept); border-color: var(--accept); font-weight: 600; }
|
|
166
|
+
.toggle button.on.reject { background: var(--reject-bg); color: var(--reject); border-color: var(--reject); font-weight: 600; }
|
|
167
|
+
.toggle button.on.custom { background: var(--custom-bg); color: var(--custom); border-color: var(--custom); font-weight: 600; }
|
|
168
|
+
|
|
169
|
+
.priority-badge {
|
|
170
|
+
display: inline-block;
|
|
171
|
+
padding: 1px 7px;
|
|
172
|
+
margin-right: 8px;
|
|
173
|
+
font-size: 10.5px;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
font-family: ui-monospace, monospace;
|
|
176
|
+
letter-spacing: 0.5px;
|
|
177
|
+
border-radius: 4px;
|
|
178
|
+
vertical-align: middle;
|
|
179
|
+
}
|
|
180
|
+
.priority-badge.P0 { background: var(--p0-bg); color: var(--p0-fg); }
|
|
181
|
+
.priority-badge.P1 { background: var(--p1-bg); color: var(--p1-fg); }
|
|
182
|
+
.priority-badge.P2 { background: var(--p2-bg); color: var(--p2-fg); }
|
|
183
|
+
.priority-badge.P3 { background: var(--p3-bg); color: var(--p3-fg); }
|
|
184
|
+
.priority-badge.P4 { background: var(--p4-bg); color: var(--p4-fg); }
|
|
185
|
+
|
|
186
|
+
.custom-feedback {
|
|
187
|
+
border-top: 1px solid var(--border-subtle);
|
|
188
|
+
background: var(--custom-bg);
|
|
189
|
+
padding: 10px 14px;
|
|
190
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
191
|
+
}
|
|
192
|
+
.custom-feedback label {
|
|
193
|
+
font-size: 11px;
|
|
194
|
+
color: var(--custom);
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
letter-spacing: 0.3px;
|
|
197
|
+
}
|
|
198
|
+
.custom-feedback textarea {
|
|
199
|
+
width: 100%;
|
|
200
|
+
min-height: 80px;
|
|
201
|
+
padding: 8px 10px;
|
|
202
|
+
border: 1px solid var(--custom);
|
|
203
|
+
border-radius: 5px;
|
|
204
|
+
background: var(--bg-card);
|
|
205
|
+
color: var(--fg);
|
|
206
|
+
font-family: inherit;
|
|
207
|
+
font-size: 12.5px;
|
|
208
|
+
line-height: 1.5;
|
|
209
|
+
resize: vertical;
|
|
210
|
+
}
|
|
211
|
+
.custom-feedback textarea:focus { outline: none; box-shadow: 0 0 0 2px var(--custom-bg); }
|
|
212
|
+
|
|
213
|
+
.card .reason {
|
|
214
|
+
padding: 0 14px 8px;
|
|
215
|
+
color: var(--fg-muted);
|
|
216
|
+
font-size: 12.5px;
|
|
217
|
+
line-height: 1.5;
|
|
218
|
+
}
|
|
219
|
+
.card .impact {
|
|
220
|
+
padding: 8px 14px 10px;
|
|
221
|
+
border-top: 1px solid var(--border-subtle);
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
color: var(--fg);
|
|
224
|
+
background: var(--bg-card-soft);
|
|
225
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
226
|
+
}
|
|
227
|
+
.impact .row { display: flex; gap: 8px; align-items: flex-start; }
|
|
228
|
+
.impact .badge {
|
|
229
|
+
display: inline-block;
|
|
230
|
+
padding: 1px 7px;
|
|
231
|
+
border-radius: 4px;
|
|
232
|
+
font-size: 10.5px;
|
|
233
|
+
font-weight: 600;
|
|
234
|
+
font-family: ui-monospace, monospace;
|
|
235
|
+
letter-spacing: 0.3px;
|
|
236
|
+
background: var(--warn-bg);
|
|
237
|
+
color: var(--warn-fg);
|
|
238
|
+
flex-shrink: 0;
|
|
239
|
+
}
|
|
240
|
+
.impact .desc { color: var(--fg); }
|
|
241
|
+
.details { border-top: 1px solid var(--border-subtle); }
|
|
242
|
+
.details summary {
|
|
243
|
+
padding: 8px 14px;
|
|
244
|
+
font-size: 11.5px;
|
|
245
|
+
color: var(--fg-muted);
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
user-select: none;
|
|
248
|
+
background: var(--bg-card-soft);
|
|
249
|
+
}
|
|
250
|
+
.details summary:hover { color: var(--fg); }
|
|
251
|
+
.details[open] summary { border-bottom: 1px solid var(--border-subtle); }
|
|
252
|
+
.entry-list {
|
|
253
|
+
margin: 0;
|
|
254
|
+
padding: 8px 14px 12px 28px;
|
|
255
|
+
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
256
|
+
font-size: 11.5px;
|
|
257
|
+
line-height: 1.55;
|
|
258
|
+
color: var(--fg);
|
|
259
|
+
word-break: break-word;
|
|
260
|
+
max-height: 240px;
|
|
261
|
+
overflow-y: auto;
|
|
262
|
+
}
|
|
263
|
+
.entry-list li { margin: 2px 0; }
|
|
264
|
+
.entry-list .file-tag {
|
|
265
|
+
display: inline-block;
|
|
266
|
+
margin-right: 6px;
|
|
267
|
+
color: var(--fg-subtle);
|
|
268
|
+
font-size: 10.5px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.unproposed {
|
|
272
|
+
margin-top: 22px;
|
|
273
|
+
background: var(--bg-card);
|
|
274
|
+
border: 1px solid var(--border-subtle);
|
|
275
|
+
border-radius: var(--radius);
|
|
276
|
+
overflow: hidden;
|
|
277
|
+
}
|
|
278
|
+
.unproposed > summary {
|
|
279
|
+
padding: 12px 16px;
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
font-size: 13px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
color: var(--fg);
|
|
284
|
+
background: var(--bg-card-soft);
|
|
285
|
+
list-style: none;
|
|
286
|
+
}
|
|
287
|
+
.unproposed > summary::-webkit-details-marker { display: none; }
|
|
288
|
+
.unproposed > summary::before { content: "▸ "; color: var(--fg-subtle); }
|
|
289
|
+
.unproposed[open] > summary::before { content: "▾ "; }
|
|
290
|
+
.unproposed[open] > summary { border-bottom: 1px solid var(--border-subtle); }
|
|
291
|
+
.unproposed .file-block {
|
|
292
|
+
padding: 8px 16px 10px;
|
|
293
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
294
|
+
font-size: 12px;
|
|
295
|
+
}
|
|
296
|
+
.unproposed .file-block:last-child { border-bottom: none; }
|
|
297
|
+
.unproposed .file-label {
|
|
298
|
+
font-family: ui-monospace, monospace;
|
|
299
|
+
font-size: 11.5px;
|
|
300
|
+
color: var(--fg-muted);
|
|
301
|
+
margin-bottom: 4px;
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: baseline;
|
|
304
|
+
gap: 10px;
|
|
305
|
+
flex-wrap: wrap;
|
|
306
|
+
}
|
|
307
|
+
.unproposed .file-purpose {
|
|
308
|
+
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", system-ui, sans-serif;
|
|
309
|
+
font-size: 11px;
|
|
310
|
+
color: var(--fg-subtle);
|
|
311
|
+
font-weight: normal;
|
|
312
|
+
font-style: italic;
|
|
313
|
+
}
|
|
314
|
+
.unproposed .uplus {
|
|
315
|
+
margin: 0; padding-left: 18px;
|
|
316
|
+
font-family: ui-monospace, monospace;
|
|
317
|
+
font-size: 11.5px;
|
|
318
|
+
color: var(--fg);
|
|
319
|
+
word-break: break-word;
|
|
320
|
+
}
|
|
321
|
+
.unproposed .uplus li { margin: 3px 0; }
|
|
322
|
+
.unproposed .uplus li .extra-action {
|
|
323
|
+
margin-left: 8px;
|
|
324
|
+
font-size: 10.5px;
|
|
325
|
+
color: var(--fg-subtle);
|
|
326
|
+
cursor: pointer;
|
|
327
|
+
border: 1px solid var(--border);
|
|
328
|
+
padding: 1px 6px;
|
|
329
|
+
border-radius: 3px;
|
|
330
|
+
background: var(--bg-card);
|
|
331
|
+
user-select: none;
|
|
332
|
+
}
|
|
333
|
+
.unproposed .uplus li .extra-action.on {
|
|
334
|
+
background: var(--reject-bg); color: var(--reject); border-color: var(--reject);
|
|
335
|
+
}
|
|
336
|
+
.unproposed .unproposed-block {
|
|
337
|
+
border-left: 2px solid var(--border-subtle);
|
|
338
|
+
padding: 4px 8px 4px 12px;
|
|
339
|
+
margin: 6px 0 10px;
|
|
340
|
+
background: rgba(255,255,255,0.4);
|
|
341
|
+
}
|
|
342
|
+
.unproposed .unproposed-block-head {
|
|
343
|
+
display: flex; align-items: center; gap: 6px;
|
|
344
|
+
flex-wrap: wrap;
|
|
345
|
+
margin-bottom: 4px;
|
|
346
|
+
font-size: 11px;
|
|
347
|
+
color: var(--fg-subtle);
|
|
348
|
+
}
|
|
349
|
+
.unproposed .unproposed-block-head .extra-action {
|
|
350
|
+
margin-left: auto;
|
|
351
|
+
font-size: 10.5px;
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
border: 1px solid var(--border);
|
|
354
|
+
padding: 1px 6px;
|
|
355
|
+
border-radius: 3px;
|
|
356
|
+
background: var(--bg-card);
|
|
357
|
+
user-select: none;
|
|
358
|
+
color: var(--fg-subtle);
|
|
359
|
+
}
|
|
360
|
+
.unproposed .unproposed-block-head .extra-action.on {
|
|
361
|
+
background: var(--reject-bg); color: var(--reject); border-color: var(--reject);
|
|
362
|
+
}
|
|
363
|
+
.unproposed .unproposed-block-preview {
|
|
364
|
+
font-family: ui-monospace, monospace;
|
|
365
|
+
color: var(--fg);
|
|
366
|
+
word-break: break-word;
|
|
367
|
+
flex: 1 1 auto;
|
|
368
|
+
min-width: 0;
|
|
369
|
+
}
|
|
370
|
+
.unproposed .unproposed-section { margin-bottom: 8px; }
|
|
371
|
+
|
|
372
|
+
.notes {
|
|
373
|
+
margin-top: 18px;
|
|
374
|
+
background: var(--bg-card);
|
|
375
|
+
border: 1px solid var(--border-subtle);
|
|
376
|
+
border-radius: var(--radius);
|
|
377
|
+
padding: 12px 14px;
|
|
378
|
+
}
|
|
379
|
+
.notes label { font-size: 12px; color: var(--fg-muted); display: block; margin-bottom: 6px; font-weight: 600; }
|
|
380
|
+
.notes textarea {
|
|
381
|
+
width: 100%; min-height: 60px;
|
|
382
|
+
padding: 7px 10px;
|
|
383
|
+
border: 1px solid var(--border);
|
|
384
|
+
border-radius: 6px;
|
|
385
|
+
background: var(--bg);
|
|
386
|
+
color: var(--fg);
|
|
387
|
+
font-family: inherit;
|
|
388
|
+
font-size: 12.5px;
|
|
389
|
+
resize: vertical;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.guide {
|
|
393
|
+
font-size: 12px;
|
|
394
|
+
color: var(--fg-muted);
|
|
395
|
+
margin-top: 18px;
|
|
396
|
+
padding: 10px 14px;
|
|
397
|
+
background: var(--bg-card);
|
|
398
|
+
border: 1px dashed var(--border);
|
|
399
|
+
border-radius: var(--radius);
|
|
400
|
+
line-height: 1.65;
|
|
401
|
+
}
|
|
402
|
+
.guide code { font-family: ui-monospace, monospace; background: var(--bg); padding: 1px 5px; border-radius: 3px; font-size: 11.5px; }
|
|
403
|
+
.empty {
|
|
404
|
+
padding: 14px;
|
|
405
|
+
color: var(--fg-muted);
|
|
406
|
+
text-align: center;
|
|
407
|
+
font-size: 12.5px;
|
|
408
|
+
background: var(--bg-card);
|
|
409
|
+
border: 1px dashed var(--border);
|
|
410
|
+
border-radius: var(--radius);
|
|
411
|
+
}
|
|
412
|
+
</style>
|
|
413
|
+
</head>
|
|
414
|
+
<body>
|
|
415
|
+
|
|
416
|
+
<div class="head">
|
|
417
|
+
<h1>dev-memory tidy review</h1>
|
|
418
|
+
<span class="scope" id="scope-label">__SCOPE_LABEL_PLACEHOLDER__</span>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div class="toolbar">
|
|
422
|
+
<button class="btn" onclick="bulkSet(true)">全部 accept</button>
|
|
423
|
+
<button class="btn" onclick="bulkSet(false)">全部 reject</button>
|
|
424
|
+
<button class="btn primary" onclick="exportPlan()">导出 plan.json</button>
|
|
425
|
+
<span class="summary" id="summary"></span>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div id="proposals" class="grid"></div>
|
|
429
|
+
|
|
430
|
+
<details class="unproposed" id="unproposed">
|
|
431
|
+
<summary id="unproposed-summary">未被任何 proposal 覆盖的条目</summary>
|
|
432
|
+
<div id="unproposed-body"></div>
|
|
433
|
+
</details>
|
|
434
|
+
|
|
435
|
+
<div class="notes">
|
|
436
|
+
<label for="notes">备注(可选,写给 agent 看)</label>
|
|
437
|
+
<textarea id="notes" placeholder="例:这次跳过了 X 因为它还在跑;下次 tidy 该重点看 Y..."></textarea>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div class="guide">
|
|
441
|
+
<strong>使用说明</strong><br>
|
|
442
|
+
· 主视图是 <strong>proposal 卡片</strong>,按 priority 排序(<span class="priority-badge P0">P0</span> 最紧急 → <span class="priority-badge P4">P4</span> 可选),agent 已经把相关条目聚合好<br>
|
|
443
|
+
· 每张卡片三选一:<code>accept</code>(按原方案做)/ <code>reject</code>(不动)/ <code>custom</code>(写文字反馈让 agent 参考你的思路处理)<br>
|
|
444
|
+
· 默认每张卡都是 accept。同意就跳过,不同意点 reject,要按你思路改写就点 custom 写反馈<br>
|
|
445
|
+
· 漏网之鱼:底部"未提议"折叠区可看 agent 没聚合的 entry,里面提供单条"标 delete"按钮<br>
|
|
446
|
+
· 审完点蓝色"导出 plan.json" → 文件下载到 Downloads → 告诉 agent 路径继续:accept 的会被 apply(永远先备份),custom 的 agent 读完反馈后视情况处理(直接动手 / 跟你澄清 / 重新出方案)
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<script>
|
|
450
|
+
"use strict";
|
|
451
|
+
|
|
452
|
+
const TIDY = __TIDY_DATA_PLACEHOLDER__;
|
|
453
|
+
|
|
454
|
+
const FILE_PURPOSE = {
|
|
455
|
+
"branch/overview.md": "当前分支冷启动摘要:目标 / 范围 / 当前阶段 / 关键约束 / 关联资料",
|
|
456
|
+
"branch/decisions.md": "当前分支的稳定决策 + 为什么这么决定(Why)",
|
|
457
|
+
"branch/progress.md": "当前分支进展 + 下一步 + 自动同步区(git 改动概览)",
|
|
458
|
+
"branch/risks.md": "当前分支的阻塞 / 注意点 / 续做前要留意的事",
|
|
459
|
+
"branch/glossary.md": "当前分支术语 / 有效上下文 / 源资料入口",
|
|
460
|
+
"branch/unsorted.md": "heuristic 不确定分类的内容;下次 setup 或 capture --merge 会分到 decisions/progress/risks/glossary",
|
|
461
|
+
"branch/pending_promotion.md": "capture 自动打标的跨分支候选;graduate 时优先从这里 harvest 提到 repo 共享层",
|
|
462
|
+
"repo/overview.md": "仓库级长期目标 + 关键约束(跨分支共享,所有分支会话都会读)",
|
|
463
|
+
"repo/decisions.md": "仓库级跨分支通用决策(一次写好,所有分支共享读)",
|
|
464
|
+
"repo/glossary.md": "仓库级长期有效背景 / 共享入口 / 共享注意点"
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// proposalState[pid] = "accept" | "reject" | "custom"
|
|
468
|
+
const proposalState = {};
|
|
469
|
+
// customFeedback[pid] = textarea contents (preserved across toggles)
|
|
470
|
+
const customFeedback = {};
|
|
471
|
+
// extraActions[key] = "delete" (entry-level, key is entry id)
|
|
472
|
+
// | "delete-block" (block-level, key is block id)
|
|
473
|
+
// Mixed namespace works because entry ids contain "::" + integer and
|
|
474
|
+
// block ids contain "::block-" + integer — no collision.
|
|
475
|
+
const extraActions = {};
|
|
476
|
+
const allFiles = TIDY.files || [];
|
|
477
|
+
const allBlocks = TIDY.blocks || [];
|
|
478
|
+
// blocksByFileSection[file_key][section_idx] = [block, ...]
|
|
479
|
+
const blocksByFileSection = (function () {
|
|
480
|
+
const m = {};
|
|
481
|
+
for (const b of allBlocks) {
|
|
482
|
+
if (!m[b.file_key]) m[b.file_key] = {};
|
|
483
|
+
if (!m[b.file_key][b.section_idx]) m[b.file_key][b.section_idx] = [];
|
|
484
|
+
m[b.file_key][b.section_idx].push(b);
|
|
485
|
+
}
|
|
486
|
+
return m;
|
|
487
|
+
})();
|
|
488
|
+
function blockById(bid) {
|
|
489
|
+
for (const b of allBlocks) if (b.id === bid) return b;
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
function isBlockId(key) { return typeof key === "string" && key.indexOf("::block-") !== -1; }
|
|
493
|
+
const PRIORITY_ORDER = { P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, "": 5 };
|
|
494
|
+
// Sort proposals by priority (P0 → P4), unprioritized last; tiebreak by id.
|
|
495
|
+
const allProposals = (TIDY.proposals || [])
|
|
496
|
+
.slice()
|
|
497
|
+
.sort(function (a, b) {
|
|
498
|
+
const pa = PRIORITY_ORDER[(a.priority || "").toUpperCase()] ?? 5;
|
|
499
|
+
const pb = PRIORITY_ORDER[(b.priority || "").toUpperCase()] ?? 5;
|
|
500
|
+
if (pa !== pb) return pa - pb;
|
|
501
|
+
return String(a.id).localeCompare(String(b.id));
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
(function init() {
|
|
505
|
+
for (const p of allProposals) proposalState[p.id] = "accept";
|
|
506
|
+
})();
|
|
507
|
+
|
|
508
|
+
function escapeHtml(s) {
|
|
509
|
+
return String(s == null ? "" : s)
|
|
510
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function entryById(eid) {
|
|
514
|
+
for (const f of allFiles) {
|
|
515
|
+
for (const s of f.sections) {
|
|
516
|
+
for (const e of s.entries) {
|
|
517
|
+
const id = f.file_key + "::" + s.section_idx + "::" + e.entry_idx;
|
|
518
|
+
if (id === eid) return { f: f, s: s, e: e };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function sectionByIdx(file_key, section_idx) {
|
|
526
|
+
const f = allFiles.find(function (x) { return x.file_key === file_key; });
|
|
527
|
+
if (!f) return null;
|
|
528
|
+
return f.sections.find(function (s) { return s.section_idx === section_idx; });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function summarizeAction(action) {
|
|
532
|
+
if (action.type === "reset-file") {
|
|
533
|
+
return { badge: "RESET FILE", desc: "重置 " + action.file_key + ".md 回 v2 模板(清掉所有内容包括 section 骨架)" };
|
|
534
|
+
}
|
|
535
|
+
if (action.type === "delete-section") {
|
|
536
|
+
const s = sectionByIdx(action.file_key, action.section_idx);
|
|
537
|
+
const title = s ? s.title : "section[" + action.section_idx + "]";
|
|
538
|
+
const cnt = s ? s.entries.length : "?";
|
|
539
|
+
return { badge: "DELETE SECTION", desc: action.file_key + ".md → 整段删除 \"" + title + "\"(含 " + cnt + " 条)" };
|
|
540
|
+
}
|
|
541
|
+
if (action.type === "delete-block") {
|
|
542
|
+
const b = blockById(action.block_id);
|
|
543
|
+
if (b) {
|
|
544
|
+
const cnt = (b.entry_ids || []).length;
|
|
545
|
+
const orphan = b.has_orphan_paragraphs ? " + 含 Why/How 段落" : "";
|
|
546
|
+
const hashGuard = action.expected_content_hash ? "(hash 校验启用)" : "";
|
|
547
|
+
return { badge: "DELETE BLOCK", desc: b.file + " → " + (b.first_line_preview || "").slice(0, 60) + " 等 " + cnt + " 条" + orphan + hashGuard };
|
|
548
|
+
}
|
|
549
|
+
return { badge: "DELETE BLOCK", desc: action.block_id + "(block 未在当前 prepare 中找到)" };
|
|
550
|
+
}
|
|
551
|
+
if (action.type === "delete-entries") {
|
|
552
|
+
return { badge: "DELETE", desc: "共 " + (action.ids || []).length + " 条 entry" };
|
|
553
|
+
}
|
|
554
|
+
if (action.type === "edit-entries") {
|
|
555
|
+
return { badge: "EDIT", desc: "共 " + (action.edits || []).length + " 条 entry 改写" };
|
|
556
|
+
}
|
|
557
|
+
return { badge: action.type || "?", desc: JSON.stringify(action) };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function entriesPreview(action) {
|
|
561
|
+
const ids = [];
|
|
562
|
+
if (action.type === "delete-entries") for (const id of (action.ids || [])) ids.push(id);
|
|
563
|
+
if (action.type === "edit-entries") for (const e of (action.edits || [])) ids.push(e.id);
|
|
564
|
+
if (action.type === "delete-section") {
|
|
565
|
+
const s = sectionByIdx(action.file_key, action.section_idx);
|
|
566
|
+
if (s) for (const e of s.entries) ids.push(action.file_key + "::" + action.section_idx + "::" + e.entry_idx);
|
|
567
|
+
}
|
|
568
|
+
if (action.type === "delete-block") {
|
|
569
|
+
const b = blockById(action.block_id);
|
|
570
|
+
if (b) for (const eid of (b.entry_ids || [])) ids.push(eid);
|
|
571
|
+
}
|
|
572
|
+
if (action.type === "reset-file") {
|
|
573
|
+
const f = allFiles.find(function (x) { return x.file_key === action.file_key; });
|
|
574
|
+
if (f) for (const s of f.sections) for (const e of s.entries) {
|
|
575
|
+
ids.push(action.file_key + "::" + s.section_idx + "::" + e.entry_idx);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const items = [];
|
|
579
|
+
for (const eid of ids) {
|
|
580
|
+
const ref = entryById(eid);
|
|
581
|
+
if (ref) {
|
|
582
|
+
items.push("<li><span class=\"file-tag\">" + escapeHtml(ref.f.label) + " · " + escapeHtml(ref.s.title) + "</span>" + escapeHtml(ref.e.text) + "</li>");
|
|
583
|
+
} else {
|
|
584
|
+
items.push("<li class=\"file-tag\">" + escapeHtml(eid) + "</li>");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return items.length
|
|
588
|
+
? "<ul class=\"entry-list\">" + items.join("") + "</ul>"
|
|
589
|
+
: "<div class=\"empty\">(无具体 entry 引用)</div>";
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function renderProposals() {
|
|
593
|
+
const root = document.getElementById("proposals");
|
|
594
|
+
root.innerHTML = "";
|
|
595
|
+
if (!allProposals.length) {
|
|
596
|
+
root.innerHTML = "<div class=\"empty\">没有 proposal — agent 应在 prepare 时通过 --proposals-json 注入</div>";
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
for (const p of allProposals) {
|
|
600
|
+
const cur = proposalState[p.id] || "accept";
|
|
601
|
+
const card = document.createElement("div");
|
|
602
|
+
const stateClass = cur === "accept" ? "accepted" : (cur === "custom" ? "custom" : "rejected");
|
|
603
|
+
card.className = "card " + stateClass;
|
|
604
|
+
card.dataset.pid = p.id;
|
|
605
|
+
|
|
606
|
+
const impactRows = (p.actions || []).map(function (a) {
|
|
607
|
+
const s = summarizeAction(a);
|
|
608
|
+
return "<div class=\"row\"><span class=\"badge\">" + escapeHtml(s.badge) + "</span><span class=\"desc\">" + escapeHtml(s.desc) + "</span></div>";
|
|
609
|
+
}).join("");
|
|
610
|
+
|
|
611
|
+
const previewBlocks = (p.actions || []).map(function (a) { return entriesPreview(a); }).join("");
|
|
612
|
+
|
|
613
|
+
const priority = (p.priority || "").toUpperCase();
|
|
614
|
+
const priorityBadge = priority
|
|
615
|
+
? "<span class=\"priority-badge " + priority + "\">" + priority + "</span>"
|
|
616
|
+
: "";
|
|
617
|
+
|
|
618
|
+
const customBlock = cur === "custom"
|
|
619
|
+
? "<div class=\"custom-feedback\">" +
|
|
620
|
+
"<label>你的修改意见</label>" +
|
|
621
|
+
"<textarea data-pid=\"" + escapeHtml(p.id) + "\" placeholder=\"例:不要整段删,只删第一句;或:保留这条但改写成 xxx;或:先放着,下次 tidy 再看\">" + escapeHtml(customFeedback[p.id] || "") + "</textarea>" +
|
|
622
|
+
"</div>"
|
|
623
|
+
: "";
|
|
624
|
+
|
|
625
|
+
card.innerHTML =
|
|
626
|
+
"<header>" +
|
|
627
|
+
"<div class=\"title\">" + priorityBadge + "<span class=\"pid\">" + escapeHtml(p.id) + "</span>" + escapeHtml(p.title) + "</div>" +
|
|
628
|
+
"<div class=\"toggle\">" +
|
|
629
|
+
"<button data-act=\"accept\" class=\"" + (cur === "accept" ? "on accept" : "") + "\">accept</button>" +
|
|
630
|
+
"<button data-act=\"reject\" class=\"" + (cur === "reject" ? "on reject" : "") + "\">reject</button>" +
|
|
631
|
+
"<button data-act=\"custom\" class=\"" + (cur === "custom" ? "on custom" : "") + "\">custom</button>" +
|
|
632
|
+
"</div>" +
|
|
633
|
+
"</header>" +
|
|
634
|
+
(p.reason ? "<div class=\"reason\">" + escapeHtml(p.reason) + "</div>" : "") +
|
|
635
|
+
"<div class=\"impact\">" + impactRows + "</div>" +
|
|
636
|
+
"<details class=\"details\">" +
|
|
637
|
+
"<summary>查看影响 entries</summary>" +
|
|
638
|
+
previewBlocks +
|
|
639
|
+
"</details>" +
|
|
640
|
+
customBlock;
|
|
641
|
+
|
|
642
|
+
for (const btn of card.querySelectorAll(".toggle button")) {
|
|
643
|
+
btn.addEventListener("click", function () {
|
|
644
|
+
proposalState[p.id] = btn.dataset.act;
|
|
645
|
+
renderProposals();
|
|
646
|
+
refreshSummary();
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const ta = card.querySelector(".custom-feedback textarea");
|
|
650
|
+
if (ta) {
|
|
651
|
+
ta.addEventListener("input", function () { customFeedback[p.id] = ta.value; });
|
|
652
|
+
}
|
|
653
|
+
root.appendChild(card);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function renderUnproposed() {
|
|
658
|
+
// covered entry ids — any of these has been claimed by some accepted proposal
|
|
659
|
+
// (or any proposal at all, since the unproposed list is purely informational
|
|
660
|
+
// before the user decides per-card). Track block ids separately so the
|
|
661
|
+
// block-level "标 delete-block" affordance is hidden for already-covered blocks.
|
|
662
|
+
const coveredEntries = new Set();
|
|
663
|
+
const coveredBlocks = new Set();
|
|
664
|
+
for (const p of allProposals) {
|
|
665
|
+
for (const a of (p.actions || [])) {
|
|
666
|
+
if (a.type === "delete-entries") for (const id of (a.ids || [])) coveredEntries.add(id);
|
|
667
|
+
if (a.type === "edit-entries") for (const e of (a.edits || [])) coveredEntries.add(e.id);
|
|
668
|
+
if (a.type === "delete-section") {
|
|
669
|
+
const s = sectionByIdx(a.file_key, a.section_idx);
|
|
670
|
+
if (s) for (const e of s.entries) coveredEntries.add(a.file_key + "::" + a.section_idx + "::" + e.entry_idx);
|
|
671
|
+
const sblocks = (blocksByFileSection[a.file_key] || {})[a.section_idx] || [];
|
|
672
|
+
for (const b of sblocks) coveredBlocks.add(b.id);
|
|
673
|
+
}
|
|
674
|
+
if (a.type === "delete-block") {
|
|
675
|
+
const b = blockById(a.block_id);
|
|
676
|
+
if (b) {
|
|
677
|
+
for (const eid of (b.entry_ids || [])) coveredEntries.add(eid);
|
|
678
|
+
coveredBlocks.add(b.id);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (a.type === "reset-file") {
|
|
682
|
+
const f = allFiles.find(function (x) { return x.file_key === a.file_key; });
|
|
683
|
+
if (f) for (const s of f.sections) for (const e of s.entries) {
|
|
684
|
+
coveredEntries.add(a.file_key + "::" + s.section_idx + "::" + e.entry_idx);
|
|
685
|
+
}
|
|
686
|
+
for (const b of allBlocks) if (b.file_key === a.file_key) coveredBlocks.add(b.id);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const body = document.getElementById("unproposed-body");
|
|
692
|
+
body.innerHTML = "";
|
|
693
|
+
let totalUnproposedEntries = 0;
|
|
694
|
+
for (const f of allFiles) {
|
|
695
|
+
// Group surviving entries by section, then by block (so the user sees the
|
|
696
|
+
// semantic structure rather than a flat bullet list).
|
|
697
|
+
const sectionChunks = [];
|
|
698
|
+
for (const s of f.sections) {
|
|
699
|
+
const sectionBlocks = (blocksByFileSection[f.file_key] || {})[s.section_idx] || [];
|
|
700
|
+
const blockChunks = [];
|
|
701
|
+
// entries that survived covered filter, keyed by block_idx for grouping
|
|
702
|
+
const entryByIdx = {};
|
|
703
|
+
for (const e of s.entries) {
|
|
704
|
+
const id = f.file_key + "::" + s.section_idx + "::" + e.entry_idx;
|
|
705
|
+
if (coveredEntries.has(id)) continue;
|
|
706
|
+
entryByIdx[e.entry_idx] = e;
|
|
707
|
+
}
|
|
708
|
+
if (Object.keys(entryByIdx).length === 0) continue;
|
|
709
|
+
// Map each surviving entry to its enclosing block (entry_idx_range).
|
|
710
|
+
// Blocks whose entries are *all* covered won't appear here.
|
|
711
|
+
for (const b of sectionBlocks) {
|
|
712
|
+
if (coveredBlocks.has(b.id)) continue;
|
|
713
|
+
const blockEntries = [];
|
|
714
|
+
for (const eid of (b.entry_ids || [])) {
|
|
715
|
+
const idx = parseInt(eid.split("::").pop(), 10);
|
|
716
|
+
if (entryByIdx[idx] != null) blockEntries.push({ id: eid, e: entryByIdx[idx] });
|
|
717
|
+
}
|
|
718
|
+
if (!blockEntries.length) continue;
|
|
719
|
+
const blockAction = extraActions[b.id];
|
|
720
|
+
const blockActive = blockAction === "delete-block";
|
|
721
|
+
// Only show block-level affordance when the block has ≥2 entries or
|
|
722
|
+
// has orphan paragraphs — single-entry blocks degenerate into the
|
|
723
|
+
// entry-level case and showing both buttons would be noise.
|
|
724
|
+
const blockButtonHtml = ((b.entry_ids || []).length >= 2 || b.has_orphan_paragraphs)
|
|
725
|
+
? "<span class=\"extra-action " + (blockActive ? "on" : "") + "\" onclick=\"toggleExtraBlock('" + b.id + "')\" title=\"删整个语义单元(含 Why/How 段落,校验 content_hash)\">" +
|
|
726
|
+
(blockActive ? "✓ 标 delete-block" : "标 delete-block") +
|
|
727
|
+
"</span>"
|
|
728
|
+
: "";
|
|
729
|
+
const itemsHtml = blockEntries.map(function (be) {
|
|
730
|
+
const entryAction = extraActions[be.id];
|
|
731
|
+
// When the parent block is flagged for delete-block, suppress the
|
|
732
|
+
// per-entry button to avoid the user double-flagging the same line.
|
|
733
|
+
const entryButton = blockActive
|
|
734
|
+
? "<span class=\"file-tag\" style=\"opacity:0.55\">block 覆盖</span>"
|
|
735
|
+
: "<span class=\"extra-action " + (entryAction === "delete" ? "on" : "") + "\" onclick=\"toggleExtra('" + be.id + "')\">" + (entryAction === "delete" ? "✓ 标 delete" : "标 delete") + "</span>";
|
|
736
|
+
const placeholder = be.e.is_placeholder ? "<span class=\"file-tag\">[占位]</span>" : "";
|
|
737
|
+
return "<li data-eid=\"" + be.id + "\">" + placeholder + escapeHtml(be.e.text) + entryButton + "</li>";
|
|
738
|
+
}).join("");
|
|
739
|
+
const blockHeaderText = (b.first_line_preview || "").slice(0, 80) || "(empty block)";
|
|
740
|
+
const orphanTag = b.has_orphan_paragraphs ? "<span class=\"file-tag\">含 Why/How</span>" : "";
|
|
741
|
+
blockChunks.push(
|
|
742
|
+
"<div class=\"unproposed-block\" data-bid=\"" + b.id + "\">" +
|
|
743
|
+
"<div class=\"unproposed-block-head\">" +
|
|
744
|
+
"<span class=\"file-tag\">" + escapeHtml(b.id) + "</span>" +
|
|
745
|
+
orphanTag +
|
|
746
|
+
"<span class=\"unproposed-block-preview\">" + escapeHtml(blockHeaderText) + "</span>" +
|
|
747
|
+
blockButtonHtml +
|
|
748
|
+
"</div>" +
|
|
749
|
+
"<ul class=\"uplus\">" + itemsHtml + "</ul>" +
|
|
750
|
+
"</div>"
|
|
751
|
+
);
|
|
752
|
+
totalUnproposedEntries += blockEntries.length;
|
|
753
|
+
}
|
|
754
|
+
if (!blockChunks.length) continue;
|
|
755
|
+
sectionChunks.push(
|
|
756
|
+
"<div class=\"unproposed-section\">" +
|
|
757
|
+
"<div class=\"file-tag\" style=\"margin: 6px 0 4px;\">[" + escapeHtml(s.title) + "]</div>" +
|
|
758
|
+
blockChunks.join("") +
|
|
759
|
+
"</div>"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
if (!sectionChunks.length) continue;
|
|
763
|
+
const block = document.createElement("div");
|
|
764
|
+
block.className = "file-block";
|
|
765
|
+
const purpose = FILE_PURPOSE[f.label] || "";
|
|
766
|
+
block.innerHTML =
|
|
767
|
+
"<div class=\"file-label\">" +
|
|
768
|
+
"<span>" + escapeHtml(f.label) + "</span>" +
|
|
769
|
+
(purpose ? "<span class=\"file-purpose\">" + escapeHtml(purpose) + "</span>" : "") +
|
|
770
|
+
"</div>" +
|
|
771
|
+
sectionChunks.join("");
|
|
772
|
+
body.appendChild(block);
|
|
773
|
+
}
|
|
774
|
+
document.getElementById("unproposed-summary").textContent =
|
|
775
|
+
"未被任何 proposal 覆盖的条目(" + totalUnproposedEntries + " 条,按 block 分组,默认全部 keep)";
|
|
776
|
+
if (!totalUnproposedEntries) {
|
|
777
|
+
body.innerHTML = "<div class=\"empty\" style=\"margin: 12px;\">所有条目都已被 proposal 覆盖</div>";
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
window.toggleExtra = function (id) {
|
|
782
|
+
if (extraActions[id] === "delete") delete extraActions[id];
|
|
783
|
+
else extraActions[id] = "delete";
|
|
784
|
+
renderUnproposed();
|
|
785
|
+
refreshSummary();
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
window.toggleExtraBlock = function (blockId) {
|
|
789
|
+
if (extraActions[blockId] === "delete-block") {
|
|
790
|
+
delete extraActions[blockId];
|
|
791
|
+
} else {
|
|
792
|
+
extraActions[blockId] = "delete-block";
|
|
793
|
+
// When toggling on a block, clear any per-entry delete flags inside it —
|
|
794
|
+
// delete-block subsumes them and they'd just produce redundant actions.
|
|
795
|
+
const b = blockById(blockId);
|
|
796
|
+
if (b) for (const eid of (b.entry_ids || [])) {
|
|
797
|
+
if (extraActions[eid] === "delete") delete extraActions[eid];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
renderUnproposed();
|
|
801
|
+
refreshSummary();
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
function bulkSet(accept) {
|
|
805
|
+
const v = accept ? "accept" : "reject";
|
|
806
|
+
for (const p of allProposals) proposalState[p.id] = v;
|
|
807
|
+
renderProposals();
|
|
808
|
+
refreshSummary();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
window.bulkSet = bulkSet;
|
|
812
|
+
|
|
813
|
+
function refreshSummary() {
|
|
814
|
+
let a = 0, r = 0, c = 0;
|
|
815
|
+
for (const v of Object.values(proposalState)) {
|
|
816
|
+
if (v === "accept") a++;
|
|
817
|
+
else if (v === "reject") r++;
|
|
818
|
+
else if (v === "custom") c++;
|
|
819
|
+
}
|
|
820
|
+
const e = Object.keys(extraActions).length;
|
|
821
|
+
let line = allProposals.length + " 个 proposal · accept " + a + " · reject " + r;
|
|
822
|
+
if (c) line += " · custom " + c;
|
|
823
|
+
if (e) line += " · 额外 " + e + " 条 delete";
|
|
824
|
+
document.getElementById("summary").textContent = line;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function exportPlan() {
|
|
828
|
+
const accepted = [];
|
|
829
|
+
const rejected = [];
|
|
830
|
+
const custom = [];
|
|
831
|
+
const actions = [];
|
|
832
|
+
for (const p of allProposals) {
|
|
833
|
+
const st = proposalState[p.id];
|
|
834
|
+
if (st === "accept") {
|
|
835
|
+
accepted.push(p.id);
|
|
836
|
+
for (const a of (p.actions || [])) actions.push(a);
|
|
837
|
+
} else if (st === "reject") {
|
|
838
|
+
rejected.push(p.id);
|
|
839
|
+
} else if (st === "custom") {
|
|
840
|
+
custom.push({
|
|
841
|
+
proposal_id: p.id,
|
|
842
|
+
title: p.title,
|
|
843
|
+
original_actions: p.actions || [],
|
|
844
|
+
user_feedback: (customFeedback[p.id] || "").trim(),
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
for (const key in extraActions) {
|
|
849
|
+
const v = extraActions[key];
|
|
850
|
+
if (v === "delete") {
|
|
851
|
+
// entry-level extra: legacy {id, action} form for apply backward compat
|
|
852
|
+
actions.push({ id: key, action: "delete" });
|
|
853
|
+
} else if (v === "delete-block") {
|
|
854
|
+
// block-level extra: emit a delete-block action and pin content_hash so
|
|
855
|
+
// apply refuses to fire if the block content drifted between prepare/apply
|
|
856
|
+
const b = blockById(key);
|
|
857
|
+
const act = { type: "delete-block", block_id: key };
|
|
858
|
+
if (b && b.content_hash) act.expected_content_hash = b.content_hash;
|
|
859
|
+
actions.push(act);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const plan = {
|
|
863
|
+
tidy_id: TIDY.scope.generated_at,
|
|
864
|
+
scope: { include_repo: !!TIDY.scope.include_repo },
|
|
865
|
+
accepted_proposals: accepted,
|
|
866
|
+
rejected_proposals: rejected,
|
|
867
|
+
custom_proposals: custom,
|
|
868
|
+
actions: actions,
|
|
869
|
+
notes: document.getElementById("notes").value || "",
|
|
870
|
+
};
|
|
871
|
+
const blob = new Blob([JSON.stringify(plan, null, 2)], { type: "application/json" });
|
|
872
|
+
const url = URL.createObjectURL(blob);
|
|
873
|
+
const a = document.createElement("a");
|
|
874
|
+
a.href = url;
|
|
875
|
+
a.download = "tidy_plan_" + TIDY.scope.generated_at + ".json";
|
|
876
|
+
document.body.appendChild(a);
|
|
877
|
+
a.click();
|
|
878
|
+
document.body.removeChild(a);
|
|
879
|
+
URL.revokeObjectURL(url);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
window.exportPlan = exportPlan;
|
|
883
|
+
|
|
884
|
+
renderProposals();
|
|
885
|
+
renderUnproposed();
|
|
886
|
+
refreshSummary();
|
|
887
|
+
</script>
|
|
888
|
+
</body>
|
|
889
|
+
</html>
|