@oberion/wildo 0.6.4

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.
@@ -0,0 +1,739 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>wildo — AI agents at your service</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #f8f9fb;
11
+ --bg-panel: #ffffff;
12
+ --bg-card: #ffffff;
13
+ --bg-hover: #f0f2f5;
14
+ --border: #e2e6ea;
15
+ --text: #1a1d21;
16
+ --text-dim: #5c6370;
17
+ --text-muted: #9ca3ad;
18
+
19
+ --architect: #0891b2;
20
+ --developer: #16a34a;
21
+ --tester: #4f6df5;
22
+ --reviewer: #e5700a;
23
+ --reporter: #8b5cf6;
24
+
25
+ --pass: #16a34a;
26
+ --fail: #dc2626;
27
+ --accent: #f0d78c;
28
+ --accent-bg: rgba(240, 215, 140, 0.15);
29
+ --active: #f0d78c;
30
+ }
31
+
32
+ * { margin: 0; padding: 0; box-sizing: border-box; }
33
+
34
+ body {
35
+ font-family: 'DM Sans', system-ui, sans-serif;
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ height: 100vh;
39
+ overflow: hidden;
40
+ display: grid;
41
+ grid-template-columns: 360px 1fr;
42
+ grid-template-rows: 56px 1fr;
43
+ }
44
+
45
+ header {
46
+ grid-column: 1 / -1;
47
+ background: var(--bg-panel);
48
+ border-bottom: 1px solid var(--border);
49
+ display: flex;
50
+ align-items: center;
51
+ padding: 0 24px;
52
+ gap: 16px;
53
+ }
54
+
55
+ header h1 { font-size: 15px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text); }
56
+ header h1 span { color: var(--accent); font-weight: 700; }
57
+
58
+ #status-badge { font-size: 12px; font-family: 'JetBrains Mono', monospace; padding: 3px 10px; border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border); color: var(--text-dim); }
59
+ #status-badge.running { color: var(--active); border-color: var(--active); animation: pulse-border 2s ease-in-out infinite; }
60
+ #status-badge.done { color: var(--pass); border-color: var(--pass); }
61
+ #status-badge.stopped { color: var(--fail); border-color: var(--fail); }
62
+ @keyframes pulse-border { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
63
+ .header-spacer { flex: 1; }
64
+
65
+ #btn-lang { font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text-dim); cursor: pointer; letter-spacing: 0.04em; transition: all 0.15s; }
66
+ #btn-lang:hover { border-color: var(--accent); color: var(--accent); }
67
+ #controls { display: flex; gap: 8px; }
68
+ #controls button { font-family: 'DM Sans', sans-serif; font-size: 13px; font-weight: 500; padding: 6px 16px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text-dim); cursor: pointer; transition: all 0.15s; }
69
+ #controls button:hover { background: var(--bg-hover); color: var(--text); }
70
+ #controls button:disabled { opacity: 0.3; cursor: not-allowed; }
71
+ #controls button.danger { border-color: var(--fail); color: var(--fail); }
72
+ #controls button.danger:hover { background: rgba(244, 112, 103, 0.1); }
73
+
74
+ aside { background: var(--bg-panel); border-right: 1px solid var(--border); padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; box-shadow: 2px 0 8px rgba(0,0,0,0.04); }
75
+ .field-group { display: flex; flex-direction: column; gap: 6px; }
76
+ .field-group label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); }
77
+ .field-group input, .field-group select, .field-group textarea { font-family: 'JetBrains Mono', monospace; font-size: 13px; padding: 10px 12px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; color: var(--text); outline: none; transition: border-color 0.15s; }
78
+ .field-group input:focus, .field-group select:focus, .field-group textarea:focus { border-color: var(--accent); }
79
+ .field-group textarea { resize: vertical; min-height: 120px; line-height: 1.5; }
80
+ .field-group select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%237d8590'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px; }
81
+ .row { display: flex; gap: 12px; }
82
+ .row .field-group { flex: 1; }
83
+
84
+ #btn-start { font-family: 'DM Sans', sans-serif; font-size: 14px; font-weight: 700; padding: 12px; border-radius: 8px; border: none; background: var(--active); color: #3a3520; cursor: pointer; transition: all 0.15s; letter-spacing: 0.02em; }
85
+ #btn-start:hover { filter: brightness(1.1); transform: translateY(-1px); }
86
+ #btn-start:active { transform: translateY(0); }
87
+ #btn-start:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
88
+
89
+ main { overflow-y: auto; padding: 24px 32px; scroll-behavior: smooth; }
90
+ #timeline { max-width: 900px; margin: 0 auto; display: flex; flex-direction: column; gap: 2px; }
91
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 60vh; color: var(--text-muted); gap: 12px; }
92
+ .empty-state svg { opacity: 0.3; }
93
+ .empty-state p { font-size: 14px; font-style: italic; }
94
+
95
+ .phase-header { margin: 24px 0 12px; padding: 8px 0; border-bottom: 2px solid var(--border); display: flex; align-items: center; gap: 12px; animation: slide-in 0.3s ease; }
96
+ .phase-header:first-child { margin-top: 0; }
97
+ .phase-header h2 { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); }
98
+ .phase-header .phase-line { flex: 1; height: 1px; background: var(--border); }
99
+
100
+ .timeline-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; animation: slide-in 0.3s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
101
+ @keyframes slide-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
102
+ .card-header { display: flex; align-items: center; gap: 10px; padding: 10px 16px; cursor: pointer; user-select: none; }
103
+ .card-header:hover { background: var(--bg-hover); }
104
+ .role-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
105
+ .role-tag { font-size: 12px; font-weight: 700; font-family: 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: 0.04em; }
106
+ .card-label { font-size: 13px; color: var(--text-dim); flex: 1; }
107
+ .card-time { font-size: 11px; font-family: 'JetBrains Mono', monospace; color: var(--text-muted); }
108
+ .card-verdict { font-size: 11px; font-weight: 700; font-family: 'JetBrains Mono', monospace; padding: 2px 8px; border-radius: 4px; }
109
+ .card-verdict.pass { color: var(--pass); background: rgba(22, 163, 74, 0.1); }
110
+ .card-verdict.fail { color: var(--fail); background: rgba(220, 38, 38, 0.1); }
111
+ .card-body { display: none; padding: 0 16px 14px; border-top: 1px solid var(--border); }
112
+ .card-body.open { display: block; padding-top: 14px; }
113
+ .card-body pre { font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.6; color: var(--text-dim); white-space: pre-wrap; word-break: break-word; }
114
+
115
+ .role-architect .role-dot { background: var(--architect); } .role-architect .role-tag { color: var(--architect); } .role-architect { border-left: 3px solid var(--architect); }
116
+ .role-developer .role-dot { background: var(--developer); } .role-developer .role-tag { color: var(--developer); } .role-developer { border-left: 3px solid var(--developer); }
117
+ .role-tester .role-dot { background: var(--tester); } .role-tester .role-tag { color: var(--tester); } .role-tester { border-left: 3px solid var(--tester); }
118
+ .role-reviewer .role-dot { background: var(--reviewer); } .role-reviewer .role-tag { color: var(--reviewer); } .role-reviewer { border-left: 3px solid var(--reviewer); }
119
+ .role-reporter .role-dot { background: var(--reporter); } .role-reporter .role-tag { color: var(--reporter); } .role-reporter { border-left: 3px solid var(--reporter); }
120
+
121
+ .tool-status { margin: 2px 0; padding: 6px 16px; display: flex; align-items: center; gap: 8px; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: var(--text-muted); animation: fade-in 0.2s ease; }
122
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
123
+ .tool-status .tool-icon { width: 12px; height: 12px; border: 1.5px solid var(--text-muted); border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
124
+
125
+ .timeline-card.streaming .card-body { display: block; padding-top: 14px; }
126
+ .timeline-card.streaming .card-body pre { border-left: 2px solid var(--active); padding-left: 12px; }
127
+ .streaming-dots { display: inline; color: var(--active); font-family: 'JetBrains Mono', monospace; font-size: 13px; }
128
+ .streaming-dots::after { content: ''; animation: dots 1.2s steps(4, end) infinite; }
129
+ @keyframes dots { 0% { content: '.'; } 33% { content: '..'; } 66% { content: '...'; } }
130
+
131
+ .agent-active { margin: 8px 0; padding: 10px 16px; display: flex; align-items: center; gap: 10px; color: var(--text-muted); font-size: 13px; animation: fade-pulse 1.5s ease-in-out infinite; }
132
+ @keyframes fade-pulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
133
+ .agent-active .spinner { width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--text-dim); border-radius: 50%; animation: spin 0.8s linear infinite; }
134
+ @keyframes spin { to { transform: rotate(360deg); } }
135
+
136
+ ::-webkit-scrollbar { width: 6px; }
137
+ ::-webkit-scrollbar-track { background: transparent; }
138
+ ::-webkit-scrollbar-thumb { background: #d0d5db; border-radius: 3px; }
139
+ ::-webkit-scrollbar-thumb:hover { background: #b0b8c4; }
140
+
141
+ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.3); backdrop-filter: blur(4px); z-index: 1000; align-items: center; justify-content: center; }
142
+ .modal-overlay.active { display: flex; }
143
+ .modal { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 16px; padding: 24px; max-width: 520px; width: 90%; box-shadow: 0 8px 32px rgba(0,0,0,0.12); }
144
+ .modal h3 { font-size: 15px; font-weight: 700; margin-bottom: 16px; color: var(--text); }
145
+ .modal pre { font-family: 'JetBrains Mono', monospace; font-size: 12px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; padding: 12px; white-space: pre-wrap; color: var(--text-dim); margin-bottom: 12px; }
146
+ .modal .modal-warning { color: var(--active); font-size: 13px; margin-bottom: 16px; line-height: 1.5; }
147
+ .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
148
+ .modal-actions button { padding: 8px 20px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); font-size: 13px; cursor: pointer; }
149
+ .modal-actions button:hover { background: var(--bg-hover); }
150
+ .modal-actions button.primary { background: var(--active); color: #3a3520; border-color: var(--active); font-weight: 600; }
151
+ .modal-actions button.primary:hover { opacity: 0.85; }
152
+
153
+ .cleanup-item { background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; padding: 12px; margin-bottom: 10px; }
154
+ .cleanup-item .cleanup-meta { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-dim); margin-bottom: 8px; line-height: 1.5; }
155
+ .cleanup-item .cleanup-meta strong { color: var(--text); }
156
+ .cleanup-item .cleanup-choices { display: flex; gap: 12px; margin-top: 8px; }
157
+ .cleanup-item .cleanup-choices label { font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; color: var(--text-dim); }
158
+ .cleanup-item .cleanup-choices input[type="radio"]:checked + span { color: var(--text); font-weight: 600; }
159
+ .cleanup-item .cleanup-choices .discard-label input[type="radio"]:checked + span { color: var(--fail); }
160
+
161
+ /* ── Setup page ──────────────────────────── */
162
+ #setup-overlay { position: fixed; inset: 0; background: #f0f2f5; z-index: 2000; display: flex; align-items: center; justify-content: center; }
163
+ #setup-overlay.hidden { display: none; }
164
+ .setup-box { background: #ffffff; border: 1px solid var(--border); border-radius: 16px; padding: 32px; max-width: 520px; width: 90%; box-shadow: 0 4px 24px rgba(0,0,0,0.08); }
165
+ .setup-box h2 { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
166
+ .setup-box h2 span { color: var(--accent); }
167
+ .setup-box .setup-subtitle { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; line-height: 1.5; }
168
+ .setup-section { margin-bottom: 20px; }
169
+ .setup-section h3 { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
170
+ .setup-section h3 .detect-tag { font-size: 10px; font-weight: 500; padding: 2px 8px; border-radius: 4px; text-transform: none; letter-spacing: 0; }
171
+ .detect-tag.found { background: rgba(63, 185, 80, 0.15); color: var(--pass); }
172
+ .detect-tag.missing { background: rgba(244, 112, 103, 0.1); color: var(--text-muted); }
173
+ .setup-options { display: flex; flex-direction: column; gap: 8px; }
174
+ .setup-option { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; transition: border-color 0.15s; }
175
+ .setup-option:hover { border-color: var(--text-muted); }
176
+ .setup-option input[type="radio"] { margin-top: 2px; accent-color: var(--active); }
177
+ .setup-option .option-text { flex: 1; }
178
+ .setup-option .option-label { font-size: 13px; font-weight: 500; }
179
+ .setup-option .option-desc { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
180
+ .setup-option .key-input { margin-top: 8px; width: 100%; font-family: 'JetBrains Mono', monospace; font-size: 12px; padding: 8px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); outline: none; }
181
+ .setup-option .key-input:focus { border-color: var(--accent); }
182
+ .setup-option .key-input:disabled { opacity: 0.3; }
183
+ #btn-setup-save { font-family: 'DM Sans', sans-serif; font-size: 14px; font-weight: 700; padding: 12px; width: 100%; border-radius: 8px; border: none; background: var(--active); color: #3a3520; cursor: pointer; transition: all 0.15s; letter-spacing: 0.02em; margin-top: 8px; }
184
+ #btn-setup-save:hover { filter: brightness(1.1); }
185
+ #btn-setup-save:disabled { opacity: 0.4; cursor: not-allowed; }
186
+ </style>
187
+ </head>
188
+ <body>
189
+
190
+ <div id="setup-overlay" class="hidden">
191
+ <div class="setup-box">
192
+ <h2><span>wildo</span> — <span data-i18n="setup_title">first-time setup</span></h2>
193
+ <p class="setup-subtitle" data-i18n="setup_subtitle">wildo uses Claude (architect / reviewer / reporter) and Codex (developer / tester). Configure how to authenticate with each provider.</p>
194
+
195
+ <div class="setup-section">
196
+ <h3>Language / 语言</h3>
197
+ <div class="setup-options">
198
+ <label class="setup-option" style="cursor:pointer">
199
+ <input type="radio" name="setup-lang" value="en" checked>
200
+ <div class="option-text"><span class="option-label">English</span></div>
201
+ </label>
202
+ <label class="setup-option" style="cursor:pointer">
203
+ <input type="radio" name="setup-lang" value="zh">
204
+ <div class="option-text"><span class="option-label">中文</span></div>
205
+ </label>
206
+ </div>
207
+ </div>
208
+
209
+ <div class="setup-section" id="setup-claude">
210
+ <h3>Claude <span class="detect-tag" id="claude-detect"></span></h3>
211
+ <div class="setup-options" id="claude-options"></div>
212
+ </div>
213
+
214
+ <div class="setup-section" id="setup-codex">
215
+ <h3>Codex <span class="detect-tag" id="codex-detect"></span></h3>
216
+ <div class="setup-options" id="codex-options"></div>
217
+ </div>
218
+
219
+ <button id="btn-setup-save" data-i18n="setup_save">Save Configuration</button>
220
+ </div>
221
+ </div>
222
+
223
+ <header>
224
+ <h1>wildo <span data-i18n="header_pipeline">Pipeline</span></h1>
225
+ <div id="status-badge" data-i18n="status_idle">idle</div>
226
+ <div class="header-spacer"></div>
227
+ <button id="btn-lang" title="Switch language / 切换语言">EN</button>
228
+ <div id="controls">
229
+ <button id="btn-stop" disabled data-i18n="btn_stop">Stop</button>
230
+ <button id="btn-abort" class="danger" disabled data-i18n="btn_abort">Abort</button>
231
+ </div>
232
+ </header>
233
+
234
+ <aside>
235
+ <div class="field-group">
236
+ <label data-i18n="label_requirement">Requirement</label>
237
+ <textarea id="input-req" data-i18n-placeholder="placeholder_requirement" placeholder="Describe the requirement, or paste a file path..."></textarea>
238
+ </div>
239
+ <div class="field-group">
240
+ <label data-i18n="label_project_dir">Project Directory</label>
241
+ <input id="input-dir" type="text" placeholder="/path/to/project" value=".">
242
+ </div>
243
+ <div class="row">
244
+ <div class="field-group">
245
+ <label data-i18n="label_mode">Mode</label>
246
+ <select id="input-mode">
247
+ <option value="greenfield" data-i18n="mode_greenfield">Greenfield</option>
248
+ <option value="feature" data-i18n="mode_feature">Feature</option>
249
+ <option value="bugfix" data-i18n="mode_bugfix">Bugfix</option>
250
+ </select>
251
+ </div>
252
+ <div class="field-group">
253
+ <label data-i18n="label_language">Language</label>
254
+ <select id="input-lang">
255
+ <option value="en">English</option>
256
+ <option value="zh">中文</option>
257
+ <option value="ja">日本語</option>
258
+ <option value="ko">한국어</option>
259
+ <option value="es">Español</option>
260
+ <option value="fr">Français</option>
261
+ <option value="de">Deutsch</option>
262
+ </select>
263
+ </div>
264
+ </div>
265
+ <button id="btn-start" data-i18n="btn_start">Start Pipeline</button>
266
+ </aside>
267
+
268
+ <main>
269
+ <div id="timeline">
270
+ <div class="empty-state" id="empty-state">
271
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
272
+ <path d="M12 2L2 7l10 5 10-5-10-5z"/>
273
+ <path d="M2 17l10 5 10-5"/>
274
+ <path d="M2 12l10 5 10-5"/>
275
+ </svg>
276
+ <p data-i18n="empty_state">Configure and start a pipeline to begin</p>
277
+ </div>
278
+ </div>
279
+ </main>
280
+
281
+ <div class="modal-overlay" id="confirm-modal">
282
+ <div class="modal">
283
+ <h3 data-i18n="confirm_title">Confirm Pipeline Configuration</h3>
284
+ <pre id="confirm-summary"></pre>
285
+ <div id="confirm-warning" class="modal-warning"></div>
286
+ <div class="modal-actions">
287
+ <button id="confirm-cancel" data-i18n="btn_cancel">Cancel</button>
288
+ <button id="confirm-ok" class="primary" data-i18n="btn_proceed">Proceed</button>
289
+ </div>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="modal-overlay" id="cleanup-modal">
294
+ <div class="modal">
295
+ <h3 data-i18n="cleanup_title">Unfinished Runs from Previous Sessions</h3>
296
+ <div class="modal-warning" data-i18n="cleanup_desc">
297
+ The following runs didn't complete successfully. Choose Keep or Discard for each.
298
+ </div>
299
+ <div id="cleanup-list"></div>
300
+ <div class="modal-actions">
301
+ <button id="cleanup-submit" class="primary" data-i18n="btn_continue">Continue</button>
302
+ </div>
303
+ </div>
304
+ </div>
305
+
306
+ <script>
307
+ // ── i18n (fetched from /api/i18n, shared with terminal) ──
308
+ var uiLang = 'en';
309
+ var uiDict = {}; // populated from server
310
+ function ut(key, vars) {
311
+ var s = uiDict['ui_' + key] || uiDict[key] || key;
312
+ if (vars) { for (var k in vars) { s = s.replace('{' + k + '}', vars[k]); } }
313
+ return s;
314
+ }
315
+ function applyI18n() {
316
+ document.querySelectorAll('[data-i18n]').forEach(function(el) {
317
+ el.textContent = ut(el.getAttribute('data-i18n'));
318
+ });
319
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
320
+ el.placeholder = ut(el.getAttribute('data-i18n-placeholder'));
321
+ });
322
+ }
323
+ function setUiLang(lang) {
324
+ uiLang = lang;
325
+ document.documentElement.setAttribute('data-lang', uiLang);
326
+ fetch('/api/i18n?lang=' + lang).then(function(r){return r.json()}).then(function(d) {
327
+ uiDict = d;
328
+ applyI18n();
329
+ }).catch(function(){});
330
+ }
331
+ // Load default dict immediately
332
+ fetch('/api/i18n?lang=en').then(function(r){return r.json()}).then(function(d){ uiDict = d; }).catch(function(){});
333
+
334
+ // ── DOM refs ──────────────────────────────────
335
+ var timeline = document.getElementById('timeline');
336
+ var emptyState = document.getElementById('empty-state');
337
+ var btnStart = document.getElementById('btn-start');
338
+ var btnStop = document.getElementById('btn-stop');
339
+ var btnAbort = document.getElementById('btn-abort');
340
+ var statusBadge = document.getElementById('status-badge');
341
+ var inputReq = document.getElementById('input-req');
342
+ var inputDir = document.getElementById('input-dir');
343
+ var inputMode = document.getElementById('input-mode');
344
+ var inputLang = document.getElementById('input-lang');
345
+ var ws = null;
346
+ var running = false;
347
+
348
+ // Language switch triggers UI i18n
349
+ var btnLang = document.getElementById('btn-lang');
350
+ inputLang.addEventListener('change', function() { setUiLang(inputLang.value); btnLang.textContent = inputLang.value.toUpperCase(); });
351
+ btnLang.addEventListener('click', function() {
352
+ var next = (uiLang === 'zh') ? 'en' : 'zh';
353
+ inputLang.value = next;
354
+ btnLang.textContent = next.toUpperCase();
355
+ setUiLang(next);
356
+ });
357
+
358
+ // Pre-fill from CLI --ui config
359
+ fetch('/api/config').then(function(r){return r.json()}).then(function(c){
360
+ if(c.requirement)inputReq.value=c.requirement;
361
+ if(c.workingDir)inputDir.value=c.workingDir;
362
+ if(c.mode)inputMode.value=c.mode;
363
+ if(c.lang){ inputLang.value=c.lang; btnLang.textContent=c.lang.toUpperCase(); setUiLang(c.lang); }
364
+ }).catch(function(){});
365
+
366
+ // Confirm modal
367
+ var confirmModal=document.getElementById('confirm-modal');
368
+ var confirmSummary=document.getElementById('confirm-summary');
369
+ var confirmWarning=document.getElementById('confirm-warning');
370
+ document.getElementById('confirm-ok').addEventListener('click',function(){
371
+ confirmModal.classList.remove('active');send('confirm',{confirmed:true});
372
+ });
373
+ document.getElementById('confirm-cancel').addEventListener('click',function(){
374
+ confirmModal.classList.remove('active');send('confirm',{confirmed:false});
375
+ setRunning(false);setStatus('idle','idle');
376
+ });
377
+
378
+ // Cleanup modal
379
+ var cleanupModal=document.getElementById('cleanup-modal');
380
+ var cleanupList=document.getElementById('cleanup-list');
381
+ document.getElementById('cleanup-submit').addEventListener('click',function(){
382
+ var decisions={};
383
+ cleanupList.querySelectorAll('.cleanup-item').forEach(function(item){
384
+ var runId=item.dataset.runId;
385
+ var choice=item.querySelector('input[type="radio"]:checked').value;
386
+ decisions[runId]=choice;
387
+ });
388
+ cleanupModal.classList.remove('active');send('cleanup',{decisions:decisions});
389
+ });
390
+
391
+ function showCleanupModal(crashed){
392
+ cleanupList.textContent='';
393
+ crashed.forEach(function(run){
394
+ var item=document.createElement('div');item.className='cleanup-item';item.dataset.runId=run.run_id;
395
+ var meta=document.createElement('div');meta.className='cleanup-meta';
396
+ // Build meta content safely using DOM
397
+ var fields=[['Run',run.run_id],['Status',run.status||'?'],['Mode',run.mode||'?'],['Branch',run.branch||'?'],['Requirement',(run.requirement||'').slice(0,100)]];
398
+ fields.forEach(function(f,i){
399
+ var b=document.createElement('strong');b.textContent=f[0]+': ';
400
+ meta.appendChild(b);meta.appendChild(document.createTextNode(f[1]));
401
+ if(i<fields.length-1)meta.appendChild(document.createElement('br'));
402
+ });
403
+ item.appendChild(meta);
404
+ var choices=document.createElement('div');choices.className='cleanup-choices';
405
+ var keepLabel=document.createElement('label');
406
+ var keepRadio=document.createElement('input');keepRadio.type='radio';keepRadio.name='choice-'+run.run_id;keepRadio.value='keep';keepRadio.checked=true;
407
+ var keepSpan=document.createElement('span');keepSpan.textContent=ut('cleanup_keep');
408
+ keepLabel.appendChild(keepRadio);keepLabel.appendChild(keepSpan);choices.appendChild(keepLabel);
409
+ var discardLabel=document.createElement('label');discardLabel.className='discard-label';
410
+ var discardRadio=document.createElement('input');discardRadio.type='radio';discardRadio.name='choice-'+run.run_id;discardRadio.value='discard';
411
+ var discardSpan=document.createElement('span');discardSpan.textContent=ut('cleanup_discard');
412
+ discardLabel.appendChild(discardRadio);discardLabel.appendChild(discardSpan);choices.appendChild(discardLabel);
413
+ item.appendChild(choices);cleanupList.appendChild(item);
414
+ });
415
+ cleanupModal.classList.add('active');
416
+ }
417
+
418
+ // Streaming state
419
+ var streamingCards={};
420
+ var toolStatusEl=null;
421
+
422
+ function connect(){
423
+ var proto=location.protocol==='https:'?'wss:':'ws:';
424
+ ws=new WebSocket(proto+'//'+location.host);
425
+ ws.onmessage=function(e){handleEvent(JSON.parse(e.data))};
426
+ ws.onclose=function(){setTimeout(connect,2000)};
427
+ ws.onerror=function(){ws.close()};
428
+ }
429
+
430
+ function send(action,data){
431
+ if(ws&&ws.readyState===WebSocket.OPEN)ws.send(JSON.stringify(Object.assign({action:action},data||{})));
432
+ }
433
+
434
+ function setRunning(v){
435
+ running=v;btnStart.disabled=v;btnStop.disabled=!v;btnAbort.disabled=!v;
436
+ inputReq.disabled=v;inputDir.disabled=v;inputMode.disabled=v;inputLang.disabled=v;
437
+ }
438
+
439
+ function setStatus(key,cls){statusBadge.textContent=ut('status_'+key);statusBadge.className=cls||'';statusBadge.setAttribute('data-i18n','status_'+key);}
440
+ function removeToolStatus(){if(toolStatusEl){toolStatusEl.remove();toolStatusEl=null;}}
441
+ function scrollToBottom(){var m=document.querySelector('main');requestAnimationFrame(function(){m.scrollTop=m.scrollHeight})}
442
+
443
+ function getRoleClass(role){
444
+ var r=(role||'').toLowerCase();
445
+ if(r.indexOf('architect')>=0)return'role-architect';if(r.indexOf('developer')>=0)return'role-developer';
446
+ if(r.indexOf('tester')>=0)return'role-tester';if(r.indexOf('reviewer')>=0)return'role-reviewer';
447
+ if(r.indexOf('reporter')>=0)return'role-reporter';return'';
448
+ }
449
+ function getRoleVar(role){
450
+ var r=(role||'').toLowerCase();
451
+ if(r.indexOf('architect')>=0)return'architect';if(r.indexOf('developer')>=0)return'developer';
452
+ if(r.indexOf('tester')>=0)return'tester';if(r.indexOf('reviewer')>=0)return'reviewer';
453
+ if(r.indexOf('reporter')>=0)return'reporter';return'text-dim';
454
+ }
455
+ function nowStr(){return new Date().toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'})}
456
+
457
+ function addPhaseHeader(name){
458
+ removeToolStatus();
459
+ var el=document.createElement('div');el.className='phase-header';
460
+ var h2=document.createElement('h2');h2.textContent=name;
461
+ var line=document.createElement('div');line.className='phase-line';
462
+ el.appendChild(h2);el.appendChild(line);timeline.appendChild(el);scrollToBottom();
463
+ }
464
+
465
+ function getOrCreateStreamingCard(role){
466
+ if(streamingCards[role])return streamingCards[role];
467
+ if(emptyState&&emptyState.parentNode)emptyState.remove();
468
+ var card=document.createElement('div');card.className='timeline-card streaming '+getRoleClass(role);
469
+ var header=document.createElement('div');header.className='card-header';
470
+ header.addEventListener('click',function(){this.nextElementSibling.classList.toggle('open')});
471
+ var dot=document.createElement('div');dot.className='role-dot';header.appendChild(dot);
472
+ var tag=document.createElement('span');tag.className='role-tag';tag.textContent=role;header.appendChild(tag);
473
+ var lbl=document.createElement('span');lbl.className='card-label';lbl.textContent=ut('ev_streaming');header.appendChild(lbl);
474
+ var timeEl=document.createElement('span');timeEl.className='card-time';timeEl.textContent=nowStr();header.appendChild(timeEl);
475
+ card.appendChild(header);
476
+ var body=document.createElement('div');body.className='card-body open';
477
+ var pre=document.createElement('pre');
478
+ var dots=document.createElement('span');dots.className='streaming-dots';
479
+ body.appendChild(pre);body.appendChild(dots);card.appendChild(body);
480
+ timeline.appendChild(card);scrollToBottom();
481
+ var state={card:card,pre:pre,dots:dots,label:lbl,timeEl:timeEl};
482
+ streamingCards[role]=state;return state;
483
+ }
484
+
485
+ function appendStreamText(role,text){var s=getOrCreateStreamingCard(role);s.pre.textContent+=text;scrollToBottom()}
486
+
487
+ function finalizeStreamingCard(role,label,verdict){
488
+ var s=streamingCards[role];if(!s)return;
489
+ s.dots.remove();s.card.classList.remove('streaming');
490
+ if(label)s.label.textContent=label;else s.label.textContent=ut('ev_output');
491
+ s.timeEl.textContent=nowStr();
492
+ if(verdict){
493
+ var vEl=document.createElement('span');
494
+ vEl.className='card-verdict '+(verdict==='PASS'?'pass':'fail');vEl.textContent=verdict;
495
+ s.card.querySelector('.card-header').insertBefore(vEl,s.timeEl);
496
+ }
497
+ s.card.querySelector('.card-body').classList.remove('open');
498
+ delete streamingCards[role];
499
+ }
500
+
501
+ function addCard(role,label,content,verdict){
502
+ removeToolStatus();if(emptyState&&emptyState.parentNode)emptyState.remove();
503
+ var card=document.createElement('div');card.className='timeline-card '+getRoleClass(role);
504
+ var header=document.createElement('div');header.className='card-header';
505
+ header.addEventListener('click',function(){this.nextElementSibling.classList.toggle('open')});
506
+ var dot=document.createElement('div');dot.className='role-dot';header.appendChild(dot);
507
+ var tag=document.createElement('span');tag.className='role-tag';tag.textContent=role;header.appendChild(tag);
508
+ var lbl=document.createElement('span');lbl.className='card-label';lbl.textContent=label;header.appendChild(lbl);
509
+ if(verdict){var vEl=document.createElement('span');vEl.className='card-verdict '+(verdict==='PASS'?'pass':'fail');vEl.textContent=verdict;header.appendChild(vEl)}
510
+ var timeEl=document.createElement('span');timeEl.className='card-time';timeEl.textContent=nowStr();header.appendChild(timeEl);
511
+ card.appendChild(header);
512
+ var body=document.createElement('div');body.className='card-body';
513
+ var pre=document.createElement('pre');pre.textContent=content||'';body.appendChild(pre);card.appendChild(body);
514
+ timeline.appendChild(card);scrollToBottom();
515
+ }
516
+
517
+ function showToolStatus(role,description){
518
+ removeToolStatus();toolStatusEl=document.createElement('div');toolStatusEl.className='tool-status';
519
+ var icon=document.createElement('div');icon.className='tool-icon';toolStatusEl.appendChild(icon);
520
+ var roleTag=document.createElement('span');roleTag.className='role-tag';roleTag.textContent=role;
521
+ roleTag.style.color='var(--'+getRoleVar(role)+')';roleTag.style.fontSize='11px';toolStatusEl.appendChild(roleTag);
522
+ var desc=document.createElement('span');desc.textContent=description;toolStatusEl.appendChild(desc);
523
+ timeline.appendChild(toolStatusEl);scrollToBottom();
524
+ }
525
+
526
+ function handleEvent(event){
527
+ var data=event.data||{};var role=data.role||event.agent||'';
528
+ switch(event.type){
529
+ case'cleanup_request':showCleanupModal(data.crashed||event.crashed||[]);break;
530
+ case'confirm_request':
531
+ confirmSummary.textContent=data.summary||event.summary||'';
532
+ var w=data.warning||event.warning||'';confirmWarning.textContent=w;
533
+ confirmWarning.style.display=w?'block':'none';confirmModal.classList.add('active');break;
534
+ case'pipeline_done':
535
+ removeToolStatus();Object.keys(streamingCards).forEach(function(r){finalizeStreamingCard(r,ut('ev_output'),null)});
536
+ setRunning(false);setStatus('done','done');
537
+ addCard('orchestrator',ut('ev_complete'),ut('ev_report')+': '+(data.report||event.report||'')+'\n'+ut('ev_branch')+': '+(data.branch||event.branch||''),null);break;
538
+ case'pipeline_stopped':
539
+ removeToolStatus();Object.keys(streamingCards).forEach(function(r){finalizeStreamingCard(r,ut('ev_interrupted'),null)});
540
+ setRunning(false);setStatus('stopped','stopped');
541
+ addCard('orchestrator',ut('ev_stopped'),ut('ev_partial'),null);break;
542
+ case'pipeline_paused':
543
+ removeToolStatus();setRunning(false);setStatus('paused','stopped');
544
+ addCard('orchestrator',ut('ev_paused',{phase:data.phase||event.phase||'?'}),data.diagnosis||event.diagnosis||'',null);break;
545
+ case'pipeline_error':case'error':
546
+ addCard('orchestrator',ut('ev_error'),data.message||event.message||'Unknown error',null);break;
547
+ case'timeline':
548
+ if(data.phase){if(emptyState&&emptyState.parentNode)emptyState.remove();setRunning(true);setStatus('running','running');addPhaseHeader(data.phase);}break;
549
+ case'agent_start':removeToolStatus();break;
550
+ case'agent_done':
551
+ removeToolStatus();
552
+ if(role&&streamingCards[role]&&streamingCards[role].dots.parentNode)streamingCards[role].dots.remove();break;
553
+ case'content_block':
554
+ var bt=data.type||'';
555
+ if(bt==='text'){removeToolStatus();appendStreamText(role||data.role,data.text||'')}
556
+ else if(bt==='tool_status'){showToolStatus(role||data.role,data.description||'')}
557
+ else if(bt==='tool_done'){removeToolStatus()}
558
+ else if(bt==='agent_output'){
559
+ removeToolStatus();var verdict=null;var label=data.label||'';
560
+ if(label.indexOf('PASS')>=0)verdict='PASS';else if(label.indexOf('FAIL')>=0)verdict='FAIL';
561
+ var r2=role||data.role;
562
+ if(streamingCards[r2])finalizeStreamingCard(r2,label,verdict);
563
+ else addCard(r2,label,data.content||'',verdict);
564
+ }break;
565
+ }
566
+ }
567
+
568
+ btnStart.addEventListener('click',function(){
569
+ var req=inputReq.value.trim();if(!req){inputReq.focus();return}
570
+ timeline.textContent='';streamingCards={};
571
+ send('start',{requirement:req,working_dir:inputDir.value.trim()||'.',mode:inputMode.value,lang:inputLang.value});
572
+ });
573
+ btnStop.addEventListener('click',function(){send('stop')});
574
+ btnAbort.addEventListener('click',function(){if(confirm(ut('abort_confirm')))send('abort')});
575
+
576
+ // ── Setup flow ──────────────────────────────────
577
+ var setupOverlay = document.getElementById('setup-overlay');
578
+
579
+ function buildProviderOptions(container, detectTag, provider, sources) {
580
+ container.textContent = '';
581
+ var src = sources[provider];
582
+ var hasAny = src.hasCli || src.hasEnvKey;
583
+ detectTag.textContent = hasAny ? ut('setup_detected') : ut('setup_no_auth');
584
+ detectTag.className = 'detect-tag ' + (hasAny ? 'found' : 'missing');
585
+
586
+ var idx = 0;
587
+
588
+ if (src.hasCli) {
589
+ var provName = provider === 'claude' ? 'Claude Code' : 'Codex';
590
+ var opt = createOption(provider, idx++, 'subscription',
591
+ ut('setup_use_sub', {provider: provName}),
592
+ ut('setup_sub_hint'), null);
593
+ container.appendChild(opt);
594
+ }
595
+
596
+ if (src.hasEnvKey) {
597
+ var opt2 = createOption(provider, idx++, 'env_key',
598
+ ut('setup_use_env'),
599
+ ut('setup_use_env_hint', {key: src.envKey}), null);
600
+ container.appendChild(opt2);
601
+ }
602
+
603
+ var opt3 = createOption(provider, idx++, 'new_key',
604
+ ut('setup_new_key'),
605
+ provider === 'claude' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY',
606
+ true);
607
+ container.appendChild(opt3);
608
+
609
+ // Check first option by default
610
+ var first = container.querySelector('input[type="radio"]');
611
+ if (first) first.checked = true;
612
+ }
613
+
614
+ function createOption(provider, idx, value, label, desc, showInput) {
615
+ var div = document.createElement('div');
616
+ div.className = 'setup-option';
617
+
618
+ var radio = document.createElement('input');
619
+ radio.type = 'radio';
620
+ radio.name = provider + '-auth';
621
+ radio.value = value;
622
+ radio.id = provider + '-opt-' + idx;
623
+ div.appendChild(radio);
624
+
625
+ var textDiv = document.createElement('div');
626
+ textDiv.className = 'option-text';
627
+
628
+ var lbl = document.createElement('label');
629
+ lbl.className = 'option-label';
630
+ lbl.setAttribute('for', radio.id);
631
+ lbl.textContent = label;
632
+ textDiv.appendChild(lbl);
633
+
634
+ if (desc) {
635
+ var d = document.createElement('div');
636
+ d.className = 'option-desc';
637
+ d.textContent = desc;
638
+ textDiv.appendChild(d);
639
+ }
640
+
641
+ if (showInput) {
642
+ var inp = document.createElement('input');
643
+ inp.type = 'password';
644
+ inp.className = 'key-input';
645
+ inp.placeholder = 'sk-...';
646
+ inp.dataset.provider = provider;
647
+ inp.disabled = true;
648
+ textDiv.appendChild(inp);
649
+
650
+ radio.addEventListener('change', function() {
651
+ inp.disabled = false;
652
+ inp.focus();
653
+ });
654
+ }
655
+
656
+ // Disable key input when other options selected
657
+ radio.addEventListener('change', function() {
658
+ var inputs = div.parentElement.querySelectorAll('.key-input');
659
+ inputs.forEach(function(i) {
660
+ if (!div.contains(i)) i.disabled = true;
661
+ });
662
+ });
663
+
664
+ div.appendChild(textDiv);
665
+
666
+ // Click anywhere on option to select radio
667
+ div.addEventListener('click', function(e) {
668
+ if (e.target.tagName !== 'INPUT' || e.target.type !== 'text') {
669
+ radio.checked = true;
670
+ radio.dispatchEvent(new Event('change'));
671
+ }
672
+ });
673
+
674
+ return div;
675
+ }
676
+
677
+ function getProviderAuth(provider) {
678
+ var selected = document.querySelector('input[name="' + provider + '-auth"]:checked');
679
+ if (!selected) return { authMode: 'subscription' };
680
+
681
+ if (selected.value === 'subscription') return { authMode: 'subscription' };
682
+ if (selected.value === 'env_key') return { authMode: 'api_key' };
683
+ if (selected.value === 'new_key') {
684
+ var inp = selected.closest('.setup-option').querySelector('.key-input');
685
+ var key = inp ? inp.value.trim() : '';
686
+ return key ? { authMode: 'api_key', apiKey: key } : { authMode: 'api_key' };
687
+ }
688
+ return { authMode: 'subscription' };
689
+ }
690
+
691
+ document.getElementById('btn-setup-save').addEventListener('click', function() {
692
+ var auth = { claude: getProviderAuth('claude'), codex: getProviderAuth('codex') };
693
+ var langRadio = document.querySelector('input[name="setup-lang"]:checked');
694
+ var lang = langRadio ? langRadio.value : 'en';
695
+ inputLang.value = lang;
696
+ btnLang.textContent = lang.toUpperCase();
697
+ setUiLang(lang);
698
+ fetch('/api/setup/save', {
699
+ method: 'POST',
700
+ headers: { 'Content-Type': 'application/json' },
701
+ body: JSON.stringify({ auth: auth, lang: lang })
702
+ }).then(function(r) { return r.json() }).then(function(d) {
703
+ if (d.ok) setupOverlay.classList.add('hidden');
704
+ }).catch(function(e) { alert('Failed to save: ' + e) });
705
+ });
706
+
707
+ // Setup language radios also trigger UI i18n + rebuild provider options
708
+ var _setupSources = null;
709
+ document.querySelectorAll('input[name="setup-lang"]').forEach(function(radio) {
710
+ radio.addEventListener('change', function() {
711
+ setUiLang(radio.value);
712
+ inputLang.value = radio.value;
713
+ if (_setupSources) {
714
+ buildProviderOptions(document.getElementById('claude-options'), document.getElementById('claude-detect'), 'claude', _setupSources);
715
+ buildProviderOptions(document.getElementById('codex-options'), document.getElementById('codex-detect'), 'codex', _setupSources);
716
+ }
717
+ });
718
+ });
719
+
720
+ // Check setup status on load
721
+ fetch('/api/setup/status').then(function(r) { return r.json() }).then(function(d) {
722
+ if (!d.configured) {
723
+ _setupSources = d.sources;
724
+ setupOverlay.classList.remove('hidden');
725
+ buildProviderOptions(
726
+ document.getElementById('claude-options'),
727
+ document.getElementById('claude-detect'),
728
+ 'claude', d.sources);
729
+ buildProviderOptions(
730
+ document.getElementById('codex-options'),
731
+ document.getElementById('codex-detect'),
732
+ 'codex', d.sources);
733
+ }
734
+ }).catch(function() {});
735
+
736
+ connect();
737
+ </script>
738
+ </body>
739
+ </html>