pi-midcompact 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/README.zh-CN.md +16 -1
- package/package.json +4 -2
- package/src/SPEC.md +24 -1
- package/src/content-metrics.ts +75 -3
- package/src/projection.ts +12 -3
- package/src/review-webui.html +837 -481
- package/src/review-webui.ts +150 -57
package/src/review-webui.html
CHANGED
|
@@ -14,313 +14,324 @@
|
|
|
14
14
|
} catch (e) {}
|
|
15
15
|
</script>
|
|
16
16
|
<style>
|
|
17
|
+
/* ---- tokens: zinc-neutral surfaces, color reserved for semantics ---- */
|
|
17
18
|
:root {
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--line-soft: #1a2032;
|
|
24
|
-
--fg: #e6e9f2;
|
|
25
|
-
--fg-dim: #8f9ab4;
|
|
26
|
-
--fg-mute: #5f6a84;
|
|
27
|
-
--accent: #7aa2f7;
|
|
28
|
-
--accent-2: #bb9af7;
|
|
29
|
-
--keep: #9ece6a;
|
|
30
|
-
--range: #7dcfff;
|
|
31
|
-
--warn: #e0af68;
|
|
32
|
-
--danger: #f7768e;
|
|
33
|
-
--mark: rgba(224,175,104,.28);
|
|
34
|
-
--shadow: 0 10px 34px rgba(0,0,0,.45);
|
|
35
|
-
--ring: rgba(122,162,247,.45);
|
|
36
|
-
--r: 8px;
|
|
37
|
-
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
38
|
-
--sans: "HarmonyOS Sans SC", "HarmonyOS Sans", "Noto Sans CJK SC", "Noto Sans SC", "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
19
|
+
--font-sans: Inter, "HarmonyOS Sans SC", "Noto Sans CJK SC", "Microsoft YaHei UI", system-ui, "Segoe UI", sans-serif;
|
|
20
|
+
--font-mono: "JetBrains Mono", ui-monospace, Consolas, Menlo, monospace;
|
|
21
|
+
--r: 8px; --r-lg: 10px;
|
|
22
|
+
--ring: rgba(24, 24, 27, .25);
|
|
23
|
+
--shadow: 0 18px 50px rgba(9, 9, 11, .22);
|
|
39
24
|
}
|
|
40
25
|
[data-theme="light"] {
|
|
41
|
-
--bg: #
|
|
42
|
-
--
|
|
43
|
-
--
|
|
44
|
-
--
|
|
45
|
-
--
|
|
46
|
-
--
|
|
47
|
-
--
|
|
48
|
-
--
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
--
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
--
|
|
55
|
-
--
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
26
|
+
--bg: #fafafa; --panel: #ffffff; --card: #ffffff; --elev: #f4f4f5;
|
|
27
|
+
--border: #e4e4e7; --border-soft: #ececef; --border-strong: #a1a1aa;
|
|
28
|
+
--fg: #09090b; --fg-dim: #52525b; --fg-mute: #71717a;
|
|
29
|
+
--primary: #18181b; --primary-fg: #fafafa;
|
|
30
|
+
--keep: #059669; --warn: #b45309; --danger: #dc2626;
|
|
31
|
+
--sel-bg: #f4f4f5; --sel-border: #09090b;
|
|
32
|
+
--bar-now: #18181b; --bar-fill: rgba(24, 24, 27, .72); --bar-track: rgba(24, 24, 27, .18);
|
|
33
|
+
--tint-read: rgba(14, 165, 233, .14); --tint-read-fg: #0369a1;
|
|
34
|
+
--tint-bash: rgba(245, 158, 11, .17); --tint-bash-fg: #92400e;
|
|
35
|
+
--tint-asst: rgba(139, 92, 246, .14); --tint-asst-fg: #6d28d9;
|
|
36
|
+
--tint-lock: rgba(9, 9, 11, .06); --tint-lock-fg: #52525b;
|
|
37
|
+
}
|
|
38
|
+
[data-theme="dark"] {
|
|
39
|
+
--bg: #09090b; --panel: #0c0c0d; --card: #101012; --elev: #18181b;
|
|
40
|
+
--border: #27272a; --border-soft: #202024; --border-strong: #52525b;
|
|
41
|
+
--fg: #fafafa; --fg-dim: #d4d4d8; --fg-mute: #a1a1aa;
|
|
42
|
+
--primary: #fafafa; --primary-fg: #18181b;
|
|
43
|
+
--keep: #10b981; --warn: #f59e0b; --danger: #ef4444;
|
|
44
|
+
--sel-bg: #18181b; --sel-border: #fafafa;
|
|
45
|
+
--bar-now: #fafafa; --bar-fill: rgba(250, 250, 250, .7); --bar-track: rgba(250, 250, 250, .16);
|
|
46
|
+
--tint-read: rgba(56, 189, 248, .16); --tint-read-fg: #7dd3fc;
|
|
47
|
+
--tint-bash: rgba(245, 158, 11, .18); --tint-bash-fg: #fcd34d;
|
|
48
|
+
--tint-asst: rgba(167, 139, 250, .16); --tint-asst-fg: #c4b5fd;
|
|
49
|
+
--tint-lock: rgba(250, 250, 250, .07); --tint-lock-fg: #a1a1aa;
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
* { box-sizing: border-box; }
|
|
62
53
|
html, body { height: 100%; margin: 0; }
|
|
63
54
|
body {
|
|
64
55
|
background: var(--bg); color: var(--fg);
|
|
65
|
-
font:
|
|
56
|
+
font: 14px/1.6 var(--font-sans);
|
|
66
57
|
display: flex; flex-direction: column;
|
|
67
58
|
-webkit-font-smoothing: antialiased;
|
|
68
59
|
}
|
|
69
60
|
button, input, textarea { font: inherit; color: inherit; }
|
|
70
|
-
:focus-visible { outline: 2px solid var(--
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/* ---------- header ---------- */
|
|
75
|
-
header {
|
|
76
|
-
background: var(--panel); border-bottom: 1px solid var(--line);
|
|
77
|
-
padding: 12px 18px 14px; flex: 0 0 auto;
|
|
78
|
-
}
|
|
79
|
-
.bar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
80
|
-
.brand { font-size: 15px; font-weight: 650; letter-spacing: 0; margin: 0; }
|
|
81
|
-
.brand b { color: var(--accent); font-weight: 750; }
|
|
82
|
-
.chip {
|
|
83
|
-
background: var(--panel-2); border: 1px solid var(--line); color: var(--fg-dim);
|
|
84
|
-
padding: 3px 9px; border-radius: 99px; font-size: 11px; font-family: var(--mono); white-space: nowrap;
|
|
61
|
+
:focus-visible { outline: 2px solid var(--border-strong); outline-offset: 1px; }
|
|
62
|
+
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
63
|
+
@media (prefers-reduced-motion: reduce) {
|
|
64
|
+
*, *::before, *::after { transition: none !important; }
|
|
85
65
|
}
|
|
86
|
-
.chip.dirty { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); }
|
|
87
|
-
.spacer { flex: 1 1 auto; }
|
|
88
66
|
|
|
67
|
+
/* ---- dead session banner ---- */
|
|
68
|
+
.banner { display: none; padding: 9px 18px; font-size: 13px; background: color-mix(in srgb, var(--danger) 12%, var(--panel)); border-bottom: 1px solid var(--danger); color: var(--danger); flex: 0 0 auto; }
|
|
69
|
+
.banner.show { display: block; }
|
|
70
|
+
body.dead .field, body.dead .btn, body.dead .ritem, body.dead .srow { pointer-events: none; opacity: .55; }
|
|
71
|
+
|
|
72
|
+
/* ---- header ---- */
|
|
73
|
+
header { background: var(--panel); border-bottom: 1px solid var(--border); padding: 10px 16px 12px; flex: 0 0 auto; }
|
|
74
|
+
.bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
75
|
+
.brand { font-size: 15px; font-weight: 650; margin: 0; }
|
|
76
|
+
.brand b { color: var(--primary); font-weight: 700; }
|
|
77
|
+
.chip { background: var(--elev); border: 1px solid var(--border); color: var(--fg-dim); padding: 2px 10px; border-radius: 99px; font-size: 12px; font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
78
|
+
.chip.dirty { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, var(--border)); }
|
|
79
|
+
.chip.ok { color: var(--keep); border-color: color-mix(in srgb, var(--keep) 40%, var(--border)); }
|
|
80
|
+
.spacer { flex: 1 1 auto; }
|
|
89
81
|
.btn {
|
|
90
|
-
border: 1px solid var(--
|
|
91
|
-
padding: 6px
|
|
92
|
-
transition: background .13s, border-color .13s, transform .06s, opacity .13s;
|
|
82
|
+
border: 1px solid var(--border); background: var(--card); color: var(--fg);
|
|
83
|
+
padding: 6px 13px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
|
93
84
|
display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
|
|
85
|
+
transition: background .12s, border-color .12s, opacity .12s;
|
|
94
86
|
}
|
|
95
|
-
.btn:hover { background: var(--elev);
|
|
87
|
+
.btn:hover { background: var(--elev); }
|
|
96
88
|
.btn:active { transform: translateY(1px); }
|
|
97
|
-
.btn[disabled] { opacity: .
|
|
98
|
-
.btn-primary { background: var(--
|
|
99
|
-
|
|
100
|
-
.btn-
|
|
101
|
-
.btn-danger { background:
|
|
102
|
-
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); border-color: var(--danger); }
|
|
89
|
+
.btn[disabled] { opacity: .45; cursor: not-allowed; transform: none; }
|
|
90
|
+
.btn-primary { background: var(--primary); border-color: var(--primary); color: var(--primary-fg); font-weight: 600; }
|
|
91
|
+
.btn-primary:hover { background: var(--primary); opacity: .9; }
|
|
92
|
+
.btn-danger { background: transparent; border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); color: var(--danger); }
|
|
93
|
+
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); border-color: var(--danger); }
|
|
103
94
|
.btn-danger.armed { background: var(--danger); border-color: var(--danger); color: #fff; }
|
|
104
|
-
.btn-sm { padding: 4px
|
|
95
|
+
.btn-sm { padding: 4px 11px; font-size: 12.5px; border-radius: 7px; }
|
|
105
96
|
.btn-icon { padding: 6px 9px; }
|
|
106
97
|
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
display: block; font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: var(--fg-mute);
|
|
113
|
-
}
|
|
114
|
-
.metric .val { font-family: var(--mono); font-size: 14px; margin-top: 3px; }
|
|
115
|
-
.metric .val small { color: var(--fg-dim); font-size: 11px; }
|
|
116
|
-
.track {
|
|
117
|
-
position: relative; height: 7px; background: var(--elev); border-radius: 99px; margin-top: 8px; overflow: hidden;
|
|
118
|
-
}
|
|
119
|
-
.track i { position: absolute; inset: 0 auto 0 0; border-radius: 99px; display: block; }
|
|
120
|
-
.track .anchor { background: color-mix(in srgb, var(--warn) 55%, transparent); }
|
|
121
|
-
.track .proj { background: var(--accent); }
|
|
122
|
-
.legend { display: flex; gap: 12px; margin-top: 6px; font-size: 10.5px; color: var(--fg-dim); flex-wrap: wrap; }
|
|
123
|
-
.legend b { font-weight: 500; font-family: var(--mono); color: var(--fg); }
|
|
124
|
-
.dotm { width: 7px; height: 7px; border-radius: 99px; display: inline-block; margin-right: 4px; vertical-align: baseline; }
|
|
125
|
-
|
|
126
|
-
/* ---------- layout ---------- */
|
|
127
|
-
main {
|
|
128
|
-
flex: 1 1 auto; min-height: 0;
|
|
129
|
-
display: grid; gap: 1px; background: var(--line);
|
|
130
|
-
grid-template-columns: 276px minmax(0, 1fr) 388px;
|
|
131
|
-
grid-template-areas: "ranges timeline editor";
|
|
98
|
+
/* ---- decision strip ---- */
|
|
99
|
+
.strip {
|
|
100
|
+
display: flex; align-items: center; flex-wrap: wrap;
|
|
101
|
+
margin-top: 12px; background: var(--card); border: 1px solid var(--border);
|
|
102
|
+
border-radius: var(--r-lg); padding: 11px 16px;
|
|
132
103
|
}
|
|
104
|
+
.di { display: flex; align-items: baseline; gap: 7px; padding: 0 17px; }
|
|
105
|
+
.di:first-child { padding-left: 0; }
|
|
106
|
+
.di + .di { border-left: 1px solid var(--border); }
|
|
107
|
+
.di label { font-size: 12.5px; color: var(--fg-mute); }
|
|
108
|
+
.di .val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 15px; font-weight: 600; white-space: nowrap; }
|
|
109
|
+
.di .val.save { color: var(--keep); font-weight: 700; }
|
|
110
|
+
.di .val.pend { color: var(--warn); }
|
|
111
|
+
.pbar { flex: 1 1 260px; min-width: 240px; padding-left: 17px; border-left: 1px solid var(--border); }
|
|
112
|
+
.pbar .pl { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; color: var(--fg-mute); margin-bottom: 5px; flex-wrap: wrap; }
|
|
113
|
+
.pbar .pl b { color: var(--fg-dim); font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
114
|
+
.pbar .pl .delta { color: var(--keep); font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 700; }
|
|
115
|
+
.pbar .tr { position: relative; height: 12px; background: var(--elev); border-radius: 99px; }
|
|
116
|
+
.pbar .kept { position: absolute; left: 0; top: 0; bottom: 0; background: var(--bar-now); border-radius: 99px 0 0 99px; }
|
|
117
|
+
.pbar .free { position: absolute; top: 0; bottom: 0; background: repeating-linear-gradient(45deg, var(--bar-fill) 0 4px, var(--bar-track) 4px 8px); border-radius: 0 99px 99px 0; }
|
|
118
|
+
.pbar .na { position: absolute; inset: 0; display: grid; place-items: center; font-size: 11.5px; color: var(--fg-mute); }
|
|
119
|
+
|
|
120
|
+
.pbar[data-toggle-mode] { cursor: pointer; }
|
|
121
|
+
.pbar[data-toggle-mode]:hover { background: color-mix(in srgb, var(--elev) 70%, transparent); }
|
|
122
|
+
|
|
123
|
+
/* ---- layout ---- */
|
|
124
|
+
main { flex: 1 1 auto; min-height: 0; display: grid; gap: 1px; background: var(--border); grid-template-columns: 288px minmax(0, 1fr) 430px; grid-template-areas: "ranges timeline editor"; }
|
|
133
125
|
.pane { background: var(--bg); min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
|
134
126
|
.pane.ranges { grid-area: ranges; background: var(--panel); }
|
|
135
|
-
.pane.timeline { grid-area: timeline; }
|
|
136
127
|
.pane.editor { grid-area: editor; background: var(--panel); }
|
|
137
|
-
.pane-head {
|
|
138
|
-
|
|
139
|
-
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
140
|
-
}
|
|
141
|
-
.pane-title { font-size: 11px; letter-spacing: .07em; text-transform: uppercase; color: var(--fg-mute); }
|
|
128
|
+
.pane-head { flex: 0 0 auto; padding: 10px 14px; border-bottom: 1px solid var(--border-soft); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; position: sticky; top: 0; background: inherit; z-index: 3; }
|
|
129
|
+
.pane-title { font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase; color: var(--fg-mute); font-weight: 600; }
|
|
142
130
|
.scroll { flex: 1 1 auto; overflow: auto; min-height: 0; scrollbar-width: thin; }
|
|
143
131
|
.scroll::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
144
132
|
.scroll::-webkit-scrollbar-thumb { background: var(--elev); border-radius: 99px; border: 3px solid transparent; background-clip: content-box; }
|
|
145
|
-
.scroll::-webkit-scrollbar-thumb:hover { background: var(--line); background-clip: content-box; }
|
|
146
133
|
|
|
147
134
|
.search { position: relative; flex: 1 1 140px; }
|
|
148
|
-
.search input {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
.search input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
|
|
153
|
-
.search .ico { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); color: var(--fg-mute); font-size: 12px; }
|
|
154
|
-
.search .clr {
|
|
155
|
-
position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
|
|
156
|
-
background: none; border: 0; color: var(--fg-mute); cursor: pointer; padding: 2px 5px; border-radius: 6px;
|
|
157
|
-
}
|
|
135
|
+
.search input { width: 100%; background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 6px 26px 6px 30px; font-size: 13px; }
|
|
136
|
+
.search input:focus { outline: none; border-color: var(--border-strong); box-shadow: 0 0 0 3px var(--ring); }
|
|
137
|
+
.search .ico { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--fg-mute); font-size: 12px; }
|
|
138
|
+
.search .clr { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); background: none; border: 0; color: var(--fg-mute); cursor: pointer; padding: 2px 6px; border-radius: 6px; }
|
|
158
139
|
.search .clr:hover { color: var(--fg); background: var(--elev); }
|
|
140
|
+
.seg { display: inline-flex; background: var(--elev); border: 1px solid var(--border); border-radius: 8px; padding: 2px; }
|
|
141
|
+
.seg button { border: 0; background: none; color: var(--fg-dim); padding: 3px 11px; border-radius: 6px; cursor: pointer; font-size: 12px; }
|
|
142
|
+
.seg button[aria-pressed="true"] { background: var(--card); color: var(--fg); box-shadow: 0 1px 2px rgba(0,0,0,.08); }
|
|
159
143
|
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
border: 0; background: none; color: var(--fg-dim); padding: 3px 9px; border-radius: 6px; cursor: pointer; font-size: 11.5px;
|
|
163
|
-
}
|
|
164
|
-
.seg button[aria-pressed="true"] { background: var(--elev); color: var(--fg); }
|
|
165
|
-
|
|
166
|
-
/* ---------- range list ---------- */
|
|
167
|
-
.rlist { padding: 8px; display: flex; flex-direction: column; gap: 6px; }
|
|
144
|
+
/* ---- range list ---- */
|
|
145
|
+
.rlist { padding: 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
168
146
|
.ritem {
|
|
169
|
-
width: 100%; text-align: left; background: var(--
|
|
170
|
-
border
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.ritem:hover { background: var(--elev); }
|
|
174
|
-
.ritem[aria-selected="true"] { border-color: var(--range); border-left-color: var(--range); background: color-mix(in srgb, var(--range) 12%, var(--panel-2)); }
|
|
175
|
-
.ritem .r1 { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 12px; }
|
|
176
|
-
.ritem .rid { color: var(--range); font-weight: 700; }
|
|
177
|
-
.ritem .refs { color: var(--fg-mute); font-size: 11px; }
|
|
178
|
-
.ritem .save { margin-left: auto; color: var(--keep); font-size: 11px; }
|
|
179
|
-
.ritem .rsum {
|
|
180
|
-
color: var(--fg-dim); font-size: 11.5px; line-height: 1.45;
|
|
181
|
-
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
147
|
+
position: relative; width: 100%; text-align: left; background: var(--card);
|
|
148
|
+
border: 1px solid var(--border); border-left: 3px solid var(--border); border-radius: var(--r);
|
|
149
|
+
padding: 11px 12px; cursor: pointer; display: grid; gap: 4px;
|
|
150
|
+
transition: background .12s, border-color .12s;
|
|
182
151
|
}
|
|
183
|
-
.ritem
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
.
|
|
189
|
-
.
|
|
152
|
+
.ritem:hover { border-color: var(--border-strong); }
|
|
153
|
+
.ritem[aria-selected="true"] { border-color: var(--sel-border); border-left-color: var(--sel-border); background: var(--sel-bg); }
|
|
154
|
+
.ritem .r1 { display: flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 13px; }
|
|
155
|
+
.ritem .rid { color: var(--fg); font-weight: 700; }
|
|
156
|
+
.ritem .refs { color: var(--fg-mute); font-size: 12px; }
|
|
157
|
+
.ritem .save { margin-left: auto; color: var(--keep); font-size: 12px; font-weight: 700; }
|
|
158
|
+
.ritem .topic { color: var(--fg-dim); font-size: 12px; font-family: var(--font-mono); }
|
|
159
|
+
.ritem .rsum { color: var(--fg-dim); font-size: 12.5px; line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
160
|
+
.ritem .meta { color: var(--fg-mute); font-size: 11.5px; font-family: var(--font-mono); font-variant-numeric: tabular-nums; display: flex; gap: 12px; flex-wrap: wrap; }
|
|
161
|
+
.udot { width: 7px; height: 7px; border-radius: 99px; background: var(--warn); display: inline-block; }
|
|
162
|
+
.pend { color: var(--warn); font-size: 11.5px; }
|
|
163
|
+
.pend::before { content: "● "; font-size: 9px; vertical-align: 1px; }
|
|
164
|
+
.rev { color: var(--keep); font-size: 11.5px; }
|
|
165
|
+
.empty { color: var(--fg-mute); text-align: center; padding: 40px 18px; font-size: 13px; line-height: 1.7; }
|
|
166
|
+
|
|
167
|
+
/* ---- timeline ---- */
|
|
168
|
+
.tl { padding: 12px 14px 44px; display: flex; flex-direction: column; gap: 10px; }
|
|
169
|
+
.group { border: 1px solid var(--border); border-left: 3px solid var(--primary); border-radius: var(--r-lg); overflow: hidden; background: var(--card); }
|
|
170
|
+
.group.keepg { border-left-color: var(--keep); }
|
|
171
|
+
.group.sel { border-color: var(--sel-border); }
|
|
190
172
|
.ghead {
|
|
191
|
-
|
|
192
|
-
display: grid; grid-template-columns:
|
|
193
|
-
padding:
|
|
173
|
+
width: 100%; text-align: left; cursor: pointer;
|
|
174
|
+
display: grid; grid-template-columns: 20px auto minmax(0, 1fr) auto; align-items: center; gap: 10px;
|
|
175
|
+
padding: 10px 12px; background: var(--elev); border: 0; border-bottom: 1px solid var(--border-soft);
|
|
194
176
|
}
|
|
195
|
-
.ghead
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
.chev { color: var(--fg-mute); transition: transform .15s; font-size:
|
|
177
|
+
.group.keepg .ghead { background: transparent; }
|
|
178
|
+
.ghead:hover { filter: brightness(.97); }
|
|
179
|
+
[data-theme="dark"] .ghead:hover { filter: brightness(1.15); }
|
|
180
|
+
.chev { color: var(--fg-mute); transition: transform .15s; font-size: 10px; text-align: center; }
|
|
199
181
|
.group.open .chev { transform: rotate(90deg); }
|
|
200
|
-
.
|
|
201
|
-
.group.keepg .
|
|
202
|
-
.
|
|
203
|
-
.
|
|
204
|
-
.
|
|
205
|
-
.gbody {
|
|
206
|
-
.
|
|
207
|
-
|
|
208
|
-
.atom {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
.atom
|
|
213
|
-
.atom:hover { background: var(--panel-2); }
|
|
214
|
-
.atom .aref { font-family: var(--mono); font-size: 11.5px; color: var(--accent-2); }
|
|
215
|
-
.atom .akind { font-family: var(--mono); font-size: 10.5px; color: var(--fg-dim); overflow: hidden; text-overflow: ellipsis; }
|
|
216
|
-
.atom .atok { font-family: var(--mono); font-size: 11px; color: var(--fg-mute); text-align: right; }
|
|
182
|
+
.gid { font-family: var(--font-mono); font-weight: 700; color: var(--fg); font-size: 13px; }
|
|
183
|
+
.group.keepg .gid { color: var(--keep); }
|
|
184
|
+
.gsum { color: var(--fg-dim); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
185
|
+
.gstat { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--fg-mute); white-space: nowrap; display: flex; align-items: center; gap: 10px; }
|
|
186
|
+
.gstat em { color: var(--keep); font-style: normal; font-weight: 700; }
|
|
187
|
+
.gbody { padding: 4px; }
|
|
188
|
+
.gbody[hidden] { display: none; }
|
|
189
|
+
|
|
190
|
+
.atom { display: grid; grid-template-columns: 64px minmax(0, 1fr) 78px; gap: 12px; align-items: start; padding: 8px 10px; border-radius: 8px; }
|
|
191
|
+
.atom + .atom { border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
|
|
192
|
+
.atom:hover { background: color-mix(in srgb, var(--elev) 55%, transparent); }
|
|
193
|
+
.atom .aref { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--fg-mute); }
|
|
194
|
+
.atom .arow { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
|
217
195
|
.atom .abody {
|
|
218
|
-
|
|
219
|
-
|
|
196
|
+
flex: 1 1 auto; min-width: 0; padding: 0; border: 0; background: none; text-align: left;
|
|
197
|
+
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.6; color: var(--fg); opacity: .84;
|
|
198
|
+
white-space: pre-wrap; word-break: break-word;
|
|
199
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; cursor: pointer;
|
|
220
200
|
}
|
|
221
|
-
.atom
|
|
222
|
-
.atom .
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
.tag.
|
|
228
|
-
.tag.
|
|
229
|
-
.tag.
|
|
230
|
-
.tag.tool
|
|
231
|
-
.tag.tool.
|
|
232
|
-
.tag.tool.bash {
|
|
233
|
-
.
|
|
234
|
-
.atom.selected-atom { background:
|
|
235
|
-
.atom.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
.
|
|
240
|
-
|
|
241
|
-
.
|
|
242
|
-
.
|
|
243
|
-
.
|
|
244
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
.
|
|
201
|
+
.atom .abody:hover { opacity: 1; }
|
|
202
|
+
.atom.user .abody { font-weight: 500; opacity: 1; }
|
|
203
|
+
.atom .atok { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 11.5px; color: var(--fg-dim); font-weight: 500; text-align: right; }
|
|
204
|
+
.sz { display: block; height: 4px; width: 52px; margin: 5px 0 1px auto; border-radius: 99px; background: linear-gradient(to right, var(--bar-fill) var(--w, 30%), var(--bar-track) var(--w, 30%)); }
|
|
205
|
+
.tags { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 5px; }
|
|
206
|
+
.tag { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; padding: 1px 7px; border-radius: 5px; background: var(--elev); color: var(--fg-dim); border: 1px solid transparent; }
|
|
207
|
+
.tag.kind-user { background: var(--primary); color: var(--primary-fg); font-weight: 600; }
|
|
208
|
+
.tag.kind-assistant { background: var(--tint-asst); color: var(--tint-asst-fg); }
|
|
209
|
+
.tag.kind-compressed, .tag.kind-custom, .tag.kind-other, .tag.kind-orphan_tool_result { background: var(--tint-lock); color: var(--tint-lock-fg); }
|
|
210
|
+
.tag.tool { background: var(--elev); }
|
|
211
|
+
.tag.tool.read { background: var(--tint-read); color: var(--tint-read-fg); }
|
|
212
|
+
.tag.tool.bash { background: var(--tint-bash); color: var(--tint-bash-fg); }
|
|
213
|
+
.tag.lock { background: var(--tint-lock); color: var(--tint-lock-fg); }
|
|
214
|
+
.atom.selected-atom { background: var(--sel-bg); outline: 2px solid var(--sel-border); outline-offset: -2px; box-shadow: inset 3px 0 0 var(--sel-border); }
|
|
215
|
+
.atom.cursor-atom { outline: 2px dashed var(--border-strong); outline-offset: -2px; }
|
|
216
|
+
.atom.locked { opacity: .6; }
|
|
217
|
+
.atom.srow { grid-template-columns: 64px minmax(0, 1fr) 128px 78px; align-items: center; }
|
|
218
|
+
.atom.srow .abody { display: block; white-space: nowrap; text-overflow: ellipsis; }
|
|
219
|
+
.mark-highlight { background: color-mix(in srgb, var(--warn) 30%, transparent); color: inherit; border-radius: 3px; padding: 0 1px; }
|
|
220
|
+
mark { background: color-mix(in srgb, var(--warn) 30%, transparent); color: inherit; border-radius: 3px; padding: 0 1px; }
|
|
221
|
+
.srow { cursor: pointer; }
|
|
222
|
+
.mark { font-size: 10.5px; font-family: var(--font-mono); white-space: nowrap; }
|
|
223
|
+
.mark.sel { color: var(--primary); font-weight: 600; }
|
|
224
|
+
.mark.lock { color: var(--fg-mute); }
|
|
225
|
+
|
|
226
|
+
/* ---- projected summary cards ---- */
|
|
227
|
+
.pcard { background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--primary); border-radius: var(--r-lg); padding: 12px 14px; }
|
|
228
|
+
.pcard.pend { border-left-color: var(--warn); border-style: dashed; background: color-mix(in srgb, var(--warn) 6%, var(--panel)); }
|
|
229
|
+
.phead { display: flex; align-items: baseline; gap: 9px; margin-bottom: 6px; flex-wrap: wrap; }
|
|
230
|
+
.pid { font-family: var(--font-mono); font-weight: 700; color: var(--fg); }
|
|
231
|
+
.ptopic { color: var(--fg); font-size: 13px; font-weight: 600; }
|
|
232
|
+
.ptopic.warn { color: var(--warn); }
|
|
233
|
+
.pmeta { margin-left: auto; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 11.5px; color: var(--fg-mute); white-space: nowrap; }
|
|
234
|
+
.ptext { color: var(--fg-dim); font-size: 13px; line-height: 1.65; }
|
|
235
|
+
.pwarnline { color: var(--warn); font-size: 12.5px; }
|
|
236
|
+
.pactions { margin-top: 10px; display: flex; gap: 8px; }
|
|
237
|
+
|
|
238
|
+
/* ---- editor ---- */
|
|
239
|
+
.epad { padding: 16px; display: flex; flex-direction: column; gap: 14px; }
|
|
240
|
+
.etitle { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
|
|
241
|
+
.etitle .eid { font-family: var(--font-mono); font-size: 16px; font-weight: 700; color: var(--fg); }
|
|
242
|
+
.etitle .erefs { font-family: var(--font-mono); font-size: 12px; color: var(--fg-mute); }
|
|
243
|
+
.checkline { display: inline-flex; align-items: center; gap: 7px; color: var(--fg-dim); font-size: 12.5px; cursor: pointer; user-select: none; margin-left: auto; }
|
|
244
|
+
.checkline input { accent-color: var(--keep); width: 15px; height: 15px; }
|
|
245
|
+
.estats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
|
246
|
+
.estat { background: var(--card); border: 1px solid var(--border); border-radius: var(--r); padding: 8px 11px; }
|
|
247
|
+
.estat label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-mute); }
|
|
248
|
+
.estat span { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 13px; }
|
|
249
|
+
.field label { display: flex; align-items: center; gap: 8px; font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-mute); margin-bottom: 5px; }
|
|
250
|
+
.field label .count { margin-left: auto; text-transform: none; letter-spacing: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
252
251
|
.field input, .field textarea {
|
|
253
|
-
width: 100%; background: var(--
|
|
254
|
-
padding:
|
|
252
|
+
width: 100%; background: var(--card); border: 1px solid var(--border); border-radius: var(--r-lg);
|
|
253
|
+
padding: 9px 11px; resize: vertical; font-family: var(--font-mono); font-size: 13px; line-height: 1.65;
|
|
255
254
|
}
|
|
256
|
-
.field textarea { min-height:
|
|
257
|
-
.field input:focus, .field textarea:focus { outline: none; border-color: var(--
|
|
258
|
-
.field.invalid
|
|
259
|
-
.erow {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
border-bottom-width: 2px; border-radius: 5px; padding: 0 4px; color: var(--fg-dim);
|
|
255
|
+
.field textarea { min-height: 130px; }
|
|
256
|
+
.field input:focus, .field textarea:focus { outline: none; border-color: var(--border-strong); box-shadow: 0 0 0 3px var(--ring); }
|
|
257
|
+
.field.invalid textarea { border-color: var(--danger); }
|
|
258
|
+
.erow {
|
|
259
|
+
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
260
|
+
position: sticky; bottom: 0; background: var(--panel); border-top: 1px solid var(--border-soft);
|
|
261
|
+
margin: 0 -16px; padding: 12px 16px;
|
|
264
262
|
}
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
.nextstep { display: none; gap: 10px; align-items: flex-start; background: color-mix(in srgb, var(--keep) 10%, var(--card)); border: 1px solid color-mix(in srgb, var(--keep) 40%, var(--border)); border-radius: var(--r); padding: 10px 13px; font-size: 12.5px; color: var(--fg-dim); }
|
|
264
|
+
.nextstep.show { display: flex; }
|
|
265
|
+
.nextstep b { color: var(--keep); }
|
|
266
|
+
.hint { color: var(--fg-mute); font-size: 12px; line-height: 1.6; }
|
|
267
|
+
kbd { font-family: var(--font-mono); font-size: 11px; background: var(--elev); border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 5px; padding: 0 4px; color: var(--fg-dim); }
|
|
268
|
+
kbd.space { padding: 0 8px; }
|
|
269
|
+
|
|
270
|
+
/* ---- overlays ---- */
|
|
268
271
|
.toast {
|
|
269
|
-
position: fixed; bottom:
|
|
270
|
-
background: var(--
|
|
271
|
-
padding: 9px
|
|
272
|
+
position: fixed; bottom: 20px; left: 50%; transform: translate(-50%, 12px);
|
|
273
|
+
background: var(--elev); border: 1px solid var(--border-strong); color: var(--fg);
|
|
274
|
+
padding: 9px 15px; border-radius: var(--r); box-shadow: var(--shadow); font-size: 12.5px;
|
|
272
275
|
opacity: 0; transition: opacity .18s, transform .18s; pointer-events: none; z-index: 50; max-width: 70vw;
|
|
273
276
|
}
|
|
274
277
|
.toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
275
278
|
.toast.err { border-color: var(--danger); color: var(--danger); }
|
|
276
|
-
.toast.ok { border-color: color-mix(in srgb, var(--keep)
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
background: var(--panel); border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--shadow);
|
|
283
|
-
padding: 18px 20px; min-width: 300px; max-width: 420px;
|
|
279
|
+
.toast.ok { border-color: color-mix(in srgb, var(--keep) 55%, var(--border)); }
|
|
280
|
+
.overlay { position: fixed; inset: 0; background: rgba(9, 9, 11, .5); display: none; place-items: center; z-index: 60; padding: 20px; }
|
|
281
|
+
.overlay.show { display: grid; }
|
|
282
|
+
.card {
|
|
283
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg);
|
|
284
|
+
box-shadow: var(--shadow); padding: 18px 20px; min-width: 330px; max-width: 470px;
|
|
284
285
|
}
|
|
285
|
-
.
|
|
286
|
-
.
|
|
286
|
+
.card h3 { margin: 0 0 10px; font-size: 14px; }
|
|
287
|
+
.card p { margin: 0; color: var(--fg-dim); font-size: 12.5px; }
|
|
288
|
+
.warnbox { margin: 10px 0 0; border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--border)); background: color-mix(in srgb, var(--warn) 9%, transparent); border-radius: var(--r); padding: 9px 12px; font-size: 12.5px; color: var(--warn); display: grid; gap: 4px; }
|
|
289
|
+
.warnbox b { font-family: var(--font-mono); }
|
|
287
290
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
|
|
288
|
-
.help-card dl { display: grid; grid-template-columns: auto 1fr; gap: 8px 14px; margin: 0; font-size:
|
|
291
|
+
.help-card dl { display: grid; grid-template-columns: auto 1fr; gap: 8px 14px; margin: 0; font-size: 12.5px; }
|
|
289
292
|
.help-card dt { text-align: right; }
|
|
290
293
|
.help-card dd { margin: 0; color: var(--fg-dim); }
|
|
291
|
-
.closed { display: grid; place-items: center; height: 100%; color: var(--fg-dim); font-size: 13px; text-align: center; padding: 30px; }
|
|
292
294
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
/* ---- drawer ---- */
|
|
296
|
+
.drawer-bg { position: fixed; inset: 0; background: rgba(9, 9, 11, .5); display: none; z-index: 60; }
|
|
297
|
+
.drawer-bg.show { display: block; }
|
|
298
|
+
.drawer {
|
|
299
|
+
position: fixed; top: 0; right: 0; bottom: 0; width: min(600px, 92vw);
|
|
300
|
+
background: var(--panel); border-left: 1px solid var(--border); box-shadow: var(--shadow);
|
|
301
|
+
z-index: 61; display: none; flex-direction: column;
|
|
299
302
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
303
|
+
.drawer.show { display: flex; }
|
|
304
|
+
.drawer .dhead { padding: 12px 16px; border-bottom: 1px solid var(--border-soft); display: flex; align-items: center; gap: 10px; }
|
|
305
|
+
.drawer .dhead .t { font-family: var(--font-mono); font-weight: 700; color: var(--fg); }
|
|
306
|
+
.drawer .dhead .m { font-size: 11.5px; color: var(--fg-mute); font-family: var(--font-mono); }
|
|
307
|
+
.drawer .dbody { flex: 1; overflow: auto; padding: 14px 16px; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.7; white-space: pre-wrap; word-break: break-word; color: var(--fg-dim); }
|
|
308
|
+
.drawer .dfoot { padding: 9px 16px; border-top: 1px solid var(--border-soft); font-size: 11.5px; color: var(--fg-mute); }
|
|
309
|
+
.closed { display: grid; place-items: center; height: 100%; color: var(--fg-mute); font-size: 13px; text-align: center; padding: 30px; }
|
|
310
|
+
|
|
311
|
+
@media (max-width: 1280px) {
|
|
312
|
+
main { grid-template-columns: 264px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(200px, 44vh); grid-template-areas: "ranges timeline" "editor editor"; }
|
|
310
313
|
}
|
|
311
|
-
@media (max-width:
|
|
312
|
-
.
|
|
313
|
-
|
|
314
|
-
.
|
|
314
|
+
@media (max-width: 880px) {
|
|
315
|
+
.strip .di { padding: 0 12px; }
|
|
316
|
+
main { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(130px, 26vh) minmax(240px, 1fr) auto; grid-template-areas: "ranges" "timeline" "editor"; }
|
|
317
|
+
.pane.editor { max-height: 62vh; }
|
|
318
|
+
.atom { grid-template-columns: 64px minmax(0, 1fr) auto; }
|
|
319
|
+
.atom.srow { grid-template-columns: 64px minmax(0, 1fr) auto; }
|
|
320
|
+
.atom.srow .mark { grid-column: 1 / -1; }
|
|
321
|
+
.atom .atok { grid-column: 1 / -1; text-align: left; }
|
|
322
|
+
.atom .sz { margin-left: 0; }
|
|
315
323
|
}
|
|
316
324
|
</style>
|
|
317
325
|
</head>
|
|
318
326
|
<body>
|
|
327
|
+
<div class="banner" id="dead-banner">Session closed in the terminal — this page is now read-only. You can close this tab.</div>
|
|
328
|
+
|
|
319
329
|
<header>
|
|
320
330
|
<div class="bar">
|
|
321
331
|
<h1 class="brand"><b>Midcompact</b> <span id="mode-title">Review</span></h1>
|
|
322
332
|
<span class="chip" id="draft-badge"></span>
|
|
323
333
|
<span class="chip dirty" id="dirty-badge" hidden></span>
|
|
334
|
+
<span class="chip ok" id="saved-badge" hidden>Saved</span>
|
|
324
335
|
<div class="spacer"></div>
|
|
325
336
|
<button class="btn btn-sm" id="save-all" hidden>Save all</button>
|
|
326
337
|
<button class="btn btn-sm btn-primary" id="save-selection" hidden>Save selection</button>
|
|
@@ -329,7 +340,7 @@
|
|
|
329
340
|
<button class="btn btn-sm btn-icon" id="help-btn" title="Shortcuts (?)" aria-label="Shortcuts">?</button>
|
|
330
341
|
<button class="btn btn-sm btn-primary" id="close">Close & return</button>
|
|
331
342
|
</div>
|
|
332
|
-
<div class="
|
|
343
|
+
<div class="strip" id="strip"></div>
|
|
333
344
|
</header>
|
|
334
345
|
|
|
335
346
|
<main>
|
|
@@ -341,15 +352,19 @@
|
|
|
341
352
|
<button class="clr" id="q-clear" hidden aria-label="Clear filter">✕</button>
|
|
342
353
|
</div>
|
|
343
354
|
</div>
|
|
344
|
-
<div class="scroll"><div class="rlist" id="range-list"
|
|
355
|
+
<div class="scroll"><div class="rlist" id="range-list"></div></div>
|
|
345
356
|
</section>
|
|
346
357
|
|
|
347
|
-
<section class="pane
|
|
358
|
+
<section class="pane" aria-label="Anchor timeline">
|
|
348
359
|
<div class="pane-head">
|
|
349
360
|
<span class="pane-title">Timeline</span>
|
|
361
|
+
<span class="seg" id="mode-seg" role="group" aria-label="Raw or projected">
|
|
362
|
+
<button data-mode="raw" aria-pressed="true">Raw</button>
|
|
363
|
+
<button data-mode="proj" aria-pressed="false">Projected</button>
|
|
364
|
+
</span>
|
|
350
365
|
<span class="chip" id="tl-stat"></span>
|
|
351
366
|
<div class="spacer"></div>
|
|
352
|
-
<div class="seg" role="group" aria-label="Policy filter">
|
|
367
|
+
<div class="seg" id="policy-seg" role="group" aria-label="Policy filter">
|
|
353
368
|
<button data-filter="all" aria-pressed="true">All</button>
|
|
354
369
|
<button data-filter="range" aria-pressed="false">Compressed</button>
|
|
355
370
|
<button data-filter="keep" aria-pressed="false">Kept</button>
|
|
@@ -365,27 +380,33 @@
|
|
|
365
380
|
</main>
|
|
366
381
|
|
|
367
382
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
|
368
|
-
|
|
369
|
-
|
|
383
|
+
|
|
384
|
+
<div class="drawer-bg" id="drawer-bg"></div>
|
|
385
|
+
<div class="drawer" id="drawer" role="dialog" aria-modal="true" aria-label="Atom original text">
|
|
386
|
+
<div class="dhead"><span class="t" id="dw-ref"></span><span class="m" id="dw-meta"></span><div class="spacer"></div><button class="btn btn-sm" id="dw-close">✕</button></div>
|
|
387
|
+
<div class="dbody" id="dw-body"></div>
|
|
388
|
+
<div class="dfoot">Full text is served from the frozen anchor on demand — list previews stay truncated.</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div class="overlay" id="help" role="dialog" aria-modal="true" aria-label="Keyboard shortcuts">
|
|
392
|
+
<div class="card help-card">
|
|
370
393
|
<h3>Keyboard shortcuts</h3>
|
|
371
|
-
<dl id="help-list">
|
|
372
|
-
<dt><kbd>j</kbd> / <kbd>k</kbd></dt><dd>Next / previous range</dd>
|
|
373
|
-
<dt><kbd>/</kbd></dt><dd>Focus filter</dd>
|
|
374
|
-
<dt><kbd>e</kbd></dt><dd>Focus summary editor</dd>
|
|
375
|
-
<dt><kbd>⌘/Ctrl</kbd>+<kbd>S</kbd></dt><dd>Save current range</dd>
|
|
376
|
-
<dt><kbd>r</kbd></dt><dd>Reload state from server</dd>
|
|
377
|
-
<dt><kbd>x</kbd></dt><dd>Expand / collapse selected group</dd>
|
|
378
|
-
<dt><kbd>Esc</kbd></dt><dd>Blur field / close dialog</dd>
|
|
379
|
-
</dl>
|
|
394
|
+
<dl id="help-list"></dl>
|
|
380
395
|
</div>
|
|
381
396
|
</div>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
<
|
|
397
|
+
|
|
398
|
+
<div class="overlay" id="close-dialog" role="dialog" aria-modal="true" aria-label="Unsaved changes">
|
|
399
|
+
<div class="card">
|
|
400
|
+
<h3>Close review?</h3>
|
|
401
|
+
<p id="close-dialog-line"></p>
|
|
402
|
+
<div class="warnbox" id="close-warn" hidden>
|
|
403
|
+
<b><span id="close-warn-count"></span> range still has an empty summary</b>
|
|
404
|
+
<span id="close-warn-list"></span>
|
|
405
|
+
</div>
|
|
386
406
|
<div class="dialog-actions">
|
|
387
407
|
<button class="btn" id="close-stay">Stay</button>
|
|
388
|
-
<button class="btn btn-danger" id="close-discard">Discard</button>
|
|
408
|
+
<button class="btn btn-danger" id="close-discard">Discard edits</button>
|
|
409
|
+
<button class="btn" id="close-fix" hidden>Fix pending</button>
|
|
389
410
|
<button class="btn btn-primary" id="close-save">Save & close</button>
|
|
390
411
|
</div>
|
|
391
412
|
</div>
|
|
@@ -393,49 +414,79 @@
|
|
|
393
414
|
|
|
394
415
|
<script id="state" type="application/json"><!--MIDCOMPACT_STATE--></script>
|
|
395
416
|
<script type="module">
|
|
396
|
-
const EMPTY = { atoms: [], draft: { revision: 0, ranges: [] }, telemetry: {} };
|
|
417
|
+
const EMPTY = { view: 'review', est: null, atoms: [], draft: { revision: 0, ranges: [] }, telemetry: {} };
|
|
397
418
|
let state = (() => {
|
|
398
419
|
try {
|
|
399
420
|
const s = JSON.parse(document.getElementById('state').textContent);
|
|
400
|
-
return {
|
|
421
|
+
return {
|
|
422
|
+
view: s.view ?? 'review',
|
|
423
|
+
est: s.est ?? null,
|
|
424
|
+
atoms: s.atoms ?? [],
|
|
425
|
+
draft: { revision: s.draft?.revision ?? 0, ranges: s.draft?.ranges ?? [] },
|
|
426
|
+
telemetry: s.telemetry ?? {},
|
|
427
|
+
};
|
|
401
428
|
} catch { return EMPTY; }
|
|
402
429
|
})();
|
|
403
430
|
|
|
404
431
|
// The server owns the keepalive writes. If this page disappears, the stream
|
|
405
432
|
// closes and the terminal-side UI lock is released without treating inactivity
|
|
406
|
-
// in an open tab as disconnection.
|
|
433
|
+
// in an open tab as disconnection. A stream that errors and stays down means
|
|
434
|
+
// the workbench was closed in the terminal: flip the page to read-only.
|
|
407
435
|
const liveness = new EventSource('/api/liveness');
|
|
436
|
+
let deadTimer = null;
|
|
437
|
+
function markDead() {
|
|
438
|
+
document.body.classList.add('dead');
|
|
439
|
+
document.getElementById('dead-banner').classList.add('show');
|
|
440
|
+
document.querySelectorAll('input, textarea, button, select').forEach(el => { el.disabled = true; });
|
|
441
|
+
}
|
|
442
|
+
liveness.onopen = () => {
|
|
443
|
+
clearTimeout(deadTimer); deadTimer = null;
|
|
444
|
+
document.body.classList.remove('dead');
|
|
445
|
+
document.getElementById('dead-banner').classList.remove('show');
|
|
446
|
+
};
|
|
447
|
+
liveness.onerror = () => {
|
|
448
|
+
if (!deadTimer) deadTimer = setTimeout(markDead, 4000);
|
|
449
|
+
};
|
|
408
450
|
|
|
409
451
|
let selectedId = state.draft.ranges[0]?.id ?? null;
|
|
410
452
|
let query = '';
|
|
411
453
|
let policy = 'all';
|
|
454
|
+
let mode = 'raw'; // timeline: raw atoms or projected summaries
|
|
412
455
|
const edits = new Map(); // rangeId -> { topic, summary }
|
|
413
|
-
const collapsed = new Set();
|
|
414
|
-
const openAtoms = new Set(); // atom refs expanded to full preview
|
|
456
|
+
const collapsed = new Set();
|
|
415
457
|
let lastScrolled = null;
|
|
416
458
|
let removeArmed = null;
|
|
417
459
|
let selectionDirty = false;
|
|
460
|
+
let selectionRefs = new Set();
|
|
461
|
+
let cursor = 0; // selection-view atom cursor
|
|
418
462
|
|
|
419
|
-
|
|
463
|
+
// Reviewed flags are a page-session convenience, not persisted app state.
|
|
464
|
+
let reviewed = (() => {
|
|
465
|
+
try { return new Set(JSON.parse(sessionStorage.getItem('midcompact-reviewed') || '[]')); }
|
|
466
|
+
catch { return new Set(); }
|
|
467
|
+
})();
|
|
420
468
|
|
|
421
469
|
/* ---------- helpers ---------- */
|
|
422
470
|
const $ = id => document.getElementById(id);
|
|
423
|
-
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
|
|
471
|
+
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
424
472
|
const fmt = n => n == null || Number.isNaN(n) ? '—'
|
|
425
473
|
: Math.abs(n) >= 1000 ? (n / 1000).toFixed(Math.abs(n) % 1000 ? 1 : 0) + 'k' : String(Math.round(n));
|
|
426
474
|
const pct = n => n == null || Number.isNaN(n) ? '—' : n.toFixed(n % 1 ? 1 : 0) + '%';
|
|
427
475
|
const clamp = n => Math.max(0, Math.min(100, n));
|
|
428
476
|
const isSelection = () => state.view === 'selection';
|
|
429
477
|
const atomByRef = ref => state.atoms.find(a => a.ref === ref);
|
|
478
|
+
const isLocked = a => !a.compressible || !a.protocolClosed || a.kind === 'compressed';
|
|
430
479
|
const rangeAtoms = r => {
|
|
431
480
|
const start = state.atoms.findIndex(a => a.ref === r.startRef);
|
|
432
481
|
const end = state.atoms.findIndex(a => a.ref === r.endRef);
|
|
433
482
|
return start >= 0 && end >= start ? state.atoms.slice(start, end + 1) : [];
|
|
434
483
|
};
|
|
435
|
-
|
|
436
|
-
|
|
484
|
+
selectionRefs = new Set(state.draft.ranges.flatMap(r => rangeAtoms(r).map(a => a.ref)));
|
|
485
|
+
// Old-page behavior: unselected ranges start collapsed so the timeline opens
|
|
486
|
+
// on the selection, and Expand all expands from a meaningful state.
|
|
487
|
+
state.draft.ranges.forEach(r => { if (r.id !== selectedId) collapsed.add(r.id); });
|
|
437
488
|
const rangeById = id => state.draft.ranges.find(r => r.id === id);
|
|
438
|
-
const
|
|
489
|
+
const cssEsc = s => (window.CSS && CSS.escape) ? CSS.escape(s) : String(s).replace(/["\\]/g, '\\$&');
|
|
439
490
|
|
|
440
491
|
function hl(text, q) {
|
|
441
492
|
const out = esc(text);
|
|
@@ -443,6 +494,50 @@ function hl(text, q) {
|
|
|
443
494
|
const re = new RegExp(esc(q).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
444
495
|
return out.replace(re, m => '<mark>' + m + '</mark>');
|
|
445
496
|
}
|
|
497
|
+
function classify(text) {
|
|
498
|
+
let narrowChars = 0, wideChars = 0;
|
|
499
|
+
for (const ch of String(text ?? '')) (ch.codePointAt(0) <= 0x7f) ? narrowChars++ : wideChars++;
|
|
500
|
+
return { narrowChars, wideChars };
|
|
501
|
+
}
|
|
502
|
+
// Mirrors the assumption table injected from content-metrics (state.est).
|
|
503
|
+
const EST = state.est ?? { narrowTokPerChar: [0.22, 0.3], wideTokPerChar: [0.5, 1.0], imageTok: [700, 1600] };
|
|
504
|
+
function estFromMix(m) {
|
|
505
|
+
const [aLo, aHi] = EST.narrowTokPerChar, [cLo, cHi] = EST.wideTokPerChar, [iLo, iHi] = EST.imageTok;
|
|
506
|
+
const mid = (lo, hi) => (lo + hi) / 2;
|
|
507
|
+
return {
|
|
508
|
+
point: Math.round(m.narrow * mid(aLo, aHi) + m.wide * mid(cLo, cHi) + m.images * mid(iLo, iHi)),
|
|
509
|
+
low: Math.round(m.narrow * aLo + m.wide * cLo + m.images * iLo),
|
|
510
|
+
high: Math.round(m.narrow * aHi + m.wide * cHi + m.images * iHi),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function mixOf(atoms) {
|
|
514
|
+
let narrow = 0, wide = 0, images = 0;
|
|
515
|
+
for (const a of atoms) { narrow += a.narrowChars || 0; wide += a.wideChars || 0; images += a.imageCount || 0; }
|
|
516
|
+
return { narrow, wide, images };
|
|
517
|
+
}
|
|
518
|
+
function replacementFieldsMix(summary, topic) {
|
|
519
|
+
return classify((topic ? `: ${topic}` : '') + summary);
|
|
520
|
+
}
|
|
521
|
+
function replacementMixOf(r) {
|
|
522
|
+
const savedFields = replacementFieldsMix(r.summary, r.topic);
|
|
523
|
+
const current = draftOf(r);
|
|
524
|
+
const currentFields = replacementFieldsMix(current.summary, current.topic);
|
|
525
|
+
return {
|
|
526
|
+
narrow: Math.max(0, (r.replacementNarrowChars ?? savedFields.narrowChars)
|
|
527
|
+
- savedFields.narrowChars + currentFields.narrowChars),
|
|
528
|
+
wide: Math.max(0, (r.replacementWideChars ?? savedFields.wideChars)
|
|
529
|
+
- savedFields.wideChars + currentFields.wideChars),
|
|
530
|
+
images: 0,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function replacementCharsOf(r) {
|
|
534
|
+
const mix = replacementMixOf(r);
|
|
535
|
+
return mix.narrow + mix.wide;
|
|
536
|
+
}
|
|
537
|
+
function saveOf(r) {
|
|
538
|
+
return Math.max(0, (r.originalContentChars ?? 0) - replacementCharsOf(r));
|
|
539
|
+
}
|
|
540
|
+
|
|
446
541
|
function atomText(a) {
|
|
447
542
|
return [a.ref, a.kind, a.preview, ...(a.toolNames ?? []), ...(a.roles ?? [])].join(' ').toLowerCase();
|
|
448
543
|
}
|
|
@@ -458,6 +553,74 @@ function isDirty(r) {
|
|
|
458
553
|
return !!e && (e.topic !== (r.topic ?? '') || e.summary !== r.summary);
|
|
459
554
|
}
|
|
460
555
|
const dirtyIds = () => state.draft.ranges.filter(isDirty).map(r => r.id);
|
|
556
|
+
const pendingIds = () => state.draft.ranges.filter(r => !r.summary.trim()).map(r => r.id);
|
|
557
|
+
const selectionAtoms = () => state.atoms.filter(a => selectionRefs.has(a.ref));
|
|
558
|
+
const totalContentChars = () => state.atoms.reduce((s, a) => s + (a.contentChars ?? 0), 0);
|
|
559
|
+
const maxContentChars = () => state.atoms.reduce((m, a) => Math.max(m, a.contentChars ?? 0), 0);
|
|
560
|
+
|
|
561
|
+
function computeSpans() {
|
|
562
|
+
const spans = [];
|
|
563
|
+
state.atoms.forEach((a, index) => {
|
|
564
|
+
if (!selectionRefs.has(a.ref)) return;
|
|
565
|
+
const last = spans.at(-1);
|
|
566
|
+
if (last && last.endIndex + 1 === index) { last.endRef = a.ref; last.endIndex = index; }
|
|
567
|
+
else spans.push({ startRef: a.ref, endRef: a.ref, endIndex: index });
|
|
568
|
+
});
|
|
569
|
+
return spans;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Post-commit projection of the anchor usage. Band propagated from the
|
|
573
|
+
// assumption table by content composition; display-only, never gates anything.
|
|
574
|
+
function projection() {
|
|
575
|
+
const usage = state.telemetry && state.telemetry.anchorUsage;
|
|
576
|
+
if (!usage || usage.tokens == null || !usage.contextWindow) return null;
|
|
577
|
+
const now = usage.tokens, win = usage.contextWindow;
|
|
578
|
+
const removed = estFromMix(mixOf(isSelection() ? selectionAtoms() : state.atoms.filter(a => a.owningRangeId)));
|
|
579
|
+
const addedMix = state.draft.ranges.reduce((total, range) => {
|
|
580
|
+
const mix = replacementMixOf(range);
|
|
581
|
+
total.narrow += mix.narrow;
|
|
582
|
+
total.wide += mix.wide;
|
|
583
|
+
return total;
|
|
584
|
+
}, { narrow: 0, wide: 0, images: 0 });
|
|
585
|
+
const added = isSelection() ? { point: 0, low: 0, high: 0 } : estFromMix(addedMix);
|
|
586
|
+
const post = {
|
|
587
|
+
point: Math.max(0, now - removed.point + added.point),
|
|
588
|
+
low: Math.max(0, now - removed.high + added.low),
|
|
589
|
+
high: Math.max(0, now - removed.low + added.high),
|
|
590
|
+
};
|
|
591
|
+
const spreadPct = post.point > 0
|
|
592
|
+
? Math.round(Math.max((post.point - post.low) / post.point, (post.high - post.point) / post.point) * 100)
|
|
593
|
+
: 0;
|
|
594
|
+
return {
|
|
595
|
+
now, win,
|
|
596
|
+
nowPct: clamp(now / win * 100),
|
|
597
|
+
postPoint: post.point, postPct: clamp(post.point / win * 100),
|
|
598
|
+
keptPct: clamp(post.point / win * 100),
|
|
599
|
+
freePct: clamp((now - post.point) / win * 100),
|
|
600
|
+
saveLow: Math.max(0, now - post.high),
|
|
601
|
+
saveHigh: Math.max(0, now - post.low),
|
|
602
|
+
spreadPct,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const EST_TIP = `Estimate only, never a gate: assumption table ≈${EST.narrowTokPerChar[0]}–${EST.narrowTokPerChar[1]} tok per ASCII char, `
|
|
606
|
+
+ `≈${EST.wideTokPerChar[0]}–${EST.wideTokPerChar[1]} tok per non-ASCII char, ≈${EST.imageTok[0]}–${EST.imageTok[1]} tok per image. `
|
|
607
|
+
+ 'The provider\u2019s report is the final authority.';
|
|
608
|
+
|
|
609
|
+
function pbarHtml(proj, label, interactive, caveat = '') {
|
|
610
|
+
const tip = caveat + (interactive ? 'Click to preview the post-commit context (toggles Raw ⇄ Projected). ' : '') + EST_TIP;
|
|
611
|
+
const attrs = interactive ? ' data-toggle-mode role="button" tabindex="0"' : '';
|
|
612
|
+
if (!proj) {
|
|
613
|
+
return `<div class="pbar"${attrs}><div class="pl"><span>anchor usage</span></div><div class="tr"><span class="na">Pi-reported usage unavailable — the projection needs it</span></div></div>`;
|
|
614
|
+
}
|
|
615
|
+
return `<div class="pbar"${attrs} title="${esc(tip)}">
|
|
616
|
+
<div class="pl">
|
|
617
|
+
<span>now <b>${fmt(proj.now)} tok</b> · ${pct(proj.nowPct)} (reported by Pi)</span>
|
|
618
|
+
<span>${esc(label)} <b>≈${fmt(proj.postPoint)} tok · ${pct(proj.postPct)}</b>
|
|
619
|
+
<span class="delta">−${fmt(proj.saveHigh)} … −${fmt(proj.saveLow)} tok (est. ±${proj.spreadPct}%)</span></span>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="tr"><i class="kept" style="width:${proj.keptPct}%"></i><i class="free" style="left:${proj.keptPct}%;width:${proj.freePct}%"></i></div>
|
|
622
|
+
</div>`;
|
|
623
|
+
}
|
|
461
624
|
|
|
462
625
|
/* ---------- render ---------- */
|
|
463
626
|
function render() {
|
|
@@ -467,16 +630,20 @@ function render() {
|
|
|
467
630
|
const dirtyCount = d.length + (selectionDirty ? 1 : 0);
|
|
468
631
|
$('dirty-badge').hidden = dirtyCount === 0;
|
|
469
632
|
$('dirty-badge').textContent = `${dirtyCount} unsaved`;
|
|
633
|
+
const saved = $('saved-badge');
|
|
634
|
+
if (isSelection() && !selectionDirty && state.draft.ranges.length > 0) {
|
|
635
|
+
saved.hidden = false;
|
|
636
|
+
saved.textContent = `Saved · v${state.draft.revision}`;
|
|
637
|
+
} else saved.hidden = true;
|
|
470
638
|
$('mode-title').textContent = isSelection() ? 'Selection' : 'Review';
|
|
471
639
|
document.title = `Midcompact ${isSelection() ? 'Selection' : 'Review'}`;
|
|
472
|
-
document.querySelector('.seg').hidden = isSelection();
|
|
473
|
-
if (isSelection()) {
|
|
474
|
-
$('help-list').innerHTML = '<dt><kbd>/</kbd></dt><dd>Focus filter</dd><dt><kbd>Click</kbd></dt><dd>Add atom or KEEP</dd><dt><kbd>Save</kbd></dt><dd>Persist DraftPlan</dd><dt><kbd>Esc</kbd></dt><dd>Close dialog</dd>';
|
|
475
|
-
}
|
|
476
640
|
$('save-selection').hidden = !isSelection();
|
|
477
641
|
$('save-all').hidden = isSelection() || d.length < 2;
|
|
478
642
|
$('save-all').textContent = `Save all (${d.length})`;
|
|
479
|
-
|
|
643
|
+
$('policy-seg').hidden = isSelection();
|
|
644
|
+
$('mode-seg').hidden = isSelection();
|
|
645
|
+
renderHelp();
|
|
646
|
+
renderStrip();
|
|
480
647
|
renderRanges();
|
|
481
648
|
renderTimeline();
|
|
482
649
|
renderEditor();
|
|
@@ -486,73 +653,60 @@ function render() {
|
|
|
486
653
|
document.querySelector(`[data-range="${cssEsc(selectedId)}"]`)?.scrollIntoView({ block: 'nearest' });
|
|
487
654
|
}
|
|
488
655
|
}
|
|
489
|
-
const cssEsc = s => (window.CSS && CSS.escape) ? CSS.escape(s) : String(s).replace(/["\\]/g, '\\$&');
|
|
490
|
-
|
|
491
|
-
function renderMetrics() {
|
|
492
|
-
const t = state.telemetry ?? {};
|
|
493
|
-
const usage = t.anchorUsage;
|
|
494
|
-
const usageLabel = usage?.tokens == null ? 'unavailable' : `${fmt(usage.tokens)} / ${fmt(usage.contextWindow)} (${pct(usage.percent)})`;
|
|
495
|
-
const selected = isSelection()
|
|
496
|
-
? state.atoms.filter(a => selectionRefs.has(a.ref) && a.compressible && !keepRefs.has(a.ref))
|
|
497
|
-
: state.atoms.filter(a => a.owningRangeId);
|
|
498
|
-
const chars = selected.reduce((sum, a) => sum + (a.contentChars ?? 0), 0);
|
|
499
|
-
const totalChars = state.atoms.reduce((sum, a) => sum + (a.contentChars ?? 0), 0);
|
|
500
|
-
const selectedShare = totalChars > 0 ? clamp((chars / totalChars) * 100) : 0;
|
|
501
|
-
const images = selected.reduce((sum, a) => sum + (a.imageCount ?? 0), 0);
|
|
502
|
-
const pending = state.draft.ranges.filter(r => !r.summary.trim()).length;
|
|
503
|
-
const usagePercent = usage?.percent == null ? 0 : clamp(usage.percent);
|
|
504
|
-
const contentMetric = isSelection()
|
|
505
|
-
? `<div class="metric"><label>Selected content chars</label><div class="val">${fmt(chars)} / ${fmt(totalChars)} <small>${pct(selectedShare)} of anchor</small></div><div class="track"><i class="proj" style="width:${selectedShare}%"></i></div><div class="legend"><span>up to <b>${pct(selectedShare)}</b> fewer anchor chars · actual is lower after summaries</span></div></div>`
|
|
506
|
-
: `<div class="metric"><label>Content chars</label><div class="val">${fmt(chars)}</div><div class="legend"><span>${selected.length} selected atoms</span></div></div>`;
|
|
507
|
-
$('metrics').innerHTML = `
|
|
508
|
-
<div class="metric"><label>Pi reported anchor</label><div class="val">${usageLabel}</div><div class="track"><i class="anchor" style="width:${usagePercent}%"></i></div><div class="legend"><span>provider usage fact · no local token estimate</span></div></div>
|
|
509
|
-
<div class="metric"><label>Draft ranges</label><div class="val">${state.draft.ranges.length} <small>${pending ? `${pending} pending` : 'ready for review'}</small></div><div class="legend"><span>revision <b>${state.draft.revision}</b></span></div></div>
|
|
510
|
-
${contentMetric}
|
|
511
|
-
<div class="metric"><label>Images</label><div class="val">${images}</div><div class="legend"><span>payload facts only</span></div></div>`;
|
|
512
|
-
}
|
|
513
656
|
|
|
514
|
-
function
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
657
|
+
function renderStrip() {
|
|
658
|
+
const proj = projection();
|
|
659
|
+
let items;
|
|
660
|
+
if (isSelection()) {
|
|
661
|
+
const sel = selectionAtoms();
|
|
662
|
+
const chars = sel.reduce((s, a) => s + (a.contentChars ?? 0), 0);
|
|
663
|
+
const total = totalContentChars();
|
|
664
|
+
const share = total > 0 ? clamp(chars / total * 100) : 0;
|
|
665
|
+
items = `
|
|
666
|
+
<div class="di"><label>selected</label><span class="val">${sel.length} atoms</span></div>
|
|
667
|
+
<div class="di"><label>anchor content</label><span class="val save">−${fmt(chars)} chars (${pct(share)})</span></div>
|
|
668
|
+
<div class="di"><label>will become</label><span class="val">${computeSpans().length} range${computeSpans().length === 1 ? '' : 's'}</span></div>`;
|
|
669
|
+
} else {
|
|
670
|
+
const chars = state.atoms.filter(a => a.owningRangeId).reduce((s, a) => s + (a.contentChars ?? 0), 0);
|
|
671
|
+
const total = totalContentChars();
|
|
672
|
+
const save = state.draft.ranges.reduce((s, r) => s + saveOf(r), 0);
|
|
673
|
+
const share = total > 0 ? clamp(save / total * 100) : 0;
|
|
674
|
+
const coverage = state.draft.ranges.reduce((s, r) => s + r.atomCount, 0);
|
|
675
|
+
const pending = pendingIds().length;
|
|
676
|
+
items = `
|
|
677
|
+
<div class="di"><label>draft</label><span class="val">${state.draft.ranges.length} range${state.draft.ranges.length === 1 ? '' : 's'}</span></div>
|
|
678
|
+
<div class="di"><label>coverage</label><span class="val">${coverage} / ${state.atoms.length} atoms</span></div>
|
|
679
|
+
<div class="di"><label>anchor content</label><span class="val save">−${fmt(save)} chars (−${pct(share)})</span></div>
|
|
680
|
+
<div class="di"><label>pending</label><span class="val ${pending ? 'pend' : ''}">${pending}</span></div>`;
|
|
524
681
|
}
|
|
525
|
-
|
|
682
|
+
const caveat = isSelection() ? 'Selected content removal only; future summaries are not included. ' : '';
|
|
683
|
+
$('strip').innerHTML = items + pbarHtml(proj, isSelection() ? 'if committed now' : 'after commit', !isSelection(), caveat);
|
|
526
684
|
}
|
|
527
685
|
|
|
528
686
|
function renderRanges() {
|
|
529
687
|
const list = $('range-list');
|
|
530
688
|
if (isSelection()) {
|
|
531
|
-
const
|
|
532
|
-
if (!
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
<div class="
|
|
541
|
-
<div class="rsum">Pending summary</div>
|
|
542
|
-
<div class="meta">${range.atoms.length} atoms · ${fmt(chars)} chars · ${images} images</div>
|
|
689
|
+
const spans = computeSpans();
|
|
690
|
+
if (!state.atoms.length) { list.innerHTML = '<div class="empty">No atoms in anchor snapshot.</div>'; return; }
|
|
691
|
+
if (!spans.length) { list.innerHTML = '<div class="empty">Nothing selected.<br>Click atoms or groups in the timeline, or use <kbd>↑↓</kbd> + <kbd class="space">Space</kbd>.</div>'; return; }
|
|
692
|
+
list.innerHTML = spans.map((span, index) => {
|
|
693
|
+
const atoms = state.atoms.slice(state.atoms.findIndex(a => a.ref === span.startRef), state.atoms.findIndex(a => a.ref === span.endRef) + 1);
|
|
694
|
+
const chars = atoms.reduce((s, a) => s + (a.contentChars ?? 0), 0);
|
|
695
|
+
return `<div class="ritem">
|
|
696
|
+
<div class="r1"><span class="rid">range ${index + 1}</span><span class="refs">${esc(span.startRef)}→${esc(span.endRef)}</span>
|
|
697
|
+
<button class="btn btn-sm btn-icon" data-unsel-start="${esc(span.startRef)}" data-unsel-end="${esc(span.endRef)}" title="Unselect this span" aria-label="Unselect this span">✕</button></div>
|
|
698
|
+
<div class="meta">${atoms.length} atoms · ${fmt(chars)} chars</div>
|
|
543
699
|
</div>`;
|
|
544
700
|
}).join('');
|
|
545
701
|
return;
|
|
546
702
|
}
|
|
703
|
+
if (!state.draft.ranges.length) { list.innerHTML = '<div class="empty">No compression ranges in this draft.</div>'; return; }
|
|
547
704
|
const rs = state.draft.ranges.filter(r => !query || rangeText(r).includes(query));
|
|
548
|
-
if (!state.draft.ranges.length) {
|
|
549
|
-
list.innerHTML = '<div class="empty">No compression ranges in this draft.</div>';
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
705
|
if (!rs.length) { list.innerHTML = '<div class="empty">No range matches the filter.</div>'; return; }
|
|
553
706
|
list.innerHTML = rs.map(r => {
|
|
554
707
|
const d = draftOf(r);
|
|
555
|
-
const
|
|
708
|
+
const replacementChars = replacementCharsOf(r);
|
|
709
|
+
const ratio = r.originalContentChars ? Math.round((1 - replacementChars / r.originalContentChars) * 100) : 0;
|
|
556
710
|
return `<div class="ritem" role="option" tabindex="0" data-range="${esc(r.id)}" aria-selected="${r.id === selectedId}">
|
|
557
711
|
<div class="r1">
|
|
558
712
|
<span class="rid">${hl(r.id, query)}</span>
|
|
@@ -560,9 +714,12 @@ function renderRanges() {
|
|
|
560
714
|
${isDirty(r) ? '<span class="udot" title="unsaved edits"></span>' : ''}
|
|
561
715
|
<span class="save">−${fmt(saveOf(r))}</span>
|
|
562
716
|
</div>
|
|
563
|
-
${d.topic ? `<div class="
|
|
717
|
+
${d.topic ? `<div class="topic">${hl(d.topic, query)}</div>` : ''}
|
|
564
718
|
<div class="rsum">${hl(d.summary, query)}</div>
|
|
565
|
-
<div class="meta">${r.atomCount} atoms · ${fmt(r.originalContentChars)}→${fmt(
|
|
719
|
+
<div class="meta">${r.atomCount} atoms · ${fmt(r.originalContentChars)}→${fmt(replacementChars)} chars · ${ratio}% fewer
|
|
720
|
+
${draftOf(r).summary.trim() ? '' : '<span class="pend">pending</span>'}
|
|
721
|
+
${reviewed.has(r.id) ? '<span class="rev">✓ reviewed</span>' : ''}
|
|
722
|
+
</div>
|
|
566
723
|
</div>`;
|
|
567
724
|
}).join('');
|
|
568
725
|
}
|
|
@@ -584,61 +741,89 @@ function buildGroups() {
|
|
|
584
741
|
function renderTimeline() {
|
|
585
742
|
const tl = $('timeline');
|
|
586
743
|
if (!state.atoms.length) { tl.innerHTML = '<div class="empty">No atoms in anchor snapshot.</div>'; $('tl-stat').textContent = ''; return; }
|
|
744
|
+
if (isSelection()) { renderSelectionTimeline(tl); return; }
|
|
587
745
|
|
|
588
746
|
let shown = 0;
|
|
747
|
+
const maxChars = maxContentChars();
|
|
748
|
+
const keepGroupMatch = g => {
|
|
749
|
+
const label = `${g.atoms[0]?.ref ?? ''} → ${g.atoms.at(-1)?.ref ?? ''} verbatim ${g.label ?? ''}`.toLowerCase();
|
|
750
|
+
return label.includes(query) || g.atoms.some(a => atomText(a).includes(query));
|
|
751
|
+
};
|
|
589
752
|
const html = buildGroups().map(g => {
|
|
590
|
-
if (!isSelection() && policy === 'range' && !g.key) return '';
|
|
591
|
-
if (!isSelection() && policy === 'keep' && g.key) return '';
|
|
592
|
-
const r = !isSelection() && g.key ? rangeById(g.key) : null;
|
|
593
753
|
const gkey = g.key ?? ('keep-' + g.idx);
|
|
754
|
+
if (mode === 'proj') {
|
|
755
|
+
// SPEC §3.2: Raw and Projected share the same policy + query semantics.
|
|
756
|
+
if (!g.key) {
|
|
757
|
+
if (policy === 'range') return '';
|
|
758
|
+
if (query && !keepGroupMatch(g)) return '';
|
|
759
|
+
return keepGroupHtml(g, maxChars);
|
|
760
|
+
}
|
|
761
|
+
if (policy === 'keep') return '';
|
|
762
|
+
const r0 = rangeById(g.key);
|
|
763
|
+
if (!r0) return keepGroupHtml(g, maxChars);
|
|
764
|
+
if (query && !rangeText(r0).includes(query) && !g.atoms.some(a => atomText(a).includes(query))) return '';
|
|
765
|
+
return projectedCardHtml(r0, g);
|
|
766
|
+
}
|
|
767
|
+
if (policy === 'range' && !g.key) return '';
|
|
768
|
+
if (policy === 'keep' && g.key) return '';
|
|
769
|
+
const r = g.key ? rangeById(g.key) : null;
|
|
594
770
|
const groupMatch = r ? rangeText(r).includes(query) : `${g.label ?? ''} ${gkey}`.toLowerCase().includes(query);
|
|
595
771
|
const atoms = query && !groupMatch ? g.atoms.filter(a => atomText(a).includes(query)) : g.atoms;
|
|
596
772
|
if (query && !groupMatch && !atoms.length) return '';
|
|
597
773
|
shown += atoms.length;
|
|
598
|
-
|
|
599
|
-
const chars = g.atoms.reduce((sum, atom) => sum + (atom.contentChars ?? 0), 0);
|
|
600
|
-
const images = g.atoms.reduce((sum, atom) => sum + (atom.imageCount ?? 0), 0);
|
|
601
774
|
const open = !collapsed.has(gkey) || (query && !groupMatch);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (isSelection()) {
|
|
605
|
-
const allSelected = g.atoms.every(atom => selectionRefs.has(atom.ref));
|
|
606
|
-
return `<section class="group ${open ? 'open' : ''} ${allSelected ? 'sel' : ''}" data-group-key="${esc(gkey)}">
|
|
607
|
-
<div class="ghead" aria-expanded="${open}">
|
|
608
|
-
<button class="btn btn-sm btn-icon" data-toggle="${esc(gkey)}" title="Expand group">▶</button>
|
|
609
|
-
<span class="gid">${esc(gkey)}</span>
|
|
610
|
-
<span class="gsum">${esc(g.label ?? '')}</span>
|
|
611
|
-
<span class="gstat">${g.atoms.length} atoms · ${fmt(chars)} chars · ${images} img <button class="btn btn-sm" data-select-group="${esc(gkey)}">${allSelected ? 'Clear group' : 'Add group'}</button></span>
|
|
612
|
-
</div>
|
|
613
|
-
<div class="gbody">${body}</div>
|
|
614
|
-
</section>`;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (!r) {
|
|
618
|
-
return `<section class="group keepg ${open ? 'open' : ''}" data-group-key="${esc(gkey)}">
|
|
619
|
-
<button class="ghead" data-toggle="${esc(gkey)}" aria-expanded="${open}">
|
|
620
|
-
<span class="chev">▶</span>
|
|
621
|
-
<span class="gid">KEEP</span>
|
|
622
|
-
<span class="gsum">${esc(g.atoms[0].ref)} → ${esc(g.atoms[g.atoms.length - 1].ref)} verbatim</span>
|
|
623
|
-
<span class="gstat">${g.atoms.length} atoms · ${fmt(chars)} chars · ${images} img</span>
|
|
624
|
-
</button>
|
|
625
|
-
<div class="gbody">${body}</div>
|
|
626
|
-
</section>`;
|
|
627
|
-
}
|
|
775
|
+
if (!r) return keepGroupHtml(g, maxChars, open, atoms);
|
|
628
776
|
const d = draftOf(r);
|
|
629
|
-
return `<section class="group ${open ? 'open' : ''} ${r.id === selectedId ? 'sel' : ''}" data-group-key="${esc(
|
|
630
|
-
<button class="ghead" data-toggle="${esc(
|
|
777
|
+
return `<section class="group ${open ? 'open' : ''} ${r.id === selectedId ? 'sel' : ''}" data-group-key="${esc(g.key)}">
|
|
778
|
+
<button class="ghead" data-toggle="${esc(g.key)}" data-select="${esc(r.id)}" aria-expanded="${open}">
|
|
631
779
|
<span class="chev">▶</span>
|
|
632
780
|
<span class="gid">${esc(r.id)}</span>
|
|
633
781
|
<span class="gsum">${hl(d.topic || d.summary, query)}</span>
|
|
634
|
-
<span class="gstat">${r.atomCount} atoms · ${fmt(r.originalContentChars)}→${fmt(r
|
|
782
|
+
<span class="gstat">${r.atomCount} atoms · ${fmt(r.originalContentChars)}→${fmt(replacementCharsOf(r))} <em>−${fmt(saveOf(r))}</em>
|
|
783
|
+
${draftOf(r).summary.trim() ? '' : '<span class="pend">pending</span>'}${reviewed.has(r.id) ? '<span class="rev">✓</span>' : ''}</span>
|
|
635
784
|
</button>
|
|
636
|
-
<div class="gbody">${
|
|
785
|
+
<div class="gbody" ${open ? '' : 'hidden'}>${atoms.map(a => atomRowRaw(a, maxChars)).join('')}</div>
|
|
637
786
|
</section>`;
|
|
638
787
|
}).join('');
|
|
639
|
-
|
|
640
788
|
tl.innerHTML = html || '<div class="empty">Nothing matches the current filter.</div>';
|
|
641
|
-
$('tl-stat').textContent =
|
|
789
|
+
$('tl-stat').textContent = mode === 'proj'
|
|
790
|
+
? `${state.atoms.length} atoms · projected`
|
|
791
|
+
: (query || policy !== 'all' ? `${shown}/${state.atoms.length} atoms` : `${state.atoms.length} atoms`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function keepGroupHtml(g, maxChars, open = !collapsed.has('keep-' + g.idx), atoms = g.atoms) {
|
|
795
|
+
const gkey = 'keep-' + g.idx;
|
|
796
|
+
const chars = g.atoms.reduce((s, a) => s + (a.contentChars ?? 0), 0);
|
|
797
|
+
return `<section class="group keepg ${open ? 'open' : ''}" data-group-key="${esc(gkey)}">
|
|
798
|
+
<button class="ghead" data-toggle="${esc(gkey)}" aria-expanded="${open}">
|
|
799
|
+
<span class="chev">▶</span>
|
|
800
|
+
<span class="gid">KEEP</span>
|
|
801
|
+
<span class="gsum">${esc(g.atoms[0].ref)} → ${esc(g.atoms.at(-1).ref)} verbatim</span>
|
|
802
|
+
<span class="gstat">${g.atoms.length} atoms · ${fmt(chars)} chars · verbatim</span>
|
|
803
|
+
</button>
|
|
804
|
+
<div class="gbody" ${open ? '' : 'hidden'}>${atoms.map(a => atomRowRaw(a, maxChars)).join('')}</div>
|
|
805
|
+
</section>`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function projectedCardHtml(r, g) {
|
|
809
|
+
const d = draftOf(r);
|
|
810
|
+
const replacementChars = replacementCharsOf(r);
|
|
811
|
+
if (!d.summary.trim()) {
|
|
812
|
+
return `<div class="pcard pend" data-group-key="${esc(r.id)}">
|
|
813
|
+
<div class="phead"><span class="pid">${esc(r.id)}</span><span class="ptopic warn">pending summary</span>
|
|
814
|
+
<span class="pmeta">${r.atomCount} atoms → 0 chars</span></div>
|
|
815
|
+
<div class="pwarnline">⚠ No summary yet — <span class="mono">/midcompact:commit</span> will refuse this range.</div>
|
|
816
|
+
<div class="pactions"><button class="btn btn-sm" data-fix="${esc(r.id)}">Write summary</button>
|
|
817
|
+
<button class="btn btn-sm" data-orig="${esc(g.atoms[0].ref)}">View raw start</button></div>
|
|
818
|
+
</div>`;
|
|
819
|
+
}
|
|
820
|
+
return `<div class="pcard" data-group-key="${esc(r.id)}">
|
|
821
|
+
<div class="phead"><span class="pid">${esc(r.id)}</span><span class="ptopic">${hl(d.topic, query)}</span>
|
|
822
|
+
<span class="pmeta">${r.atomCount} atoms → ${fmt(replacementChars)} chars · replaces ${esc(r.startRef)}→${esc(r.endRef)}</span></div>
|
|
823
|
+
<div class="ptext">${hl(d.summary, query)}</div>
|
|
824
|
+
<div class="pactions"><button class="btn btn-sm" data-orig="${esc(g.atoms[0].ref)}">View original start</button>
|
|
825
|
+
<button class="btn btn-sm" data-edit="${esc(r.id)}">Edit summary</button></div>
|
|
826
|
+
</div>`;
|
|
642
827
|
}
|
|
643
828
|
|
|
644
829
|
function toolTag(name) {
|
|
@@ -646,49 +831,101 @@ function toolTag(name) {
|
|
|
646
831
|
return `<span class="tag tool${tone}">${esc(name)}</span>`;
|
|
647
832
|
}
|
|
648
833
|
|
|
649
|
-
function
|
|
650
|
-
const
|
|
834
|
+
function atomRowRaw(a, maxChars) {
|
|
835
|
+
const w = maxChars ? Math.round((a.contentChars ?? 0) / maxChars * 100) : 0;
|
|
836
|
+
const lockTags = [
|
|
651
837
|
...(a.toolNames ?? []).slice(0, 3).map(toolTag),
|
|
652
838
|
...(a.roles ?? []).slice(0, 2).map(t => `<span class="tag">${esc(t)}</span>`),
|
|
653
|
-
a
|
|
839
|
+
isLocked(a) ? '<span class="tag lock">can\u2019t compress</span>' : '',
|
|
654
840
|
a.protocolClosed === false ? '<span class="tag lock">open protocol</span>' : '',
|
|
655
841
|
a.compressedBlockId ? `<span class="tag">blk ${esc(a.compressedBlockId)}</span>` : '',
|
|
656
842
|
].filter(Boolean).join('');
|
|
657
|
-
|
|
658
|
-
const selected = selectionRefs.has(a.ref);
|
|
659
|
-
const kept = keepRefs.has(a.ref) || !a.compressible;
|
|
660
|
-
const actions = isSelection() ? `<div class="atom-actions">
|
|
661
|
-
<button class="btn btn-sm" data-toggle-atom="${esc(a.ref)}" ${!a.compressible ? 'disabled' : ''}>${selected ? 'Remove' : 'Add'}</button>
|
|
662
|
-
<button class="btn btn-sm" data-keep-atom="${esc(a.ref)}" ${!a.compressible ? 'disabled' : ''}>${kept ? 'KEEP' : 'Keep'}</button>
|
|
663
|
-
</div>` : '';
|
|
664
|
-
return `<div class="atom ${open ? 'open' : ''} ${selected ? 'selected-atom' : ''} ${kept ? 'kept-atom' : ''}" data-atom="${esc(a.ref)}">
|
|
843
|
+
return `<div class="atom ${a.kind === 'user' ? 'user' : ''}" data-atom="${esc(a.ref)}">
|
|
665
844
|
<span class="aref">${hl(a.ref, query)}</span>
|
|
666
|
-
<span class="akind">${hl(a.kind ?? '', query)}</span>
|
|
667
|
-
<span class="atok">${fmt(a.contentChars)} chars<br>${a.imageCount ?? 0} img</span>
|
|
668
845
|
<div>
|
|
669
|
-
<div class="
|
|
670
|
-
|
|
671
|
-
|
|
846
|
+
<div class="arow">
|
|
847
|
+
<span class="tag kind-${esc(a.kind)}">${esc(a.kind)}</span>
|
|
848
|
+
<button type="button" class="abody" data-orig="${esc(a.ref)}" title="View full original">${hl(a.preview ?? '', query)}</button>
|
|
849
|
+
</div>
|
|
850
|
+
${lockTags ? `<div class="tags">${lockTags}</div>` : ''}
|
|
672
851
|
</div>
|
|
852
|
+
<span class="atok">${fmt(a.contentChars)} chars<i class="sz" style="--w:${w}%"></i></span>
|
|
673
853
|
</div>`;
|
|
674
854
|
}
|
|
675
855
|
|
|
856
|
+
function selectionGroupMatches(g) {
|
|
857
|
+
return !query || (g.label ?? '').toLowerCase().includes(query)
|
|
858
|
+
|| g.atoms.some(a => atomText(a).includes(query));
|
|
859
|
+
}
|
|
860
|
+
function visibleSelectionIndexes() {
|
|
861
|
+
const visibleRefs = new Set(buildGroups().filter(selectionGroupMatches).flatMap(g => g.atoms.map(a => a.ref)));
|
|
862
|
+
return state.atoms.flatMap((atom, index) => visibleRefs.has(atom.ref) ? [index] : []);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function renderSelectionTimeline(tl) {
|
|
866
|
+
const maxChars = maxContentChars();
|
|
867
|
+
const visibleIndexes = visibleSelectionIndexes();
|
|
868
|
+
if (!visibleIndexes.includes(cursor)) cursor = visibleIndexes[0] ?? 0;
|
|
869
|
+
let idx = -1;
|
|
870
|
+
let shown = 0;
|
|
871
|
+
const html = buildGroups().map(g => {
|
|
872
|
+
if (!selectionGroupMatches(g)) { idx += g.atoms.length; return ''; }
|
|
873
|
+
shown += g.atoms.length;
|
|
874
|
+
const free = g.atoms.filter(a => !isLocked(a));
|
|
875
|
+
const selCount = free.filter(a => selectionRefs.has(a.ref)).length;
|
|
876
|
+
const rows = g.atoms.map(a => {
|
|
877
|
+
idx += 1;
|
|
878
|
+
const sel = selectionRefs.has(a.ref);
|
|
879
|
+
const locked = isLocked(a);
|
|
880
|
+
const w = maxChars ? Math.round((a.contentChars ?? 0) / maxChars * 100) : 0;
|
|
881
|
+
return `<div class="atom srow ${a.kind === 'user' ? 'user' : ''} ${sel ? 'selected-atom' : ''} ${locked ? 'locked' : ''} ${idx === cursor ? 'cursor-atom' : ''}"
|
|
882
|
+
data-idx="${idx}" data-atom="${esc(a.ref)}" ${locked ? 'aria-disabled="true"' : ''} tabindex="-1">
|
|
883
|
+
<span class="aref">${hl(a.ref, query)}</span>
|
|
884
|
+
<div>
|
|
885
|
+
<div class="arow">
|
|
886
|
+
<span class="tag kind-${esc(a.kind)}">${esc(a.kind)}</span>
|
|
887
|
+
<div class="abody">${hl(a.preview ?? '', query)}</div>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
<span class="mark ${sel ? 'sel' : locked ? 'lock' : ''}">${locked ? '🔒 can\u2019t compress' : sel ? '✓ compress' : ''}</span>
|
|
891
|
+
<span class="atok">${fmt(a.contentChars)} chars<i class="sz" style="--w:${w}%"></i></span>
|
|
892
|
+
</div>`;
|
|
893
|
+
}).join('');
|
|
894
|
+
return `<section class="group keepg ${selCount === free.length && free.length ? 'sel' : ''}" data-group-key="${esc(g.key)}">
|
|
895
|
+
<div class="ghead" style="cursor:default">
|
|
896
|
+
<span class="chev">·</span>
|
|
897
|
+
<span class="gid">${esc(g.key)}</span>
|
|
898
|
+
<span class="gsum">${esc(g.label ?? '')}</span>
|
|
899
|
+
<span class="gstat">${selCount}/${free.length} selected
|
|
900
|
+
<button class="btn btn-sm" data-group="${esc(g.key)}">${selCount === free.length && free.length ? 'Clear' : 'Add group'}</button></span>
|
|
901
|
+
</div>
|
|
902
|
+
<div class="gbody">${rows}</div>
|
|
903
|
+
</section>`;
|
|
904
|
+
}).join('');
|
|
905
|
+
tl.innerHTML = html || '<div class="empty">Nothing matches the current filter.</div>';
|
|
906
|
+
$('tl-stat').textContent = query ? `${shown}/${state.atoms.length} atoms` : `${state.atoms.length} atoms`;
|
|
907
|
+
}
|
|
908
|
+
|
|
676
909
|
function renderEditor() {
|
|
677
910
|
const panel = $('editor');
|
|
678
911
|
if (isSelection()) {
|
|
679
|
-
const
|
|
680
|
-
const chars =
|
|
681
|
-
const images =
|
|
912
|
+
const sel = selectionAtoms();
|
|
913
|
+
const chars = sel.reduce((s, a) => s + (a.contentChars ?? 0), 0);
|
|
914
|
+
const images = sel.reduce((s, a) => s + (a.imageCount ?? 0), 0);
|
|
682
915
|
panel.innerHTML = `<div class="epad">
|
|
683
|
-
<div class="etitle"><span class="eid">Selection</span><span class="erefs"
|
|
916
|
+
<div class="etitle"><span class="eid">Selection</span><span class="erefs">${computeSpans().length} range${computeSpans().length === 1 ? '' : 's'} pending</span></div>
|
|
684
917
|
<div class="estats">
|
|
685
|
-
<div class="estat"><label>atoms</label><span>${
|
|
918
|
+
<div class="estat"><label>atoms</label><span>${sel.length}</span></div>
|
|
686
919
|
<div class="estat"><label>chars</label><span>${fmt(chars)}</span></div>
|
|
687
920
|
<div class="estat"><label>images</label><span>${images}</span></div>
|
|
688
921
|
</div>
|
|
689
|
-
<div class="hint">
|
|
690
|
-
|
|
691
|
-
|
|
922
|
+
<div class="hint">Selected atoms are handed to the Agent for summarization; everything left unselected stays verbatim (that is KEEP).
|
|
923
|
+
Locked atoms can never be compressed: existing compressed blocks, incomplete tool protocol, or messages without a persistent anchor entry.
|
|
924
|
+
After saving, return to the terminal and tell the Agent to continue.</div>
|
|
925
|
+
<div class="erow" style="position:static;margin:0;padding:0;border:0">
|
|
926
|
+
<button class="btn btn-primary" id="selection-save-side" ${sel.length ? '' : 'disabled'}>Save selection</button>
|
|
927
|
+
<button class="btn" id="selection-clear" ${sel.length ? '' : 'disabled'}>Clear selection</button>
|
|
928
|
+
</div>
|
|
692
929
|
</div>`;
|
|
693
930
|
return;
|
|
694
931
|
}
|
|
@@ -701,36 +938,40 @@ function renderEditor() {
|
|
|
701
938
|
}
|
|
702
939
|
const d = draftOf(r);
|
|
703
940
|
const dirty = isDirty(r);
|
|
704
|
-
const
|
|
941
|
+
const ready = state.draft.ranges.length > 0 && pendingIds().length === 0 && dirtyIds().length === 0;
|
|
705
942
|
panel.innerHTML = `<div class="epad">
|
|
706
|
-
<div>
|
|
707
|
-
<
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
943
|
+
<div class="etitle">
|
|
944
|
+
<span class="eid">${esc(r.id)}</span>
|
|
945
|
+
<span class="erefs">${esc(r.startRef)} → ${esc(r.endRef)}</span>
|
|
946
|
+
<label class="checkline"><input type="checkbox" id="rev-check" ${reviewed.has(r.id) ? 'checked' : ''}> reviewed</label>
|
|
947
|
+
</div>
|
|
948
|
+
<div class="nextstep ${ready ? 'show' : ''}" id="nextstep">
|
|
949
|
+
<span>✅</span><span><b>Draft saved — nothing is applied yet.</b><br>
|
|
950
|
+
Next step in the terminal: <kbd>/midcompact:commit</kbd> applies this projection · <kbd>/midcompact:abort</kbd> discards it.</span>
|
|
951
|
+
</div>
|
|
952
|
+
<div class="estats">
|
|
953
|
+
<div class="estat"><label>atoms</label><span>${r.atomCount}</span></div>
|
|
954
|
+
<div class="estat"><label>chars</label><span>${fmt(r.originalContentChars)}→<span id="replacement-chars">${fmt(replacementCharsOf(r))}</span></span></div>
|
|
955
|
+
<div class="estat"><label>images</label><span>${r.originalImageCount ?? 0}</span></div>
|
|
716
956
|
</div>
|
|
717
957
|
<div class="field" id="f-topic">
|
|
718
958
|
<label for="topic">Topic <span class="count">optional</span></label>
|
|
719
959
|
<input id="topic" value="${esc(d.topic)}" placeholder="short label, e.g. auth refactor" autocomplete="off" spellcheck="false">
|
|
720
960
|
</div>
|
|
721
961
|
<div class="field ${d.summary.trim() ? '' : 'invalid'}" id="f-summary">
|
|
722
|
-
<label for="summary">Summary <span class="count" id="sum-count"></span></label>
|
|
723
|
-
<textarea id="summary" spellcheck="false">${esc(d.summary)}</textarea>
|
|
962
|
+
<label for="summary">Summary — the model will see this instead of ${r.atomCount} atoms <span class="count" id="sum-count"></span></label>
|
|
963
|
+
<textarea id="summary" spellcheck="false" placeholder="What must survive after these atoms are replaced?">${esc(d.summary)}</textarea>
|
|
724
964
|
</div>
|
|
725
965
|
<div class="erow">
|
|
726
|
-
<button class="btn btn-primary" id="save-btn" ${dirty ? '' : 'disabled'}>Save</button>
|
|
966
|
+
<button class="btn btn-primary" id="save-btn" ${dirty ? '' : 'disabled'}>Save ${esc(r.id)}</button>
|
|
727
967
|
<button class="btn" id="revert-btn" ${dirty ? '' : 'disabled'}>Revert</button>
|
|
728
968
|
<div class="spacer"></div>
|
|
729
969
|
<button class="btn btn-danger ${removeArmed === r.id ? 'armed' : ''}" id="remove-btn">
|
|
730
|
-
${removeArmed === r.id ? 'Confirm remove' : 'Remove
|
|
970
|
+
${removeArmed === r.id ? 'Confirm remove' : 'Remove ' + esc(r.id)}
|
|
731
971
|
</button>
|
|
732
972
|
</div>
|
|
733
|
-
<div class="hint">
|
|
973
|
+
<div class="hint">Saving edits the draft only — nothing is applied until you run <kbd>/midcompact:commit</kbd> in the terminal.
|
|
974
|
+
Removing a range keeps its atoms verbatim. Topic and summary are saved together (<kbd>⌘/Ctrl</kbd>+<kbd>S</kbd>).</div>
|
|
734
975
|
</div>`;
|
|
735
976
|
updateCount();
|
|
736
977
|
}
|
|
@@ -738,8 +979,25 @@ function renderEditor() {
|
|
|
738
979
|
function updateCount() {
|
|
739
980
|
const ta = $('summary');
|
|
740
981
|
if (!ta) return;
|
|
741
|
-
|
|
742
|
-
|
|
982
|
+
$('sum-count').textContent = `${[...ta.value].length} chars`;
|
|
983
|
+
const range = rangeById(selectedId);
|
|
984
|
+
if (range && $('replacement-chars')) $('replacement-chars').textContent = fmt(replacementCharsOf(range));
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function renderHelp() {
|
|
988
|
+
$('help-list').innerHTML = isSelection()
|
|
989
|
+
? '<dt><kbd>/</kbd></dt><dd>Focus filter</dd>'
|
|
990
|
+
+ '<dt><kbd>↑</kbd> <kbd>↓</kbd></dt><dd>Move atom cursor</dd>'
|
|
991
|
+
+ '<dt><kbd class="space">Space</kbd></dt><dd>Select / unselect atom</dd>'
|
|
992
|
+
+ '<dt><kbd>G</kbd></dt><dd>Select / clear whole group</dd>'
|
|
993
|
+
+ '<dt><kbd>Esc</kbd></dt><dd>Close dialog</dd>'
|
|
994
|
+
: '<dt><kbd>j</kbd> / <kbd>k</kbd></dt><dd>Next / previous range</dd>'
|
|
995
|
+
+ '<dt><kbd>/</kbd></dt><dd>Focus filter</dd>'
|
|
996
|
+
+ '<dt><kbd>e</kbd></dt><dd>Focus summary editor</dd>'
|
|
997
|
+
+ '<dt><kbd>⌘/Ctrl</kbd>+<kbd>S</kbd></dt><dd>Save current range</dd>'
|
|
998
|
+
+ '<dt><kbd>r</kbd></dt><dd>Reload state from server</dd>'
|
|
999
|
+
+ '<dt><kbd>x</kbd></dt><dd>Expand / collapse selected group</dd>'
|
|
1000
|
+
+ '<dt><kbd>Esc</kbd></dt><dd>Blur field / close dialog</dd>';
|
|
743
1001
|
}
|
|
744
1002
|
|
|
745
1003
|
/* ---------- state mutation ---------- */
|
|
@@ -750,6 +1008,17 @@ function select(id) {
|
|
|
750
1008
|
removeArmed = null;
|
|
751
1009
|
render();
|
|
752
1010
|
}
|
|
1011
|
+
function afterServerState() {
|
|
1012
|
+
selectionRefs = new Set(state.draft.ranges.flatMap(r => rangeAtoms(r).map(a => a.ref)));
|
|
1013
|
+
reviewed = new Set([...reviewed].filter(id => state.draft.ranges.some(r => r.id === id)));
|
|
1014
|
+
reviewedStoreSave();
|
|
1015
|
+
if (!rangeById(selectedId)) selectedId = state.draft.ranges[0]?.id ?? null;
|
|
1016
|
+
selectionDirty = false;
|
|
1017
|
+
lastScrolled = null;
|
|
1018
|
+
}
|
|
1019
|
+
function reviewedStoreSave() {
|
|
1020
|
+
try { sessionStorage.setItem('midcompact-reviewed', JSON.stringify([...reviewed])); } catch {}
|
|
1021
|
+
}
|
|
753
1022
|
|
|
754
1023
|
async function post(path, body) {
|
|
755
1024
|
const res = await fetch(path, {
|
|
@@ -764,53 +1033,52 @@ async function post(path, body) {
|
|
|
764
1033
|
}
|
|
765
1034
|
|
|
766
1035
|
async function saveSelection() {
|
|
767
|
-
|
|
768
|
-
const spans =
|
|
769
|
-
for (const index of indices) {
|
|
770
|
-
const last = spans.at(-1);
|
|
771
|
-
if (last && last.endIndex + 1 === index) {
|
|
772
|
-
last.endRef = state.atoms[index].ref;
|
|
773
|
-
last.endIndex = index;
|
|
774
|
-
} else spans.push({ startRef: state.atoms[index].ref, endRef: state.atoms[index].ref, endIndex: index });
|
|
775
|
-
}
|
|
1036
|
+
if (document.body.classList.contains('dead')) { toast('Session closed in the terminal', 'err'); return false; }
|
|
1037
|
+
const spans = computeSpans().map(({ startRef, endRef }) => ({ startRef, endRef }));
|
|
776
1038
|
try {
|
|
777
|
-
state = await post('/api/selection', { spans
|
|
778
|
-
|
|
779
|
-
selectedId = state.draft.ranges[0]?.id ?? null;
|
|
1039
|
+
state = await post('/api/selection', { spans, keepRefs: [] });
|
|
1040
|
+
afterServerState();
|
|
780
1041
|
render();
|
|
781
|
-
toast('
|
|
1042
|
+
toast('Selection saved — tell the Agent to continue when ready.', 'ok');
|
|
782
1043
|
return true;
|
|
783
1044
|
} catch (err) { toast(String(err.message ?? err), 'err'); return false; }
|
|
784
1045
|
}
|
|
785
1046
|
|
|
786
|
-
function
|
|
787
|
-
const
|
|
788
|
-
if (!
|
|
789
|
-
|
|
790
|
-
else selectionRefs.add(ref);
|
|
1047
|
+
function toggleAtom(ref) {
|
|
1048
|
+
const a = atomByRef(ref);
|
|
1049
|
+
if (!a || isLocked(a)) return;
|
|
1050
|
+
selectionRefs.has(ref) ? selectionRefs.delete(ref) : selectionRefs.add(ref);
|
|
791
1051
|
selectionDirty = true;
|
|
792
1052
|
render();
|
|
793
1053
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
selectionRefs.
|
|
799
|
-
|
|
1054
|
+
function toggleGroupAtoms(groupKey) {
|
|
1055
|
+
const g = buildGroups().find(g => String(g.key) === String(groupKey));
|
|
1056
|
+
if (!g) return;
|
|
1057
|
+
const free = g.atoms.filter(a => !isLocked(a));
|
|
1058
|
+
const all = free.length && free.every(a => selectionRefs.has(a.ref));
|
|
1059
|
+
for (const a of free) all ? selectionRefs.delete(a.ref) : selectionRefs.add(a.ref);
|
|
800
1060
|
selectionDirty = true;
|
|
801
1061
|
render();
|
|
802
1062
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
for (
|
|
808
|
-
if (allSelected) { selectionRefs.delete(atom.ref); keepRefs.delete(atom.ref); }
|
|
809
|
-
else selectionRefs.add(atom.ref);
|
|
810
|
-
}
|
|
1063
|
+
function unselectSpan(startRef, endRef) {
|
|
1064
|
+
const start = state.atoms.findIndex(a => a.ref === startRef);
|
|
1065
|
+
const end = state.atoms.findIndex(a => a.ref === endRef);
|
|
1066
|
+
if (start < 0 || end < start) return;
|
|
1067
|
+
for (let i = start; i <= end; i++) selectionRefs.delete(state.atoms[i].ref);
|
|
811
1068
|
selectionDirty = true;
|
|
812
1069
|
render();
|
|
813
1070
|
}
|
|
1071
|
+
function moveCursor(delta) {
|
|
1072
|
+
const visible = visibleSelectionIndexes();
|
|
1073
|
+
if (!visible.length) return;
|
|
1074
|
+
const current = visible.indexOf(cursor);
|
|
1075
|
+
const next = current < 0
|
|
1076
|
+
? (delta < 0 ? visible.length - 1 : 0)
|
|
1077
|
+
: Math.max(0, Math.min(visible.length - 1, current + delta));
|
|
1078
|
+
cursor = visible[next];
|
|
1079
|
+
render();
|
|
1080
|
+
document.querySelector(`[data-idx="${cursor}"]`)?.scrollIntoView({ block: 'nearest' });
|
|
1081
|
+
}
|
|
814
1082
|
|
|
815
1083
|
async function saveRange(id) {
|
|
816
1084
|
const r = rangeById(id);
|
|
@@ -818,6 +1086,7 @@ async function saveRange(id) {
|
|
|
818
1086
|
if (!r || !e) return false;
|
|
819
1087
|
if (!e.summary.trim()) { toast('Summary cannot be empty', 'err'); $('summary')?.focus(); return false; }
|
|
820
1088
|
const base = `/api/range/${encodeURIComponent(id)}`;
|
|
1089
|
+
if (document.body.classList.contains('dead')) { toast('Session closed in the terminal', 'err'); return false; }
|
|
821
1090
|
if (e.topic.trim() !== (r.topic ?? '').trim()) state = await post(`${base}/edit-topic`, { value: e.topic.trim() });
|
|
822
1091
|
if (e.summary.trim() !== r.summary) state = await post(`${base}/edit-summary`, { value: e.summary.trim() });
|
|
823
1092
|
edits.delete(id);
|
|
@@ -825,6 +1094,7 @@ async function saveRange(id) {
|
|
|
825
1094
|
}
|
|
826
1095
|
|
|
827
1096
|
async function saveCurrent() {
|
|
1097
|
+
if (isSelection()) { await saveSelection(); return; }
|
|
828
1098
|
if (!selectedId || !isDirty(rangeById(selectedId) ?? {})) return;
|
|
829
1099
|
try { if (await saveRange(selectedId)) { render(); toast('Saved ' + selectedId, 'ok'); } }
|
|
830
1100
|
catch (err) { toast(String(err.message ?? err), 'err'); }
|
|
@@ -835,25 +1105,42 @@ async function refresh() {
|
|
|
835
1105
|
const res = await fetch('/api/state');
|
|
836
1106
|
if (!res.ok) throw new Error('state fetch failed');
|
|
837
1107
|
state = await res.json();
|
|
838
|
-
|
|
839
|
-
keepRefs.clear();
|
|
840
|
-
selectionDirty = false;
|
|
841
|
-
if (!rangeById(selectedId)) selectedId = state.draft.ranges[0]?.id ?? null;
|
|
1108
|
+
afterServerState();
|
|
842
1109
|
render();
|
|
843
1110
|
toast('Reloaded', 'ok');
|
|
844
1111
|
} catch (err) { toast(String(err.message ?? err), 'err'); }
|
|
845
1112
|
}
|
|
846
1113
|
|
|
1114
|
+
/* ---------- drawer ---------- */
|
|
1115
|
+
async function openDrawer(ref) {
|
|
1116
|
+
$('dw-ref').textContent = ref;
|
|
1117
|
+
$('dw-meta').textContent = 'loading…';
|
|
1118
|
+
$('dw-body').textContent = 'Loading…';
|
|
1119
|
+
$('drawer').classList.add('show');
|
|
1120
|
+
$('drawer-bg').classList.add('show');
|
|
1121
|
+
try {
|
|
1122
|
+
const res = await fetch('/api/atom/' + encodeURIComponent(ref));
|
|
1123
|
+
if (!res.ok) {
|
|
1124
|
+
const e = await res.json().catch(() => ({}));
|
|
1125
|
+
throw new Error(e.error || `HTTP ${res.status}`);
|
|
1126
|
+
}
|
|
1127
|
+
const a = await res.json();
|
|
1128
|
+
$('dw-ref').textContent = a.ref;
|
|
1129
|
+
$('dw-meta').textContent = `${a.kind} · ${fmt(a.contentChars)} chars · ${a.imageCount} image${a.imageCount === 1 ? '' : 's'}`;
|
|
1130
|
+
$('dw-body').textContent = a.fullText;
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
$('dw-body').textContent = `Failed to load atom: ${err.message ?? err}`;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function closeDrawer() {
|
|
1136
|
+
$('drawer').classList.remove('show');
|
|
1137
|
+
$('drawer-bg').classList.remove('show');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
847
1140
|
/* ---------- events ---------- */
|
|
848
1141
|
$('range-list').addEventListener('click', e => {
|
|
849
|
-
const
|
|
850
|
-
if (
|
|
851
|
-
const range = proposedRanges()[Number(remove.dataset.removeProposed)];
|
|
852
|
-
range?.atoms.forEach(atom => { selectionRefs.delete(atom.ref); keepRefs.delete(atom.ref); });
|
|
853
|
-
selectionDirty = true;
|
|
854
|
-
render();
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
1142
|
+
const un = e.target.closest('[data-unsel-start]');
|
|
1143
|
+
if (un) { unselectSpan(un.dataset.unselStart, un.dataset.unselEnd); return; }
|
|
857
1144
|
const item = e.target.closest('[data-range]');
|
|
858
1145
|
if (item) select(item.dataset.range);
|
|
859
1146
|
});
|
|
@@ -863,44 +1150,56 @@ $('range-list').addEventListener('keydown', e => {
|
|
|
863
1150
|
});
|
|
864
1151
|
|
|
865
1152
|
$('timeline').addEventListener('click', e => {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if (groupToggle) { toggleSelectionGroup(groupToggle.dataset.selectGroup); return; }
|
|
872
|
-
const atom = e.target.closest('[data-atom]');
|
|
873
|
-
if (atom && e.target.closest('.abody')) {
|
|
874
|
-
const ref = atom.dataset.atom;
|
|
875
|
-
openAtoms.has(ref) ? openAtoms.delete(ref) : openAtoms.add(ref);
|
|
876
|
-
atom.classList.toggle('open');
|
|
1153
|
+
if (isSelection()) {
|
|
1154
|
+
const groupBtn = e.target.closest('[data-group]');
|
|
1155
|
+
if (groupBtn) { toggleGroupAtoms(groupBtn.dataset.group); return; }
|
|
1156
|
+
const row = e.target.closest('.srow[data-idx]');
|
|
1157
|
+
if (row) { cursor = Number(row.dataset.idx); toggleAtom(row.dataset.atom); return; }
|
|
877
1158
|
return;
|
|
878
1159
|
}
|
|
1160
|
+
const orig = e.target.closest('[data-orig]');
|
|
1161
|
+
if (orig) { openDrawer(orig.dataset.orig); return; }
|
|
1162
|
+
const fix = e.target.closest('[data-fix]');
|
|
1163
|
+
if (fix) { mode = 'raw'; syncMode(); select(fix.dataset.fix); $('summary')?.focus(); return; }
|
|
1164
|
+
const edit = e.target.closest('[data-edit]');
|
|
1165
|
+
if (edit) { mode = 'raw'; syncMode(); select(edit.dataset.edit); $('summary')?.focus(); return; }
|
|
879
1166
|
const head = e.target.closest('[data-toggle]');
|
|
880
1167
|
if (!head) return;
|
|
881
1168
|
const key = head.dataset.toggle;
|
|
882
1169
|
const chevron = e.target.closest('.chev');
|
|
883
1170
|
if (head.dataset.select && !chevron && head.dataset.select !== selectedId) { select(head.dataset.select); return; }
|
|
884
1171
|
collapsed.has(key) ? collapsed.delete(key) : collapsed.add(key);
|
|
885
|
-
head.closest('.group')
|
|
1172
|
+
const group = head.closest('.group');
|
|
1173
|
+
group.classList.toggle('open');
|
|
1174
|
+
const body = group.querySelector('.gbody');
|
|
1175
|
+
if (body) body.hidden = collapsed.has(key);
|
|
886
1176
|
head.setAttribute('aria-expanded', String(!collapsed.has(key)));
|
|
887
1177
|
});
|
|
888
1178
|
|
|
889
1179
|
$('toggle-all').addEventListener('click', () => {
|
|
890
1180
|
if (collapsed.size) { collapsed.clear(); $('toggle-all').textContent = 'Collapse all'; }
|
|
891
1181
|
else {
|
|
892
|
-
buildGroups().forEach(
|
|
1182
|
+
buildGroups().forEach(g => collapsed.add(g.key ?? ('keep-' + g.idx)));
|
|
893
1183
|
$('toggle-all').textContent = 'Expand all';
|
|
894
1184
|
}
|
|
895
1185
|
renderTimeline();
|
|
896
1186
|
});
|
|
897
1187
|
|
|
898
|
-
document.querySelectorAll('
|
|
1188
|
+
document.querySelectorAll('#policy-seg [data-filter]').forEach(b => b.addEventListener('click', () => {
|
|
899
1189
|
policy = b.dataset.filter;
|
|
900
|
-
document.querySelectorAll('
|
|
1190
|
+
document.querySelectorAll('#policy-seg [data-filter]').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
|
|
901
1191
|
renderTimeline();
|
|
902
1192
|
}));
|
|
903
1193
|
|
|
1194
|
+
function syncMode() {
|
|
1195
|
+
document.querySelectorAll('#mode-seg [data-mode]').forEach(x => x.setAttribute('aria-pressed', String(x.dataset.mode === mode)));
|
|
1196
|
+
renderTimeline();
|
|
1197
|
+
}
|
|
1198
|
+
document.querySelectorAll('#mode-seg [data-mode]').forEach(b => b.addEventListener('click', () => {
|
|
1199
|
+
mode = b.dataset.mode;
|
|
1200
|
+
syncMode();
|
|
1201
|
+
}));
|
|
1202
|
+
|
|
904
1203
|
$('q').addEventListener('input', () => {
|
|
905
1204
|
query = $('q').value.trim().toLowerCase();
|
|
906
1205
|
$('q-clear').hidden = !query;
|
|
@@ -919,19 +1218,22 @@ $('editor').addEventListener('input', e => {
|
|
|
919
1218
|
$('f-summary').classList.toggle('invalid', !$('summary').value.trim());
|
|
920
1219
|
updateCount();
|
|
921
1220
|
renderRanges();
|
|
1221
|
+
renderStrip();
|
|
922
1222
|
const d = dirtyIds();
|
|
923
1223
|
$('dirty-badge').hidden = d.length === 0;
|
|
924
1224
|
$('dirty-badge').textContent = `${d.length} unsaved`;
|
|
925
1225
|
$('save-all').hidden = d.length < 2;
|
|
926
1226
|
$('save-all').textContent = `Save all (${d.length})`;
|
|
927
|
-
if (e.target.id === 'topic') renderTimeline();
|
|
1227
|
+
if (e.target.id === 'topic' || e.target.id === 'summary') renderTimeline();
|
|
928
1228
|
});
|
|
929
1229
|
|
|
930
|
-
$('save-selection').addEventListener('click', saveSelection);
|
|
931
1230
|
$('editor').addEventListener('click', async e => {
|
|
932
1231
|
if (e.target.closest('#selection-save-side')) { await saveSelection(); return; }
|
|
933
|
-
if (e.target.closest('#selection-clear')) {
|
|
934
|
-
|
|
1232
|
+
if (e.target.closest('#selection-clear')) { selectionRefs.clear(); selectionDirty = true; render(); return; }
|
|
1233
|
+
if (e.target.closest('#rev-check')) {
|
|
1234
|
+
const r = rangeById(selectedId);
|
|
1235
|
+
if (r) { $('rev-check').checked ? reviewed.add(r.id) : reviewed.delete(r.id); reviewedStoreSave(); renderRanges(); renderTimeline(); }
|
|
1236
|
+
return;
|
|
935
1237
|
}
|
|
936
1238
|
const r = rangeById(selectedId);
|
|
937
1239
|
if (!r) return;
|
|
@@ -948,23 +1250,28 @@ $('editor').addEventListener('click', async e => {
|
|
|
948
1250
|
try {
|
|
949
1251
|
state = await post(`/api/range/${encodeURIComponent(r.id)}/remove`, {});
|
|
950
1252
|
edits.delete(r.id);
|
|
951
|
-
|
|
952
|
-
lastScrolled = null;
|
|
1253
|
+
afterServerState();
|
|
953
1254
|
render();
|
|
954
1255
|
toast('Removed ' + r.id, 'ok');
|
|
955
1256
|
} catch (err) { toast(String(err.message ?? err), 'err'); }
|
|
956
1257
|
}
|
|
957
1258
|
});
|
|
958
1259
|
|
|
1260
|
+
$('save-selection').addEventListener('click', saveSelection);
|
|
959
1261
|
$('save-all').addEventListener('click', async () => {
|
|
960
1262
|
try {
|
|
961
1263
|
for (const id of dirtyIds()) await saveRange(id);
|
|
962
1264
|
render(); toast('All changes saved', 'ok');
|
|
963
1265
|
} catch (err) { render(); toast(String(err.message ?? err), 'err'); }
|
|
964
1266
|
});
|
|
965
|
-
|
|
966
1267
|
$('refresh').addEventListener('click', refresh);
|
|
967
1268
|
|
|
1269
|
+
$('strip').addEventListener('click', e => {
|
|
1270
|
+
if (isSelection() || !e.target.closest('[data-toggle-mode]')) return;
|
|
1271
|
+
mode = mode === 'raw' ? 'proj' : 'raw';
|
|
1272
|
+
syncMode();
|
|
1273
|
+
});
|
|
1274
|
+
|
|
968
1275
|
async function closeWorkbench() {
|
|
969
1276
|
try {
|
|
970
1277
|
await post('/api/close', {});
|
|
@@ -973,17 +1280,34 @@ async function closeWorkbench() {
|
|
|
973
1280
|
}
|
|
974
1281
|
|
|
975
1282
|
$('close').addEventListener('click', () => {
|
|
976
|
-
|
|
1283
|
+
const pending = isSelection() ? [] : pendingIds();
|
|
1284
|
+
if (dirtyIds().length || selectionDirty || pending.length) {
|
|
1285
|
+
const dirty = dirtyIds().length > 0 || selectionDirty;
|
|
1286
|
+
$('close-dialog-line').textContent = dirty
|
|
1287
|
+
? 'You have unsaved edits.'
|
|
1288
|
+
: 'All ranges are saved. Nothing is applied yet — the draft waits in the terminal.';
|
|
1289
|
+
$('close-warn').hidden = !pending.length;
|
|
1290
|
+
if (pending.length) {
|
|
1291
|
+
$('close-warn-count').textContent = pending.length;
|
|
1292
|
+
$('close-warn-list').textContent = `${pending.join(', ')} — /midcompact:commit will refuse ${pending.length === 1 ? 'it' : 'them'} until a summary is written.`;
|
|
1293
|
+
}
|
|
1294
|
+
$('close-fix').hidden = !pending.length;
|
|
977
1295
|
$('close-dialog').classList.add('show');
|
|
978
1296
|
return;
|
|
979
1297
|
}
|
|
980
1298
|
closeWorkbench();
|
|
981
1299
|
});
|
|
982
1300
|
$('close-stay').addEventListener('click', () => $('close-dialog').classList.remove('show'));
|
|
1301
|
+
$('close-fix').addEventListener('click', () => {
|
|
1302
|
+
$('close-dialog').classList.remove('show');
|
|
1303
|
+
const pending = pendingIds()[0];
|
|
1304
|
+
if (pending) { mode = 'raw'; syncMode(); select(pending); $('summary')?.focus(); }
|
|
1305
|
+
});
|
|
983
1306
|
$('close-discard').addEventListener('click', () => {
|
|
984
1307
|
edits.clear();
|
|
985
1308
|
selectionDirty = false;
|
|
986
1309
|
$('close-dialog').classList.remove('show');
|
|
1310
|
+
render();
|
|
987
1311
|
closeWorkbench();
|
|
988
1312
|
});
|
|
989
1313
|
$('close-save').addEventListener('click', async () => {
|
|
@@ -998,6 +1322,10 @@ $('close-save').addEventListener('click', async () => {
|
|
|
998
1322
|
await closeWorkbench();
|
|
999
1323
|
});
|
|
1000
1324
|
|
|
1325
|
+
/* drawer */
|
|
1326
|
+
$('dw-close').addEventListener('click', closeDrawer);
|
|
1327
|
+
$('drawer-bg').addEventListener('click', closeDrawer);
|
|
1328
|
+
|
|
1001
1329
|
/* theme */
|
|
1002
1330
|
function themeIcon() {
|
|
1003
1331
|
$('theme-toggle').textContent = document.documentElement.getAttribute('data-theme') === 'light' ? '☀' : '☾';
|
|
@@ -1023,14 +1351,40 @@ function moveSel(delta) {
|
|
|
1023
1351
|
}
|
|
1024
1352
|
document.addEventListener('keydown', e => {
|
|
1025
1353
|
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName);
|
|
1026
|
-
if (
|
|
1354
|
+
if (document.body.classList.contains('dead')) {
|
|
1355
|
+
if (e.key === 'Escape' && typing) e.target.blur();
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') {
|
|
1359
|
+
e.preventDefault();
|
|
1360
|
+
if (isSelection()) saveSelection(); else saveCurrent();
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1027
1363
|
if (e.key === 'Escape') {
|
|
1028
1364
|
help.classList.remove('show');
|
|
1029
1365
|
$('close-dialog').classList.remove('show');
|
|
1366
|
+
closeDrawer();
|
|
1030
1367
|
if (typing) e.target.blur();
|
|
1031
1368
|
return;
|
|
1032
1369
|
}
|
|
1033
1370
|
if (typing) return;
|
|
1371
|
+
if (isSelection()) {
|
|
1372
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); moveCursor(1); return; }
|
|
1373
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); moveCursor(-1); return; }
|
|
1374
|
+
if (e.key === ' ') {
|
|
1375
|
+
e.preventDefault();
|
|
1376
|
+
if (visibleSelectionIndexes().includes(cursor)) toggleAtom(state.atoms[cursor]?.ref);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (e.key === 'g' || e.key === 'G') {
|
|
1380
|
+
const a = visibleSelectionIndexes().includes(cursor) ? state.atoms[cursor] : null;
|
|
1381
|
+
if (a) toggleGroupAtoms(a.groupRef);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (e.key === '/') { e.preventDefault(); $('q').focus(); return; }
|
|
1385
|
+
if (e.key === '?') { help.classList.toggle('show'); return; }
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1034
1388
|
if (e.key === '/') { e.preventDefault(); $('q').focus(); return; }
|
|
1035
1389
|
if (e.key === '?') { help.classList.toggle('show'); return; }
|
|
1036
1390
|
if (e.key === 'j' || e.key === 'ArrowDown') { e.preventDefault(); moveSel(1); return; }
|
|
@@ -1052,7 +1406,9 @@ function toast(msg, kind) {
|
|
|
1052
1406
|
toastTimer = setTimeout(() => t.classList.remove('show'), kind === 'err' ? 3200 : 1800);
|
|
1053
1407
|
}
|
|
1054
1408
|
|
|
1055
|
-
window.addEventListener('beforeunload', e => {
|
|
1409
|
+
window.addEventListener('beforeunload', e => {
|
|
1410
|
+
if (dirtyIds().length || selectionDirty) { e.preventDefault(); e.returnValue = ''; }
|
|
1411
|
+
});
|
|
1056
1412
|
window.addEventListener('pagehide', () => {
|
|
1057
1413
|
if (!dirtyIds().length && !selectionDirty) navigator.sendBeacon('/api/close', new Blob(['{}'], { type: 'application/json' }));
|
|
1058
1414
|
});
|