@rajat-rastogi/maestro 0.1.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/LICENSE +21 -0
- package/README.md +254 -0
- package/defaults/agents/documentation.txt +18 -0
- package/defaults/agents/implementation.txt +16 -0
- package/defaults/agents/quality.txt +19 -0
- package/defaults/agents/simplification.txt +19 -0
- package/defaults/agents/testing.txt +16 -0
- package/defaults/config +46 -0
- package/defaults/plan-tips.md +50 -0
- package/defaults/prompts/finalize.txt +14 -0
- package/defaults/prompts/hint_analysis.txt +38 -0
- package/defaults/prompts/plan_create.txt +36 -0
- package/defaults/prompts/review_first.txt +37 -0
- package/defaults/prompts/review_second.txt +36 -0
- package/defaults/prompts/task.txt +23 -0
- package/dist/backend/backend.d.ts +29 -0
- package/dist/backend/backend.d.ts.map +1 -0
- package/dist/backend/backend.js +9 -0
- package/dist/backend/backend.js.map +1 -0
- package/dist/backend/claude.d.ts +12 -0
- package/dist/backend/claude.d.ts.map +1 -0
- package/dist/backend/claude.js +155 -0
- package/dist/backend/claude.js.map +1 -0
- package/dist/backend/copilot.d.ts +11 -0
- package/dist/backend/copilot.d.ts.map +1 -0
- package/dist/backend/copilot.js +110 -0
- package/dist/backend/copilot.js.map +1 -0
- package/dist/backend/factory.d.ts +4 -0
- package/dist/backend/factory.d.ts.map +1 -0
- package/dist/backend/factory.js +13 -0
- package/dist/backend/factory.js.map +1 -0
- package/dist/config/configserver.d.ts +9 -0
- package/dist/config/configserver.d.ts.map +1 -0
- package/dist/config/configserver.js +141 -0
- package/dist/config/configserver.js.map +1 -0
- package/dist/config/dump.d.ts +2 -0
- package/dist/config/dump.d.ts.map +1 -0
- package/dist/config/dump.js +24 -0
- package/dist/config/dump.js.map +1 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +119 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/prompts.d.ts +19 -0
- package/dist/config/prompts.d.ts.map +1 -0
- package/dist/config/prompts.js +128 -0
- package/dist/config/prompts.js.map +1 -0
- package/dist/config/reset.d.ts +3 -0
- package/dist/config/reset.d.ts.map +1 -0
- package/dist/config/reset.js +23 -0
- package/dist/config/reset.js.map +1 -0
- package/dist/config/template.d.ts +5 -0
- package/dist/config/template.d.ts.map +1 -0
- package/dist/config/template.js +11 -0
- package/dist/config/template.js.map +1 -0
- package/dist/config/types.d.ts +35 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +42 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/write.d.ts +5 -0
- package/dist/config/write.d.ts.map +1 -0
- package/dist/config/write.js +59 -0
- package/dist/config/write.js.map +1 -0
- package/dist/dashboard/assets/config.html +1012 -0
- package/dist/dashboard/assets/dashboard.html +871 -0
- package/dist/dashboard/assets/help.html +657 -0
- package/dist/dashboard/assets.d.ts +2 -0
- package/dist/dashboard/assets.d.ts.map +1 -0
- package/dist/dashboard/assets.js +5 -0
- package/dist/dashboard/assets.js.map +1 -0
- package/dist/dashboard/event-bus.d.ts +11 -0
- package/dist/dashboard/event-bus.d.ts.map +1 -0
- package/dist/dashboard/event-bus.js +4 -0
- package/dist/dashboard/event-bus.js.map +1 -0
- package/dist/dashboard/replay-parser.d.ts +6 -0
- package/dist/dashboard/replay-parser.d.ts.map +1 -0
- package/dist/dashboard/replay-parser.js +56 -0
- package/dist/dashboard/replay-parser.js.map +1 -0
- package/dist/dashboard/server.d.ts +14 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +178 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/watcher.d.ts +17 -0
- package/dist/dashboard/watcher.d.ts.map +1 -0
- package/dist/dashboard/watcher.js +73 -0
- package/dist/dashboard/watcher.js.map +1 -0
- package/dist/executor/hints.d.ts +21 -0
- package/dist/executor/hints.d.ts.map +1 -0
- package/dist/executor/hints.js +102 -0
- package/dist/executor/hints.js.map +1 -0
- package/dist/executor/task.d.ts +19 -0
- package/dist/executor/task.d.ts.map +1 -0
- package/dist/executor/task.js +119 -0
- package/dist/executor/task.js.map +1 -0
- package/dist/executor/validation.d.ts +18 -0
- package/dist/executor/validation.d.ts.map +1 -0
- package/dist/executor/validation.js +73 -0
- package/dist/executor/validation.js.map +1 -0
- package/dist/git/branch.d.ts +23 -0
- package/dist/git/branch.d.ts.map +1 -0
- package/dist/git/branch.js +64 -0
- package/dist/git/branch.js.map +1 -0
- package/dist/git/commit.d.ts +21 -0
- package/dist/git/commit.d.ts.map +1 -0
- package/dist/git/commit.js +37 -0
- package/dist/git/commit.js.map +1 -0
- package/dist/git/diff.d.ts +15 -0
- package/dist/git/diff.d.ts.map +1 -0
- package/dist/git/diff.js +26 -0
- package/dist/git/diff.js.map +1 -0
- package/dist/git/git.d.ts +9 -0
- package/dist/git/git.d.ts.map +1 -0
- package/dist/git/git.js +20 -0
- package/dist/git/git.js.map +1 -0
- package/dist/git/gitignore.d.ts +2 -0
- package/dist/git/gitignore.d.ts.map +1 -0
- package/dist/git/gitignore.js +66 -0
- package/dist/git/gitignore.js.map +1 -0
- package/dist/git/worktree.d.ts +3 -0
- package/dist/git/worktree.d.ts.map +1 -0
- package/dist/git/worktree.js +17 -0
- package/dist/git/worktree.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +212 -0
- package/dist/main.js.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +21 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.js +218 -0
- package/dist/orchestrator/orchestrator.js.map +1 -0
- package/dist/plan/creator.d.ts +27 -0
- package/dist/plan/creator.d.ts.map +1 -0
- package/dist/plan/creator.js +251 -0
- package/dist/plan/creator.js.map +1 -0
- package/dist/plan/parser.d.ts +11 -0
- package/dist/plan/parser.d.ts.map +1 -0
- package/dist/plan/parser.js +151 -0
- package/dist/plan/parser.js.map +1 -0
- package/dist/plan/types.d.ts +23 -0
- package/dist/plan/types.d.ts.map +1 -0
- package/dist/plan/types.js +2 -0
- package/dist/plan/types.js.map +1 -0
- package/dist/plan/updater.d.ts +16 -0
- package/dist/plan/updater.d.ts.map +1 -0
- package/dist/plan/updater.js +59 -0
- package/dist/plan/updater.js.map +1 -0
- package/dist/progress/colors.d.ts +9 -0
- package/dist/progress/colors.d.ts.map +1 -0
- package/dist/progress/colors.js +39 -0
- package/dist/progress/colors.js.map +1 -0
- package/dist/progress/logger.d.ts +17 -0
- package/dist/progress/logger.d.ts.map +1 -0
- package/dist/progress/logger.js +101 -0
- package/dist/progress/logger.js.map +1 -0
- package/dist/progress/notify.d.ts +2 -0
- package/dist/progress/notify.d.ts.map +1 -0
- package/dist/progress/notify.js +37 -0
- package/dist/progress/notify.js.map +1 -0
- package/dist/progress/timestamp.d.ts +5 -0
- package/dist/progress/timestamp.d.ts.map +1 -0
- package/dist/progress/timestamp.js +13 -0
- package/dist/progress/timestamp.js.map +1 -0
- package/dist/review/agents.d.ts +18 -0
- package/dist/review/agents.d.ts.map +1 -0
- package/dist/review/agents.js +43 -0
- package/dist/review/agents.js.map +1 -0
- package/dist/review/pipeline.d.ts +36 -0
- package/dist/review/pipeline.d.ts.map +1 -0
- package/dist/review/pipeline.js +210 -0
- package/dist/review/pipeline.js.map +1 -0
- package/dist/sea.d.ts +19 -0
- package/dist/sea.d.ts.map +1 -0
- package/dist/sea.js +41 -0
- package/dist/sea.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Maestro Config</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--task: #00ff00;
|
|
12
|
+
--review: #00ffff;
|
|
13
|
+
--signal: #ff6464;
|
|
14
|
+
--warn: #ffff00;
|
|
15
|
+
--error: #ff0000;
|
|
16
|
+
--info: #b4b4b4;
|
|
17
|
+
--ts: #8a8a8a;
|
|
18
|
+
--bg: #1a1a1a;
|
|
19
|
+
--section-bg: #252525;
|
|
20
|
+
--border: #333;
|
|
21
|
+
--header-bg: #111;
|
|
22
|
+
--tab-active: #333;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--info);
|
|
28
|
+
font-family: 'Consolas', 'Courier New', monospace;
|
|
29
|
+
font-size: 13px;
|
|
30
|
+
line-height: 1.5;
|
|
31
|
+
overflow-x: hidden;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ── Header bar ── */
|
|
35
|
+
#header {
|
|
36
|
+
position: fixed;
|
|
37
|
+
top: 0; left: 0; right: 0;
|
|
38
|
+
height: 40px;
|
|
39
|
+
background: var(--header-bg);
|
|
40
|
+
border-bottom: 1px solid var(--border);
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
padding: 0 12px;
|
|
44
|
+
gap: 12px;
|
|
45
|
+
z-index: 100;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#header-title {
|
|
49
|
+
font-weight: bold;
|
|
50
|
+
color: var(--task);
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#header-spacer { flex: 1; }
|
|
55
|
+
|
|
56
|
+
.btn {
|
|
57
|
+
background: #333;
|
|
58
|
+
border: 1px solid #555;
|
|
59
|
+
color: var(--info);
|
|
60
|
+
padding: 3px 12px;
|
|
61
|
+
border-radius: 3px;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
font-family: inherit;
|
|
65
|
+
}
|
|
66
|
+
.btn:hover { background: #444; }
|
|
67
|
+
.btn-primary {
|
|
68
|
+
background: #1a3a1a;
|
|
69
|
+
border-color: var(--task);
|
|
70
|
+
color: var(--task);
|
|
71
|
+
}
|
|
72
|
+
.btn-primary:hover { background: #234a23; }
|
|
73
|
+
.btn-primary:disabled {
|
|
74
|
+
opacity: 0.4;
|
|
75
|
+
cursor: default;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ── Toolbar (target selector) ── */
|
|
79
|
+
#toolbar {
|
|
80
|
+
position: fixed;
|
|
81
|
+
top: 40px; left: 0; right: 0;
|
|
82
|
+
height: 38px;
|
|
83
|
+
background: #141414;
|
|
84
|
+
border-bottom: 1px solid var(--border);
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
padding: 0 12px;
|
|
88
|
+
gap: 16px;
|
|
89
|
+
z-index: 99;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#toolbar label {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 6px;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
color: var(--ts);
|
|
99
|
+
}
|
|
100
|
+
#toolbar label.active { color: var(--info); }
|
|
101
|
+
|
|
102
|
+
#toolbar input[type=radio] {
|
|
103
|
+
accent-color: var(--task);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.target-path {
|
|
107
|
+
color: var(--ts);
|
|
108
|
+
font-size: 11px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── Search row ── */
|
|
112
|
+
#search-row {
|
|
113
|
+
position: fixed;
|
|
114
|
+
top: 78px;
|
|
115
|
+
left: 0; right: 0;
|
|
116
|
+
height: 38px;
|
|
117
|
+
background: #0a0a0a;
|
|
118
|
+
border-bottom: 1px solid var(--border);
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
padding: 0 12px;
|
|
122
|
+
z-index: 99;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#search-input {
|
|
126
|
+
width: 100%;
|
|
127
|
+
max-width: 400px;
|
|
128
|
+
background: #1a1a1a;
|
|
129
|
+
border: 1px solid #444;
|
|
130
|
+
color: var(--info);
|
|
131
|
+
font-family: inherit;
|
|
132
|
+
font-size: 13px;
|
|
133
|
+
padding: 4px 8px;
|
|
134
|
+
outline: none;
|
|
135
|
+
border-radius: 2px;
|
|
136
|
+
}
|
|
137
|
+
#search-input:focus { border-color: var(--task); }
|
|
138
|
+
#search-input::placeholder { color: #555; }
|
|
139
|
+
|
|
140
|
+
#no-match {
|
|
141
|
+
display: none;
|
|
142
|
+
color: var(--ts);
|
|
143
|
+
font-style: italic;
|
|
144
|
+
margin: 20px 12px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ── Main content ── */
|
|
148
|
+
#content {
|
|
149
|
+
margin-top: 116px;
|
|
150
|
+
padding: 12px 16px 40px;
|
|
151
|
+
max-width: 860px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ── Section ── */
|
|
155
|
+
.section {
|
|
156
|
+
margin-bottom: 6px;
|
|
157
|
+
border: 1px solid var(--border);
|
|
158
|
+
border-radius: 3px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.section-header {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 6px;
|
|
165
|
+
padding: 6px 10px;
|
|
166
|
+
background: var(--section-bg);
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
user-select: none;
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
font-weight: bold;
|
|
171
|
+
color: var(--warn);
|
|
172
|
+
border-bottom: 1px solid var(--border);
|
|
173
|
+
border-radius: 3px 3px 0 0;
|
|
174
|
+
}
|
|
175
|
+
.section-header:hover { background: #2a2a2a; }
|
|
176
|
+
.section.collapsed .section-header { border-bottom: none; border-radius: 3px; }
|
|
177
|
+
|
|
178
|
+
.section-toggle { font-size: 10px; color: var(--ts); }
|
|
179
|
+
.section-dirty { color: var(--warn); margin-left: 4px; }
|
|
180
|
+
|
|
181
|
+
.section-body { padding: 4px 0; }
|
|
182
|
+
.section.collapsed .section-body { display: none; }
|
|
183
|
+
|
|
184
|
+
/* ── Field row ── */
|
|
185
|
+
.field-row {
|
|
186
|
+
display: grid;
|
|
187
|
+
grid-template-columns: 200px 1fr 24px;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: 8px;
|
|
190
|
+
padding: 5px 10px;
|
|
191
|
+
border-bottom: 1px solid #222;
|
|
192
|
+
}
|
|
193
|
+
.field-row:last-child { border-bottom: none; }
|
|
194
|
+
|
|
195
|
+
.field-label {
|
|
196
|
+
position: relative;
|
|
197
|
+
cursor: default;
|
|
198
|
+
color: var(--ts);
|
|
199
|
+
font-size: 12px;
|
|
200
|
+
white-space: nowrap;
|
|
201
|
+
}
|
|
202
|
+
.field-label[data-desc]:hover::after {
|
|
203
|
+
content: attr(data-desc);
|
|
204
|
+
position: absolute;
|
|
205
|
+
left: 0;
|
|
206
|
+
top: calc(100% + 4px);
|
|
207
|
+
z-index: 300;
|
|
208
|
+
background: #1e1e1e;
|
|
209
|
+
border: 1px solid #555;
|
|
210
|
+
color: var(--info);
|
|
211
|
+
font-size: 11px;
|
|
212
|
+
padding: 4px 8px;
|
|
213
|
+
border-radius: 3px;
|
|
214
|
+
white-space: pre-wrap;
|
|
215
|
+
max-width: 320px;
|
|
216
|
+
line-height: 1.4;
|
|
217
|
+
pointer-events: none;
|
|
218
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.field-control { display: flex; align-items: center; gap: 6px; }
|
|
222
|
+
|
|
223
|
+
.field-input {
|
|
224
|
+
background: #222;
|
|
225
|
+
border: 1px solid var(--border);
|
|
226
|
+
color: var(--info);
|
|
227
|
+
font-family: inherit;
|
|
228
|
+
font-size: 12px;
|
|
229
|
+
padding: 3px 6px;
|
|
230
|
+
border-radius: 2px;
|
|
231
|
+
width: 100%;
|
|
232
|
+
min-width: 0;
|
|
233
|
+
}
|
|
234
|
+
.field-input:focus { outline: none; border-color: #555; }
|
|
235
|
+
.field-input.overridden { border-color: var(--warn); }
|
|
236
|
+
.field-input.dirty { border-color: var(--warn); }
|
|
237
|
+
.field-input.invalid { border-color: var(--error); }
|
|
238
|
+
|
|
239
|
+
.field-hint {
|
|
240
|
+
font-size: 10px;
|
|
241
|
+
color: var(--ts);
|
|
242
|
+
white-space: nowrap;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ── Select ── */
|
|
246
|
+
.field-select {
|
|
247
|
+
background: #222;
|
|
248
|
+
border: 1px solid var(--border);
|
|
249
|
+
color: var(--info);
|
|
250
|
+
font-family: inherit;
|
|
251
|
+
font-size: 12px;
|
|
252
|
+
padding: 3px 6px;
|
|
253
|
+
border-radius: 2px;
|
|
254
|
+
}
|
|
255
|
+
.field-select.overridden { border-color: var(--warn); }
|
|
256
|
+
.field-select.dirty { border-color: var(--warn); }
|
|
257
|
+
.field-select:focus { outline: none; border-color: #555; }
|
|
258
|
+
|
|
259
|
+
/* ── Toggle ── */
|
|
260
|
+
.toggle-wrap {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
}
|
|
265
|
+
.toggle {
|
|
266
|
+
position: relative;
|
|
267
|
+
width: 36px;
|
|
268
|
+
height: 18px;
|
|
269
|
+
flex-shrink: 0;
|
|
270
|
+
}
|
|
271
|
+
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
272
|
+
.toggle-slider {
|
|
273
|
+
position: absolute;
|
|
274
|
+
inset: 0;
|
|
275
|
+
background: #333;
|
|
276
|
+
border-radius: 9px;
|
|
277
|
+
cursor: pointer;
|
|
278
|
+
transition: background 0.2s;
|
|
279
|
+
border: 1px solid #555;
|
|
280
|
+
}
|
|
281
|
+
.toggle-slider::before {
|
|
282
|
+
content: '';
|
|
283
|
+
position: absolute;
|
|
284
|
+
width: 12px; height: 12px;
|
|
285
|
+
left: 2px; top: 2px;
|
|
286
|
+
border-radius: 50%;
|
|
287
|
+
background: var(--info);
|
|
288
|
+
transition: transform 0.2s;
|
|
289
|
+
}
|
|
290
|
+
.toggle input:checked + .toggle-slider { background: #1a3a1a; border-color: var(--task); }
|
|
291
|
+
.toggle input:checked + .toggle-slider::before {
|
|
292
|
+
transform: translateX(18px);
|
|
293
|
+
background: var(--task);
|
|
294
|
+
}
|
|
295
|
+
.toggle-label { font-size: 12px; color: var(--ts); }
|
|
296
|
+
.toggle-wrap.overridden .toggle-label { color: var(--warn); }
|
|
297
|
+
.toggle-wrap.dirty .toggle-label { color: var(--warn); }
|
|
298
|
+
|
|
299
|
+
/* ── Color swatch ── */
|
|
300
|
+
.color-wrap {
|
|
301
|
+
display: flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
gap: 6px;
|
|
304
|
+
}
|
|
305
|
+
.color-swatch {
|
|
306
|
+
width: 20px; height: 20px;
|
|
307
|
+
border-radius: 2px;
|
|
308
|
+
border: 1px solid #555;
|
|
309
|
+
cursor: pointer;
|
|
310
|
+
flex-shrink: 0;
|
|
311
|
+
}
|
|
312
|
+
.color-input-hidden {
|
|
313
|
+
position: absolute;
|
|
314
|
+
opacity: 0;
|
|
315
|
+
width: 0; height: 0;
|
|
316
|
+
pointer-events: none;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ── Reset button ── */
|
|
320
|
+
.btn-reset {
|
|
321
|
+
background: none;
|
|
322
|
+
border: none;
|
|
323
|
+
color: var(--ts);
|
|
324
|
+
cursor: pointer;
|
|
325
|
+
font-size: 14px;
|
|
326
|
+
padding: 0 2px;
|
|
327
|
+
line-height: 1;
|
|
328
|
+
opacity: 0;
|
|
329
|
+
pointer-events: none;
|
|
330
|
+
}
|
|
331
|
+
.btn-reset.visible {
|
|
332
|
+
opacity: 1;
|
|
333
|
+
pointer-events: all;
|
|
334
|
+
}
|
|
335
|
+
.btn-reset:hover { color: var(--error); }
|
|
336
|
+
|
|
337
|
+
/* ── Toast ── */
|
|
338
|
+
#toast {
|
|
339
|
+
position: fixed;
|
|
340
|
+
bottom: 20px;
|
|
341
|
+
left: 50%;
|
|
342
|
+
transform: translateX(-50%) translateY(60px);
|
|
343
|
+
background: #1a3a1a;
|
|
344
|
+
border: 1px solid var(--task);
|
|
345
|
+
color: var(--task);
|
|
346
|
+
padding: 6px 18px;
|
|
347
|
+
border-radius: 4px;
|
|
348
|
+
font-size: 12px;
|
|
349
|
+
transition: transform 0.25s ease;
|
|
350
|
+
z-index: 200;
|
|
351
|
+
pointer-events: none;
|
|
352
|
+
}
|
|
353
|
+
#toast.show { transform: translateX(-50%) translateY(0); }
|
|
354
|
+
|
|
355
|
+
/* ── Loading / error ── */
|
|
356
|
+
#loading {
|
|
357
|
+
margin-top: 80px;
|
|
358
|
+
padding: 20px;
|
|
359
|
+
color: var(--ts);
|
|
360
|
+
}
|
|
361
|
+
</style>
|
|
362
|
+
</head>
|
|
363
|
+
<body>
|
|
364
|
+
|
|
365
|
+
<div id="header">
|
|
366
|
+
<span id="header-title">Maestro Config</span>
|
|
367
|
+
<span id="header-spacer"></span>
|
|
368
|
+
<button class="btn" id="btn-discard">Discard</button>
|
|
369
|
+
<button class="btn btn-primary" id="btn-save" disabled>Save</button>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<div id="toolbar">
|
|
373
|
+
<span style="color:var(--ts);font-weight:bold">Save to:</span>
|
|
374
|
+
<label id="label-global">
|
|
375
|
+
<input type="radio" name="target" value="global"> Global
|
|
376
|
+
<span class="target-path" id="path-global"></span>
|
|
377
|
+
</label>
|
|
378
|
+
<label id="label-project">
|
|
379
|
+
<input type="radio" name="target" value="project"> Project
|
|
380
|
+
<span class="target-path" id="path-project"></span>
|
|
381
|
+
</label>
|
|
382
|
+
<span style="flex:1"></span>
|
|
383
|
+
<button class="btn" id="btn-expand-all">Expand All</button>
|
|
384
|
+
<button class="btn" id="btn-collapse-all">Collapse All</button>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<div id="search-row">
|
|
388
|
+
<input id="search-input" type="text" placeholder="Search config keys…" autocomplete="off" spellcheck="false" />
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div id="content">
|
|
392
|
+
<div id="loading">Loading configuration…</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div id="toast">Saved</div>
|
|
396
|
+
|
|
397
|
+
<script>
|
|
398
|
+
(function() {
|
|
399
|
+
'use strict';
|
|
400
|
+
|
|
401
|
+
// ── Field descriptions (shown as hover tooltips) ─────────────────────────────
|
|
402
|
+
const FIELD_DESCRIPTIONS = {
|
|
403
|
+
ai_provider: 'Which AI provider to use for task execution.',
|
|
404
|
+
claude_command: 'Shell command to invoke Claude Code CLI.',
|
|
405
|
+
claude_args: 'Arguments passed to the Claude CLI on every invocation.',
|
|
406
|
+
claude_error_patterns: 'Substrings that indicate a Claude session hit a rate-limit or fatal error. Comma-separated.',
|
|
407
|
+
copilot_command: 'Shell command to invoke the Copilot CLI.',
|
|
408
|
+
copilot_args: 'Arguments passed to the Copilot CLI on every invocation.',
|
|
409
|
+
copilot_error_patterns: 'Substrings that indicate a Copilot session hit an error. Comma-separated.',
|
|
410
|
+
max_iterations: 'Maximum number of AI iterations per task before giving up.',
|
|
411
|
+
max_external_iterations: 'Max iterations for the second (external) review phase.\n0 = auto: max(3, max_iterations / 5).',
|
|
412
|
+
iteration_delay_ms: 'Milliseconds to wait between AI iterations.',
|
|
413
|
+
task_retry_count: 'Number of times to retry a failed task before moving on.',
|
|
414
|
+
finalize_enabled: 'Run a finalize prompt at the end of the plan to clean up and summarize.',
|
|
415
|
+
use_worktree: 'Run each plan in an isolated git worktree.',
|
|
416
|
+
plans_dir: 'Directory where plan markdown files are stored.',
|
|
417
|
+
default_branch: 'Branch used as the base for review diffs.\nEmpty = auto-detect from git.',
|
|
418
|
+
vcs_command: 'Shell command used for version control operations (default: git).',
|
|
419
|
+
push_on_complete: 'Push the branch to the remote after a successful plan run.',
|
|
420
|
+
watch_dirs: 'Extra directories to scan for progress files in the dashboard. Comma-separated.',
|
|
421
|
+
port: 'TCP port for the web dashboard and --config UI.',
|
|
422
|
+
color_task: 'ANSI 24-bit hex color for task-phase output.',
|
|
423
|
+
color_review: 'ANSI 24-bit hex color for review-phase output.',
|
|
424
|
+
color_claude_eval: 'ANSI 24-bit hex color for Claude evaluation messages.',
|
|
425
|
+
color_warn: 'ANSI 24-bit hex color for warning messages.',
|
|
426
|
+
color_error: 'ANSI 24-bit hex color for error messages.',
|
|
427
|
+
color_signal: 'ANSI 24-bit hex color for signal messages (ALL_TASKS_DONE etc.).',
|
|
428
|
+
color_timestamp: 'ANSI 24-bit hex color for timestamps.',
|
|
429
|
+
color_info: 'ANSI 24-bit hex color for general info messages.',
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// ── Section definitions ──────────────────────────────────────────────────────
|
|
433
|
+
const SECTIONS = [
|
|
434
|
+
{
|
|
435
|
+
id: 'ai_provider',
|
|
436
|
+
title: 'AI Provider',
|
|
437
|
+
fields: ['ai_provider'],
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: 'claude',
|
|
441
|
+
title: 'Claude Settings',
|
|
442
|
+
fields: ['claude_command', 'claude_args', 'claude_error_patterns'],
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: 'copilot',
|
|
446
|
+
title: 'Copilot Settings',
|
|
447
|
+
fields: ['copilot_command', 'copilot_args', 'copilot_error_patterns'],
|
|
448
|
+
collapsed: true,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: 'iteration',
|
|
452
|
+
title: 'Iteration Limits',
|
|
453
|
+
fields: ['max_iterations', 'max_external_iterations', 'iteration_delay_ms', 'task_retry_count'],
|
|
454
|
+
collapsed: true,
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: 'finalize',
|
|
458
|
+
title: 'Finalize',
|
|
459
|
+
fields: ['finalize_enabled'],
|
|
460
|
+
collapsed: true,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'git',
|
|
464
|
+
title: 'Git',
|
|
465
|
+
fields: ['use_worktree', 'plans_dir', 'default_branch', 'vcs_command', 'push_on_complete'],
|
|
466
|
+
collapsed: true,
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: 'dashboard',
|
|
470
|
+
title: 'Web Dashboard',
|
|
471
|
+
fields: ['port', 'watch_dirs'],
|
|
472
|
+
collapsed: true,
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
id: 'colors',
|
|
476
|
+
title: 'Colors',
|
|
477
|
+
fields: ['color_task', 'color_review', 'color_claude_eval', 'color_warn', 'color_error', 'color_signal', 'color_timestamp', 'color_info'],
|
|
478
|
+
collapsed: true,
|
|
479
|
+
},
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
const COLOR_FIELDS = new Set([
|
|
483
|
+
'color_task', 'color_review', 'color_claude_eval', 'color_warn',
|
|
484
|
+
'color_error', 'color_signal', 'color_timestamp', 'color_info',
|
|
485
|
+
]);
|
|
486
|
+
|
|
487
|
+
const ARRAY_FIELDS = new Set([
|
|
488
|
+
'claude_error_patterns', 'copilot_error_patterns', 'watch_dirs',
|
|
489
|
+
]);
|
|
490
|
+
|
|
491
|
+
const BOOLEAN_FIELDS = new Set([
|
|
492
|
+
'finalize_enabled', 'use_worktree', 'push_on_complete',
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
const NUMBER_FIELDS = new Set([
|
|
496
|
+
'max_iterations', 'max_external_iterations', 'iteration_delay_ms', 'task_retry_count', 'port',
|
|
497
|
+
]);
|
|
498
|
+
|
|
499
|
+
const SELECT_FIELDS = {
|
|
500
|
+
ai_provider: ['copilot', 'claude'],
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// ── State ────────────────────────────────────────────────────────────────────
|
|
504
|
+
let state = null; // { merged, defaults, globalOverrides, projectOverrides, ... }
|
|
505
|
+
let dirtyValues = {};
|
|
506
|
+
let target = 'global';
|
|
507
|
+
let savedCollapsedState = null;
|
|
508
|
+
|
|
509
|
+
// ── Search / filter ───────────────────────────────────────────────────────────
|
|
510
|
+
function filterConfig(query) {
|
|
511
|
+
const q = query.trim().toLowerCase();
|
|
512
|
+
const noMatch = document.getElementById('no-match');
|
|
513
|
+
|
|
514
|
+
if (!q) {
|
|
515
|
+
// Restore saved collapsed state
|
|
516
|
+
if (savedCollapsedState !== null) {
|
|
517
|
+
SECTIONS.forEach(section => {
|
|
518
|
+
const sec = document.getElementById('section-' + section.id);
|
|
519
|
+
if (!sec) return;
|
|
520
|
+
const wasCollapsed = savedCollapsedState[section.id];
|
|
521
|
+
sec.style.display = '';
|
|
522
|
+
sec.classList.toggle('collapsed', wasCollapsed);
|
|
523
|
+
sec.querySelector('.section-toggle').textContent = wasCollapsed ? '▶' : '▼';
|
|
524
|
+
});
|
|
525
|
+
savedCollapsedState = null;
|
|
526
|
+
}
|
|
527
|
+
document.querySelectorAll('.field-row').forEach(el => { el.style.display = ''; });
|
|
528
|
+
if (noMatch) noMatch.style.display = 'none';
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Save collapsed state once (on first non-empty search)
|
|
533
|
+
if (savedCollapsedState === null) {
|
|
534
|
+
savedCollapsedState = {};
|
|
535
|
+
SECTIONS.forEach(section => {
|
|
536
|
+
const sec = document.getElementById('section-' + section.id);
|
|
537
|
+
savedCollapsedState[section.id] = sec ? sec.classList.contains('collapsed') : false;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let totalMatches = 0;
|
|
542
|
+
|
|
543
|
+
SECTIONS.forEach(section => {
|
|
544
|
+
let sectionMatches = 0;
|
|
545
|
+
section.fields.forEach(key => {
|
|
546
|
+
const row = document.getElementById('row-' + key);
|
|
547
|
+
if (!row) return;
|
|
548
|
+
const desc = (FIELD_DESCRIPTIONS[key] || '').toLowerCase();
|
|
549
|
+
const matches = key.toLowerCase().includes(q) || desc.includes(q);
|
|
550
|
+
row.style.display = matches ? '' : 'none';
|
|
551
|
+
if (matches) sectionMatches++;
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const secEl = document.getElementById('section-' + section.id);
|
|
555
|
+
if (secEl) {
|
|
556
|
+
if (sectionMatches > 0) {
|
|
557
|
+
secEl.style.display = '';
|
|
558
|
+
secEl.classList.remove('collapsed');
|
|
559
|
+
secEl.querySelector('.section-toggle').textContent = '▼';
|
|
560
|
+
totalMatches += sectionMatches;
|
|
561
|
+
} else {
|
|
562
|
+
secEl.style.display = 'none';
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (noMatch) noMatch.style.display = totalMatches === 0 ? '' : 'none';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ── Fetch + render ────────────────────────────────────────────────────────────
|
|
571
|
+
async function loadConfig() {
|
|
572
|
+
const res = await fetch('/config');
|
|
573
|
+
state = await res.json();
|
|
574
|
+
|
|
575
|
+
// Pre-select project if project config exists
|
|
576
|
+
target = state.projectExists ? 'project' : 'global';
|
|
577
|
+
render();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function getDisplayValue(key) {
|
|
581
|
+
if (key in dirtyValues) return dirtyValues[key];
|
|
582
|
+
return state.merged[key];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function isOverridden(key) {
|
|
586
|
+
// A field is "saved as override" if it differs from default in the relevant target file
|
|
587
|
+
const targetOverrides = target === 'project' ? state.projectOverrides : state.globalOverrides;
|
|
588
|
+
return key in targetOverrides;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function isDirty(key) {
|
|
592
|
+
return key in dirtyValues;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function hasDirtyFields() {
|
|
596
|
+
return Object.keys(dirtyValues).length > 0;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── Render ────────────────────────────────────────────────────────────────────
|
|
600
|
+
function render() {
|
|
601
|
+
// Toolbar
|
|
602
|
+
document.getElementById('path-global').textContent = state.globalConfigPath;
|
|
603
|
+
document.getElementById('path-project').textContent = state.projectConfigPath;
|
|
604
|
+
|
|
605
|
+
const radios = document.querySelectorAll('input[name=target]');
|
|
606
|
+
radios.forEach(r => {
|
|
607
|
+
r.checked = r.value === target;
|
|
608
|
+
r.closest('label').classList.toggle('active', r.value === target);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Save button
|
|
612
|
+
document.getElementById('btn-save').disabled = !hasDirtyFields();
|
|
613
|
+
|
|
614
|
+
// Reset search on re-render (e.g. target switch)
|
|
615
|
+
const searchInput = document.getElementById('search-input');
|
|
616
|
+
if (searchInput) searchInput.value = '';
|
|
617
|
+
savedCollapsedState = null;
|
|
618
|
+
|
|
619
|
+
// Content
|
|
620
|
+
const content = document.getElementById('content');
|
|
621
|
+
content.innerHTML = '';
|
|
622
|
+
|
|
623
|
+
const noMatch = document.createElement('div');
|
|
624
|
+
noMatch.id = 'no-match';
|
|
625
|
+
noMatch.textContent = 'No matching config keys.';
|
|
626
|
+
content.appendChild(noMatch);
|
|
627
|
+
|
|
628
|
+
for (const section of SECTIONS) {
|
|
629
|
+
content.appendChild(renderSection(section));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function renderSection(section) {
|
|
634
|
+
const div = document.createElement('div');
|
|
635
|
+
div.className = 'section' + (section.collapsed ? ' collapsed' : '');
|
|
636
|
+
div.id = 'section-' + section.id;
|
|
637
|
+
|
|
638
|
+
const hasDirty = section.fields.some(f => isDirty(f));
|
|
639
|
+
|
|
640
|
+
const header = document.createElement('div');
|
|
641
|
+
header.className = 'section-header';
|
|
642
|
+
header.innerHTML = `
|
|
643
|
+
<span class="section-toggle">${div.classList.contains('collapsed') ? '▶' : '▼'}</span>
|
|
644
|
+
<span>${section.title}</span>
|
|
645
|
+
${hasDirty ? '<span class="section-dirty">•</span>' : ''}
|
|
646
|
+
`;
|
|
647
|
+
header.addEventListener('click', () => {
|
|
648
|
+
div.classList.toggle('collapsed');
|
|
649
|
+
header.querySelector('.section-toggle').textContent =
|
|
650
|
+
div.classList.contains('collapsed') ? '▶' : '▼';
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const body = document.createElement('div');
|
|
654
|
+
body.className = 'section-body';
|
|
655
|
+
|
|
656
|
+
for (const key of section.fields) {
|
|
657
|
+
body.appendChild(renderField(key));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
div.appendChild(header);
|
|
661
|
+
div.appendChild(body);
|
|
662
|
+
return div;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderField(key) {
|
|
666
|
+
const row = document.createElement('div');
|
|
667
|
+
row.className = 'field-row';
|
|
668
|
+
row.id = 'row-' + key;
|
|
669
|
+
|
|
670
|
+
const label = document.createElement('div');
|
|
671
|
+
label.className = 'field-label';
|
|
672
|
+
label.textContent = key;
|
|
673
|
+
if (FIELD_DESCRIPTIONS[key]) label.dataset.desc = FIELD_DESCRIPTIONS[key];
|
|
674
|
+
|
|
675
|
+
const control = document.createElement('div');
|
|
676
|
+
control.className = 'field-control';
|
|
677
|
+
|
|
678
|
+
const resetBtn = document.createElement('button');
|
|
679
|
+
resetBtn.className = 'btn-reset' + (isDirty(key) || isOverridden(key) ? ' visible' : '');
|
|
680
|
+
resetBtn.textContent = '×';
|
|
681
|
+
resetBtn.title = 'Reset to default';
|
|
682
|
+
resetBtn.addEventListener('click', () => {
|
|
683
|
+
delete dirtyValues[key];
|
|
684
|
+
dirtyValues[key] = state.defaults[key];
|
|
685
|
+
// If already at default, treat as "explicitly reset"
|
|
686
|
+
updateRow(key);
|
|
687
|
+
updateSaveButton();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
if (key in SELECT_FIELDS) {
|
|
691
|
+
const sel = buildSelect(key, SELECT_FIELDS[key]);
|
|
692
|
+
control.appendChild(sel);
|
|
693
|
+
} else if (COLOR_FIELDS.has(key)) {
|
|
694
|
+
control.appendChild(buildColorInput(key));
|
|
695
|
+
} else if (BOOLEAN_FIELDS.has(key)) {
|
|
696
|
+
control.appendChild(buildToggle(key));
|
|
697
|
+
} else if (NUMBER_FIELDS.has(key)) {
|
|
698
|
+
control.appendChild(buildNumberInput(key));
|
|
699
|
+
} else if (ARRAY_FIELDS.has(key)) {
|
|
700
|
+
control.appendChild(buildTextInput(key, 'comma-separated'));
|
|
701
|
+
} else {
|
|
702
|
+
control.appendChild(buildTextInput(key, ''));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
row.appendChild(label);
|
|
706
|
+
row.appendChild(control);
|
|
707
|
+
row.appendChild(resetBtn);
|
|
708
|
+
return row;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function classesForInput(key) {
|
|
712
|
+
const classes = ['field-input'];
|
|
713
|
+
if (isDirty(key)) classes.push('dirty');
|
|
714
|
+
else if (isOverridden(key)) classes.push('overridden');
|
|
715
|
+
return classes.join(' ');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function buildTextInput(key, hint) {
|
|
719
|
+
const wrap = document.createElement('div');
|
|
720
|
+
wrap.style.cssText = 'display:flex;align-items:center;gap:6px;width:100%';
|
|
721
|
+
|
|
722
|
+
const val = getDisplayValue(key);
|
|
723
|
+
const displayVal = Array.isArray(val) ? val.join(', ') : String(val ?? '');
|
|
724
|
+
|
|
725
|
+
const input = document.createElement('input');
|
|
726
|
+
input.type = 'text';
|
|
727
|
+
input.className = classesForInput(key);
|
|
728
|
+
input.value = displayVal;
|
|
729
|
+
input.addEventListener('input', () => {
|
|
730
|
+
const newVal = ARRAY_FIELDS.has(key)
|
|
731
|
+
? input.value.split(',').map(s => s.trim()).filter(Boolean)
|
|
732
|
+
: input.value;
|
|
733
|
+
onFieldChange(key, newVal);
|
|
734
|
+
input.className = classesForInput(key);
|
|
735
|
+
updateResetBtn(key);
|
|
736
|
+
});
|
|
737
|
+
wrap.appendChild(input);
|
|
738
|
+
|
|
739
|
+
if (hint) {
|
|
740
|
+
const h = document.createElement('span');
|
|
741
|
+
h.className = 'field-hint';
|
|
742
|
+
h.textContent = hint;
|
|
743
|
+
wrap.appendChild(h);
|
|
744
|
+
}
|
|
745
|
+
return wrap;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function buildNumberInput(key) {
|
|
749
|
+
const val = getDisplayValue(key);
|
|
750
|
+
const input = document.createElement('input');
|
|
751
|
+
input.type = 'number';
|
|
752
|
+
input.className = classesForInput(key);
|
|
753
|
+
input.min = '0';
|
|
754
|
+
input.value = String(val ?? 0);
|
|
755
|
+
input.style.width = '100px';
|
|
756
|
+
input.addEventListener('input', () => {
|
|
757
|
+
onFieldChange(key, parseInt(input.value) || 0);
|
|
758
|
+
input.className = classesForInput(key);
|
|
759
|
+
updateResetBtn(key);
|
|
760
|
+
});
|
|
761
|
+
return input;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function buildSelect(key, options) {
|
|
765
|
+
const val = getDisplayValue(key);
|
|
766
|
+
const sel = document.createElement('select');
|
|
767
|
+
sel.className = 'field-select' + (isDirty(key) ? ' dirty' : isOverridden(key) ? ' overridden' : '');
|
|
768
|
+
for (const opt of options) {
|
|
769
|
+
const o = document.createElement('option');
|
|
770
|
+
o.value = opt;
|
|
771
|
+
o.textContent = opt;
|
|
772
|
+
if (opt === val) o.selected = true;
|
|
773
|
+
sel.appendChild(o);
|
|
774
|
+
}
|
|
775
|
+
sel.addEventListener('change', () => {
|
|
776
|
+
onFieldChange(key, sel.value);
|
|
777
|
+
sel.className = 'field-select' + (isDirty(key) ? ' dirty' : isOverridden(key) ? ' overridden' : '');
|
|
778
|
+
updateResetBtn(key);
|
|
779
|
+
});
|
|
780
|
+
return sel;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function buildToggle(key) {
|
|
784
|
+
const val = getDisplayValue(key);
|
|
785
|
+
const wrap = document.createElement('div');
|
|
786
|
+
wrap.className = 'toggle-wrap' + (isDirty(key) ? ' dirty' : isOverridden(key) ? ' overridden' : '');
|
|
787
|
+
|
|
788
|
+
const lbl = document.createElement('label');
|
|
789
|
+
lbl.className = 'toggle';
|
|
790
|
+
|
|
791
|
+
const inp = document.createElement('input');
|
|
792
|
+
inp.type = 'checkbox';
|
|
793
|
+
inp.checked = Boolean(val);
|
|
794
|
+
|
|
795
|
+
const slider = document.createElement('span');
|
|
796
|
+
slider.className = 'toggle-slider';
|
|
797
|
+
|
|
798
|
+
lbl.appendChild(inp);
|
|
799
|
+
lbl.appendChild(slider);
|
|
800
|
+
|
|
801
|
+
const txt = document.createElement('span');
|
|
802
|
+
txt.className = 'toggle-label';
|
|
803
|
+
txt.textContent = val ? 'true' : 'false';
|
|
804
|
+
|
|
805
|
+
inp.addEventListener('change', () => {
|
|
806
|
+
txt.textContent = inp.checked ? 'true' : 'false';
|
|
807
|
+
onFieldChange(key, inp.checked);
|
|
808
|
+
wrap.className = 'toggle-wrap' + (isDirty(key) ? ' dirty' : isOverridden(key) ? ' overridden' : '');
|
|
809
|
+
updateResetBtn(key);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
wrap.appendChild(lbl);
|
|
813
|
+
wrap.appendChild(txt);
|
|
814
|
+
return wrap;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function buildColorInput(key) {
|
|
818
|
+
const val = String(getDisplayValue(key) ?? '#000000');
|
|
819
|
+
|
|
820
|
+
const wrap = document.createElement('div');
|
|
821
|
+
wrap.className = 'color-wrap';
|
|
822
|
+
|
|
823
|
+
const swatch = document.createElement('div');
|
|
824
|
+
swatch.className = 'color-swatch';
|
|
825
|
+
swatch.style.background = isValidHex(val) ? val : '#333';
|
|
826
|
+
|
|
827
|
+
const input = document.createElement('input');
|
|
828
|
+
input.type = 'text';
|
|
829
|
+
input.className = classesForInput(key);
|
|
830
|
+
input.value = val;
|
|
831
|
+
input.style.width = '110px';
|
|
832
|
+
|
|
833
|
+
input.addEventListener('input', () => {
|
|
834
|
+
const v = input.value.trim();
|
|
835
|
+
if (isValidHex(v)) {
|
|
836
|
+
swatch.style.background = v;
|
|
837
|
+
input.classList.remove('invalid');
|
|
838
|
+
onFieldChange(key, v);
|
|
839
|
+
} else {
|
|
840
|
+
input.classList.add('invalid');
|
|
841
|
+
}
|
|
842
|
+
if (isDirty(key)) input.classList.add('dirty');
|
|
843
|
+
else if (isOverridden(key)) input.classList.add('overridden');
|
|
844
|
+
updateResetBtn(key);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
swatch.addEventListener('click', () => {
|
|
848
|
+
// Open native color picker via a hidden input
|
|
849
|
+
const picker = document.createElement('input');
|
|
850
|
+
picker.type = 'color';
|
|
851
|
+
picker.value = isValidHex(val) ? val : '#000000';
|
|
852
|
+
picker.style.cssText = 'position:absolute;opacity:0;width:0;height:0';
|
|
853
|
+
document.body.appendChild(picker);
|
|
854
|
+
picker.addEventListener('input', () => {
|
|
855
|
+
input.value = picker.value;
|
|
856
|
+
swatch.style.background = picker.value;
|
|
857
|
+
onFieldChange(key, picker.value);
|
|
858
|
+
input.className = classesForInput(key);
|
|
859
|
+
updateResetBtn(key);
|
|
860
|
+
});
|
|
861
|
+
picker.addEventListener('change', () => { picker.remove(); });
|
|
862
|
+
picker.click();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
wrap.appendChild(swatch);
|
|
866
|
+
wrap.appendChild(input);
|
|
867
|
+
return wrap;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function isValidHex(val) {
|
|
871
|
+
return /^#[0-9a-fA-F]{6}$/.test(val);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ── Field change helpers ─────────────────────────────────────────────────────
|
|
875
|
+
function valuesEqual(a, b) {
|
|
876
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
877
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
878
|
+
}
|
|
879
|
+
return a === b;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function onFieldChange(key, newVal) {
|
|
883
|
+
if (valuesEqual(newVal, state.merged[key])) {
|
|
884
|
+
// Reverted to the original saved value — field is no longer dirty
|
|
885
|
+
delete dirtyValues[key];
|
|
886
|
+
} else {
|
|
887
|
+
dirtyValues[key] = newVal;
|
|
888
|
+
}
|
|
889
|
+
updateSaveButton();
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function updateSaveButton() {
|
|
893
|
+
document.getElementById('btn-save').disabled = !hasDirtyFields();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function updateResetBtn(key) {
|
|
897
|
+
const row = document.getElementById('row-' + key);
|
|
898
|
+
if (!row) return;
|
|
899
|
+
const btn = row.querySelector('.btn-reset');
|
|
900
|
+
if (!btn) return;
|
|
901
|
+
btn.classList.toggle('visible', isDirty(key) || isOverridden(key));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function updateRow(key) {
|
|
905
|
+
// Re-render just this row in place
|
|
906
|
+
const row = document.getElementById('row-' + key);
|
|
907
|
+
if (!row) return;
|
|
908
|
+
const parent = row.parentNode;
|
|
909
|
+
const newRow = renderField(key);
|
|
910
|
+
parent.replaceChild(newRow, row);
|
|
911
|
+
|
|
912
|
+
// Update section dirty dot
|
|
913
|
+
const sectionDef = SECTIONS.find(s => s.fields.includes(key));
|
|
914
|
+
if (sectionDef) {
|
|
915
|
+
const sec = document.getElementById('section-' + sectionDef.id);
|
|
916
|
+
if (sec) {
|
|
917
|
+
const hasDirty = sectionDef.fields.some(f => isDirty(f));
|
|
918
|
+
let dot = sec.querySelector('.section-dirty');
|
|
919
|
+
if (hasDirty && !dot) {
|
|
920
|
+
dot = document.createElement('span');
|
|
921
|
+
dot.className = 'section-dirty';
|
|
922
|
+
dot.textContent = '•';
|
|
923
|
+
sec.querySelector('.section-header').appendChild(dot);
|
|
924
|
+
} else if (!hasDirty && dot) {
|
|
925
|
+
dot.remove();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── Save / Discard ────────────────────────────────────────────────────────────
|
|
932
|
+
document.getElementById('btn-save').addEventListener('click', async () => {
|
|
933
|
+
// Build the payload: start from merged, apply dirty overrides
|
|
934
|
+
const merged = Object.assign({}, state.merged);
|
|
935
|
+
for (const [k, v] of Object.entries(dirtyValues)) {
|
|
936
|
+
merged[k] = v;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
const res = await fetch('/config', {
|
|
941
|
+
method: 'POST',
|
|
942
|
+
headers: { 'Content-Type': 'application/json' },
|
|
943
|
+
body: JSON.stringify({ overrides: merged, target }),
|
|
944
|
+
});
|
|
945
|
+
const data = await res.json();
|
|
946
|
+
if (!data.ok) throw new Error(data.error || 'Save failed');
|
|
947
|
+
|
|
948
|
+
// Reload ground truth
|
|
949
|
+
dirtyValues = {};
|
|
950
|
+
const r2 = await fetch('/config');
|
|
951
|
+
state = await r2.json();
|
|
952
|
+
render();
|
|
953
|
+
showToast('Saved');
|
|
954
|
+
} catch (err) {
|
|
955
|
+
showToast('Error: ' + err.message, true);
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
document.getElementById('btn-discard').addEventListener('click', () => {
|
|
960
|
+
dirtyValues = {};
|
|
961
|
+
render();
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Target radio change
|
|
965
|
+
document.querySelectorAll('input[name=target]').forEach(r => {
|
|
966
|
+
r.addEventListener('change', () => {
|
|
967
|
+
target = r.value;
|
|
968
|
+
render();
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
document.getElementById('btn-expand-all').addEventListener('click', () => {
|
|
973
|
+
document.querySelectorAll('.section').forEach(sec => {
|
|
974
|
+
sec.classList.remove('collapsed');
|
|
975
|
+
sec.querySelector('.section-toggle').textContent = '▼';
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
document.getElementById('btn-collapse-all').addEventListener('click', () => {
|
|
979
|
+
document.querySelectorAll('.section').forEach(sec => {
|
|
980
|
+
sec.classList.add('collapsed');
|
|
981
|
+
sec.querySelector('.section-toggle').textContent = '▶';
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
document.getElementById('search-input').addEventListener('input', e => {
|
|
986
|
+
filterConfig(e.target.value);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// ── Toast ─────────────────────────────────────────────────────────────────────
|
|
990
|
+
function showToast(msg, isError) {
|
|
991
|
+
const t = document.getElementById('toast');
|
|
992
|
+
t.textContent = msg;
|
|
993
|
+
t.style.borderColor = isError ? 'var(--error)' : 'var(--task)';
|
|
994
|
+
t.style.color = isError ? 'var(--error)' : 'var(--task)';
|
|
995
|
+
t.style.background = isError ? '#3a1a1a' : '#1a3a1a';
|
|
996
|
+
t.classList.add('show');
|
|
997
|
+
setTimeout(() => t.classList.remove('show'), 1500);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── Boot ──────────────────────────────────────────────────────────────────────
|
|
1001
|
+
loadConfig().catch(err => {
|
|
1002
|
+
document.getElementById('content').innerHTML =
|
|
1003
|
+
`<div style="color:var(--error);padding:20px">Failed to load config: ${err.message}</div>`;
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// SSE connection — server detects tab close when this drops
|
|
1007
|
+
new EventSource('/events');
|
|
1008
|
+
|
|
1009
|
+
})();
|
|
1010
|
+
</script>
|
|
1011
|
+
</body>
|
|
1012
|
+
</html>
|