File size: 26,847 Bytes
9057700
6f0baaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef15e72
6f0baaa
 
 
9057700
6f0baaa
 
 
 
 
9057700
 
 
 
 
 
 
 
 
 
 
 
 
 
6f0baaa
 
 
 
 
 
05d169b
 
6f0baaa
 
 
05d169b
6f0baaa
05d169b
6f0baaa
 
 
 
 
 
 
 
 
05d169b
 
 
 
 
 
 
 
 
 
 
 
 
dd6c84a
 
e7d4595
05d169b
 
 
6f0baaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec1ded
 
 
 
47a24c7
6f0baaa
 
 
 
 
 
 
 
 
ef15e72
 
 
 
 
 
6f0baaa
 
 
 
 
 
 
 
5232c15
6f0baaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4407241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f0baaa
 
 
 
 
 
 
 
7376ac8
 
 
 
 
 
 
 
 
6f6633d
 
 
 
 
 
 
 
 
 
 
 
dd6c84a
e7d4595
4ed7180
dd6c84a
4ed7180
 
 
 
 
 
 
 
dd6c84a
 
e7d4595
 
 
 
 
 
 
 
 
 
 
 
 
 
4ed7180
e7d4595
 
 
 
 
 
 
 
 
 
9e03908
 
 
 
 
 
 
 
 
493a8d2
 
b6522ba
 
493a8d2
550affb
 
51e716f
 
 
 
550affb
9e03908
 
 
 
 
 
b6522ba
 
 
 
 
 
 
 
 
 
 
 
 
 
0d13723
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51e716f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d13723
 
 
 
 
 
 
 
 
 
 
cf06504
 
 
 
 
 
 
 
 
 
c3360ca
 
 
 
 
 
0d13723
 
c3360ca
 
 
5232c15
 
 
 
 
d109328
 
 
 
 
 
 
 
5232c15
 
6f6633d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7376ac8
 
 
 
 
 
6f0baaa
 
 
 
 
 
 
 
06eb337
 
 
7376ac8
06eb337
 
 
6f0baaa
 
 
 
 
 
9057700
4407241
6f0baaa
7376ac8
6f6633d
 
 
 
 
 
 
dd6c84a
e7d4595
9e03908
b6522ba
0d13723
9e03908
51e716f
0d13723
cf06504
 
5232c15
c3360ca
 
e7d4595
6f0baaa
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
const state = { manifest: null, node: null, selected: null, casefile: null, activeFile: 'src/agent.ts', files: {
  'agent.ts': `import { ChatMessage, ModelAdapter } from './types';
import { LocalModelRouter } from './router';

export class AgentRuntime {
  constructor(private readonly router: LocalModelRouter) {}

  async review(task: string) {
    const lanes = this.router.plan(task);
    return this.router.runBounded(lanes);
  }
}`,
  'router.ts': `export class LocalModelRouter {
  constructor(private readonly registry: ModelRegistry) {}

  plan(task: string) {
    return ['research', 'build', 'verify'].map(role => ({ role, task }));
  }

  async runBounded(lanes: Lane[]) {
    // Each lane receives only the context it needs and returns a reviewable artifact.
    return this.registry.executeSequentially(lanes, { maxTurns: 4, approval: true });
  }
}`,
  'models.ts': `export type ModelRole = 'research' | 'build' | 'verify';

export interface ModelManifest {
  id: string;
  status: 'experimental' | 'ready' | 'pending';
  roles: ModelRole[];
  runtime: 'llama.cpp' | 'ollama' | 'openai-compatible';
  endpoint: string;
}`,
  'types.ts': `export interface Lane {
  role: 'research' | 'build' | 'verify';
  task: string;
  claims?: string[];
  patch?: string;
  confidence?: number;
}`,
  'README.md': '# AIDE\n\nLocal-first development with explicit model lanes and reviewable patches.'
}};

const $ = selector => document.querySelector(selector);
const esc = value => String(value).replace(/[&<>"']/g, char => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[char]));
const patchValid = value => /^diff --git\s+\S+\s+\S+/m.test(value) && /^---\s+/m.test(value) && /^\+\+\+\s+/m.test(value) && !/^```/m.test(value);

function openFile(name) {
  const text = state.files[name] || state.files['agent.ts'];
  state.activeFile = `src/${name}`;
  $('#code').textContent = text;
  $('#line-numbers').textContent = text.split('\n').map((_, index) => index + 1).join('\n');
  document.querySelectorAll('[data-file]').forEach(button => button.classList.toggle('active', button.dataset.file === name));
}

async function saveFile() {
  try {
    const response = await fetch('http://127.0.0.1:4777/api/file/write', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ path: state.activeFile, content: $('#code').textContent, approved: true })
    });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error || 'file write rejected');
    appendLog('WORKSPACE', `${result.path} saved atomically after explicit approval.`);
  } catch (error) {
    appendLog('WORKSPACE', `Save blocked: ${error.message}. Start the local daemon and open a trusted workspace.`, 'warning');
  }
}

function renderModels() {
  const list = $('#model-list');
  const lanes = $('#lane-grid');
  list.innerHTML = '';
  lanes.innerHTML = '';
  state.manifest.models.forEach(model => {
    const installed = JSON.parse(localStorage.getItem(`aide.model.${model.id}`) || 'null');
    const visibleStatus = installed ? 'imported' : model.status;
    const roles = model.roles.join(' / ');
    const item = document.createElement('button');
    item.className = 'model-item';
    item.innerHTML = `<span class="status ${visibleStatus}"></span><span>${esc(model.name)}</span><small>${esc(model.format)} | ${esc(visibleStatus)} | ${esc(roles)}</small><strong class="pack-action">${installed ? 'REPLACE' : 'IMPORT'}</strong>`;
    item.onclick = () => selectModel(model);
    item.querySelector('.pack-action').onclick = event => { event.stopPropagation(); importModel(model); };
    list.appendChild(item);
    const lane = document.createElement('button');
    lane.className = `lane ${model.status}`;
    lane.innerHTML = `<b>${esc(model.lane.toUpperCase())}</b><span>${esc(model.name)}</span><small>${esc(roles)}</small>`;
    lane.onclick = () => selectModel(model);
    lanes.appendChild(lane);
  });
}

async function importModel(model) {
  const input = $('#model-file-input');
  input.value = '';
  input.onchange = async () => {
    const file = input.files?.[0];
    if (!file) return;
    const digest = await crypto.subtle.digest('SHA-256', await file.arrayBuffer());
    const hash = [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('');
    const record = { id: model.id, name: file.name, bytes: file.size, sha256: hash, imported_at: new Date().toISOString(), source_repo: model.source_revision };
    localStorage.setItem(`aide.model.${model.id}`, JSON.stringify(record));
    appendLog('MODEL PACK', `${model.name} imported locally. SHA-256: ${hash}. Runtime attachment is still required before inference.`);
    renderModels();
    selectModel(model);
};

let communityStore = { projects: [], issues: [], discussions: [], marketplace: [] };
  input.click();
}

function selectModel(model) {
  state.selected = model;
  $('#selected-model').textContent = model.name;
  $('#selected-detail').textContent = `${model.format} | ${model.status} | ${model.description}`;
  $('#runtime-name').textContent = model.runtime;
}

function appendLog(role, text, type = '') {
  const log = $('#collab-log');
  if (log.querySelector('.empty-state')) log.innerHTML = '';
  const entry = document.createElement('div');
  entry.className = `log-entry ${type}`;
  entry.innerHTML = `<b>${esc(role)}</b><p>${esc(text)}</p>`;
  log.appendChild(entry);
  log.scrollTop = log.scrollHeight;
}

async function requestLocal(model, messages) {
  const response = await fetch(`${model.endpoint}/chat/completions`, {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: model.model, messages, temperature: model.temperature, max_tokens: model.max_tokens })
  });
  if (!response.ok) throw new Error(`runtime returned HTTP ${response.status}`);
  const data = await response.json();
  return data.choices?.[0]?.message?.content || 'Runtime returned no content.';
}

async function runReview() {
  const task = $('#input').value.trim() || 'Review the local provider router for safer fallback behavior.';
  const runnable = model => ['ready', 'experimental'].includes(model.status) && model.endpoint?.startsWith('http');
  const ready = state.manifest.models.filter(runnable);
  const research = state.manifest.models.find(model => model.id === 'fsi-tinyliquid-training' && model.status === 'ready') || state.manifest.models.find(model => model.lane === 'research' && runnable(model)) || ready[0];
  const builder = state.manifest.models.find(model => model.lane === 'build' && runnable(model)) || ready[0];
  const verifier = state.manifest.models.find(model => model.lane === 'verify') || builder;
  $('#review-button').disabled = true;
  $('#review-button').textContent = 'RUNNING...';
  $('#collab-log').innerHTML = '';
  appendLog('COORDINATOR', `Task accepted with a four-turn maximum: ${task}`);
  try {
    if (research.status === 'pending') throw new Error('Research lane is not configured. Add a local endpoint or checkpoint first.');
    const findings = await requestLocal(research, [{ role: 'system', content: research.system_prompt }, { role: 'user', content: task }]);
    appendLog('RESEARCH', findings);
    if (builder.status === 'pending') throw new Error('Coding lane is not configured. Install a coding checkpoint before applying patches.');
    let patch = await requestLocal(builder, [{ role: 'system', content: builder.system_prompt }, { role: 'user', content: `Task: ${task}\nResearch findings:\n${findings}\nReturn a unified diff only.` }]);
    if (!patchValid(patch)) {
      appendLog('REPAIR', 'Builder output was not a valid unified diff. Requesting one bounded repair.', 'warning');
      patch = await requestLocal(builder, [{ role: 'system', content: 'Convert the raw response into one valid unfenced unified diff. Preserve intent. Return only the diff. Do not invent files or claim tests passed.' }, { role: 'user', content: `Task: ${task}\nRaw response:\n${patch}` }]);
    }
    if (!patchValid(patch)) throw new Error('Patch repair failed structural validation; no files changed.');
    appendLog('BUILD', patch, 'patch');
    if (verifier.status === 'pending') throw new Error('Verifier lane is not configured.');
    const verdict = await requestLocal(verifier, [{ role: 'system', content: verifier.system_prompt }, { role: 'user', content: `Task: ${task}\nProposed patch:\n${patch}\nReturn APPROVE, REJECT, or NEEDS-EVIDENCE with reasons.` }]);
    appendLog('VERIFY', verdict, verdict.includes('APPROVE') ? 'approved' : 'warning');
    appendLog('COORDINATOR', 'No files were changed. Review and approve the patch before applying it.');
  } catch (error) {
    appendLog('STOPPED', error.message, 'warning');
  } finally {
    await saveReplay('review-complete');
    $('#review-button').disabled = false;
    $('#review-button').textContent = 'START BOUNDED REVIEW';
  }
}

async function testRuntime() {
  const model = state.selected || state.manifest.models.find(item => item.status !== 'pending');
  if (!model) return appendLog('RUNTIME', 'No configured local model endpoint.', 'warning');
  appendLog('RUNTIME', `Testing ${model.name} at ${model.endpoint}...`);
  try {
    const response = await fetch(`${model.endpoint}/models`);
    appendLog('RUNTIME', response.ok ? 'Local runtime reachable. Model remains subject to capability checks.' : `Runtime returned HTTP ${response.status}.`, response.ok ? 'approved' : 'warning');
  } catch (error) {
    appendLog('RUNTIME', 'Offline shell is healthy, but no local HTTP runtime is reachable. This is expected until the adapter is started.', 'warning');
  }
}

async function startRuntime() {
  const model = state.selected;
  if (!model) return appendLog('RUNTIME', 'Select a model pack first.', 'warning');
  try {
    const response = await fetch('http://127.0.0.1:4777/api/models/start', {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: model.id })
    });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error || 'runtime start failed');
    appendLog('RUNTIME', `${model.name} is starting at ${result.endpoint}.`);
    setTimeout(testRuntime, 1500);
  } catch (error) {
    appendLog('RUNTIME', `Local daemon unavailable: ${error.message}. Start daemon/server.mjs first.`, 'warning');
  }
}

function sendChat() {
  const input = $('#input');
  const value = input.value.trim();
  if (!value) return;
  $('#chat').insertAdjacentHTML('beforeend', `<p><b>YOU</b><br>${esc(value)}</p><p class="assistant"><b>AIDE</b><br>Use START BOUNDED REVIEW to send this task through the research, build, and verify lanes. No files will change automatically.</p>`);
  input.value = '';
}

function localNodeId() {
  let id = localStorage.getItem('aide.node.id');
  if (!id) {
    id = crypto.randomUUID();
    localStorage.setItem('aide.node.id', id);
  }
  return id;
}

function renderCasefile() {
  const status = $('#case-status');
  const list = $('#evidence-list');
  if (!state.casefile) {
    status.textContent = 'No case open.';
    list.textContent = '';
    return;
  }
  status.innerHTML = `<b>CASE ${esc(state.casefile.id)}</b><br>${state.casefile.evidence.length} evidence item(s)<br>Local-only provenance ledger`;
  list.innerHTML = state.casefile.evidence.map(item => `<div style="border-left:2px solid #58c7ff;padding-left:5px;margin-top:6px"><b>${esc(item.name)}</b><br>${esc(item.bytes)} bytes | SHA-256 ${esc(item.hash.slice(0, 12))}...<br>dates: ${esc(item.dates.join(', ') || 'none detected')}</div>`).join('');
}

function renderCommunity(tab = 'projects') {
  const entries = communityStore[tab] || [];
  $('#community-feed').innerHTML = `<div style="color:#72ff9e;margin-bottom:5px">LOCAL CACHE / SYNC OFF</div>${entries.map((entry, index) => `<div style="border-left:2px solid #b277ff;padding-left:5px;margin:5px 0"><b>${esc(entry.title)}</b><br>${esc(entry.detail)}<br><span style="color:#718994">${esc(entry.boundary || entry.status || 'local')}</span> <button data-community-remove="${index}" style="color:#ff6d82;background:none;border:0;font:9px ui-monospace,monospace;cursor:pointer">REMOVE</button></div>`).join('')}`;
  document.querySelectorAll('[data-community-tab]').forEach(button => button.style.color = button.dataset.communityTab === tab ? '#72ff9e' : '#718994');
  document.querySelectorAll('[data-community-remove]').forEach(button => button.onclick = () => removeCommunityItem(tab, Number(button.dataset.communityRemove)));
}

async function removeCommunityItem(type, index) {
  try {
    await fetch('http://127.0.0.1:4777/api/community/items', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, index }) });
    await loadCommunity();
  } catch (error) { appendLog('COMMUNITY', `Could not remove local item: ${error.message}`, 'warning'); }
}

async function loadCommunity() {
  try {
    const response = await fetch('http://127.0.0.1:4777/api/community');
    if (response.ok) communityStore = await response.json();
  } catch { /* offline cache remains empty until the daemon is running */ }
  renderCommunity();
}

async function addCommunityIssue() {
  const title = $('#community-title').value.trim();
  if (!title) return;
  try {
    const response = await fetch('http://127.0.0.1:4777/api/community/items', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ type: $('#community-type').value, item: { title, detail: 'Created locally from AIDE.' } })
    });
    if (!response.ok) throw new Error('daemon rejected item');
    $('#community-title').value = '';
    await loadCommunity();
    appendLog('COMMUNITY', `Issue created locally: ${title}`);
  } catch (error) {
    appendLog('COMMUNITY', `Local daemon unavailable: ${error.message}`, 'warning');
  }
}

async function startTool(kind, id, label) {
  try {
    const response = await fetch(`http://127.0.0.1:4777/api/${kind}/start`, {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id })
    });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error || `${label} failed`);
    $('#tool-status').textContent = `${label}: ${result.status}`;
    appendLog('TOOLCHAIN', `${label} ${result.status}.`);
    if (kind === 'lsp') {
      const init = await fetch('http://127.0.0.1:4777/api/lsp/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, message: { method: 'initialize', params: { processId: null, rootUri: null, capabilities: {} } } }) });
      const initialized = await fetch('http://127.0.0.1:4777/api/lsp/notify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, message: { method: 'initialized', params: {} } }) });
      appendLog('LSP', init.ok && initialized.ok ? 'initialize handshake completed.' : 'initialize handshake failed.', init.ok && initialized.ok ? '' : 'warning');
    }
    if (kind === 'dap') {
      const init = await fetch('http://127.0.0.1:4777/api/dap/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, request: { command: 'initialize', arguments: { clientID: 'aide', clientName: 'AIDE', adapterID: id, linesStartAt1: true, columnsStartAt1: true, pathFormat: 'path' } } }) });
      const payload = await init.json().catch(() => ({}));
      const capabilities = Object.keys(payload.body || {}).filter(key => key.startsWith('supports'));
      $('#debug-status').textContent = init.ok ? `Adapter ready: ${capabilities.length} capabilities.` : 'Adapter initialize failed.';
      appendLog('DAP', init.ok ? `initialize handshake completed; ${capabilities.length} capabilities reported.` : 'initialize request failed.', init.ok ? '' : 'warning');
    }
  } catch (error) {
    $('#tool-status').textContent = `${label}: unavailable`;
    appendLog('TOOLCHAIN', `${label} unavailable: ${error.message}`, 'warning');
  }
}

async function checkActiveFile() {
  try {
    const content = $('#code').textContent;
    const uri = `file:///workspace/${state.activeFile}`;
    const open = await fetch('http://127.0.0.1:4777/api/lsp/notify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'typescript', message: { method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'typescript', version: 1, text: content } } } }) });
    if (!open.ok) throw new Error('LSP is not running');
    const response = await fetch('http://127.0.0.1:4777/api/lsp/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'typescript', message: { method: 'textDocument/completion', params: { textDocument: { uri }, position: { line: 0, character: 0 } } } }) });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error?.message || 'completion request failed');
    const count = result.result?.items?.length ?? result.result?.length ?? 0;
    appendLog('LSP', `Active file analyzed. Completion items returned: ${count}.`);
  } catch (error) { appendLog('LSP', error.message, 'warning'); }
}

async function lspAction(action) {
  const uri = `file:///workspace/${state.activeFile}`;
  const base = { textDocument: { uri }, position: { line: 0, character: 0 } };
  const requests = {
    hover: { method: 'textDocument/hover', params: base },
    definition: { method: 'textDocument/definition', params: base },
    rename: { method: 'textDocument/rename', params: { ...base, newName: 'AIDE_RENAMED' } },
    formatting: { method: 'textDocument/formatting', params: { textDocument: { uri }, options: { tabSize: 2, insertSpaces: true } } }
  };
  try {
    const response = await fetch('http://127.0.0.1:4777/api/lsp/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'typescript', message: requests[action] }) });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error?.message || 'LSP request failed');
    appendLog('LSP', `${action}: ${JSON.stringify(result.result || null).slice(0, 500)}`);
  } catch (error) { appendLog('LSP', `${action} blocked: ${error.message}`, 'warning'); }
}

async function debugActiveFile() {
  if (!state.activeFile.endsWith('.py')) {
    $('#debug-status').textContent = 'Blocked: active file is not Python.';
    appendLog('DAP', 'Debug launch blocked because the active file is not Python.', 'warning');
    return;
  }
  try {
    const response = await fetch('http://127.0.0.1:4777/api/dap/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'python-debugpy', request: { command: 'launch', arguments: { program: `/workspace/${state.activeFile}`, cwd: '/workspace', stopOnEntry: true } } }) });
    const result = await response.json();
    if (!response.ok || result.success === false) throw new Error(result.message || 'debug launch rejected');
    $('#debug-status').textContent = 'Debug launch requested; waiting for entry stop.';
    appendLog('DAP', 'Debug launch requested for the active Python file.');
  } catch (error) { $('#debug-status').textContent = `Debug blocked: ${error.message}`; appendLog('DAP', error.message, 'warning'); }
}

async function refreshDebugThreads() {
  try {
    const response = await fetch('http://127.0.0.1:4777/api/dap/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'python-debugpy', request: { command: 'threads', arguments: {} } }) });
    const result = await response.json();
    if (!response.ok || result.error) throw new Error(result.error?.format || 'debug adapter is not running');
    const count = result.body?.threads?.length || 0;
    $('#debug-status').textContent = `Debug threads: ${count}. Stack/variables populate after launch.`;
    appendLog('DAP', `Threads refreshed: ${count}.`);
  } catch (error) { appendLog('DAP', error.message, 'warning'); }
}

async function trainingRequest(action, payload = {}) {
  try {
    const response = await fetch(`http://127.0.0.1:4777/api/training/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error || 'Training Room request failed');
    $('#training-status').textContent = result.status === 'running' ? `Running: ${result.id}` : `Training Room: ${result.status}`;
    appendLog('TRAINING', JSON.stringify(result));
  } catch (error) { $('#training-status').textContent = `Blocked: ${error.message}`; appendLog('TRAINING', error.message, 'warning'); }
}

async function refreshTrainingStatus() {
  try {
    const response = await fetch('http://127.0.0.1:4777/api/training/status');
    if (!response.ok) return;
    const result = await response.json();
    const active = result.active ? `running: ${result.active.id}` : 'idle';
    const last = result.logs?.at(-1)?.line || 'no log output';
    $('#training-status').textContent = `Training Room ${active} | ${result.jobs.length} job(s) | ${last.slice(0, 100)}`;
  } catch { /* daemon is optional while the static shell is offline */ }
}

async function saveReplay(status = 'verified') {
  try { await fetch('http://127.0.0.1:4777/api/replays', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_class: 'code-change', model: state.selected?.id || 'unknown', status, checks: { human_approval_required: true, source_exported: false } }) }); } catch { /* replay is optional while daemon is offline */ }
}

async function compareModels() {
  $('#arena-status').textContent = 'Running models sequentially...';
  try {
    const response = await fetch('http://127.0.0.1:4777/api/arena/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ approved: true }) });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error || 'arena failed');
    $('#arena-status').textContent = result.winner ? `Winner: ${result.winner.model} (${(result.winner.score * 100).toFixed(1)}%, avg ${result.winner.latency_ms}ms)` : 'No model completed.';
    appendLog('MODEL ARENA', 'Live sequential comparison complete. Scores are benchmark-local only.');
  } catch (error) { $('#arena-status').textContent = `Arena blocked: ${error.message}`; appendLog('MODEL ARENA', error.message, 'warning'); }
}

async function digestFile(file) {
  const buffer = await file.arrayBuffer();
  const digest = await crypto.subtle.digest('SHA-256', buffer);
  const hash = [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('');
  const text = new TextDecoder().decode(buffer);
  const dates = [...new Set(text.match(/\b(?:19|20)\d{2}(?:[-/]\d{1,2}(?:[-/]\d{1,2})?)?\b/g) || [])].slice(0, 20);
  return { name: file.name, bytes: file.size, hash, dates, imported_at: new Date().toISOString() };
}

async function importEvidence(event) {
  if (!state.casefile) {
    appendLog('CASEFILE', 'Create a case before importing evidence.', 'warning');
    event.target.value = '';
    return;
  }
  for (const file of event.target.files) state.casefile.evidence.push(await digestFile(file));
  localStorage.setItem(`aide.case.${state.casefile.id}`, JSON.stringify(state.casefile));
  renderCasefile();
  appendLog('CASEFILE', `${event.target.files.length} evidence item(s) imported locally. Hashes and date anchors recorded; no network request made.`);
  event.target.value = '';
}

function renderNode() {
  if (!state.node) return;
  const id = localStorage.getItem('aide.node.id');
  $('#node-status').innerHTML = `<b>LOCAL-FIRST</b><br>Network: ${esc(state.node.network_default)}<br>Private data: ${esc(state.node.replication.private)}<br>Group sync: ${esc(state.node.capabilities.encrypted_group_sync ? 'enabled' : 'disabled')}<br>Node ID: ${id ? esc(id.slice(0, 13) + '...') : 'not created'}`;
}

async function boot() {
  try {
    const response = await fetch('models/manifest.json', { cache: 'no-store' });
    state.manifest = await response.json();
  } catch (error) {
    state.manifest = { models: [] };
    appendLog('BOOT', 'Could not load models/manifest.json.', 'warning');
  }
  try {
    const nodeResponse = await fetch('community/node-manifest.json', { cache: 'no-store' });
    state.node = await nodeResponse.json();
    renderNode();
  } catch (error) {
    $('#node-status').textContent = 'Node policy unavailable.';
  }
  renderModels();
  const firstReady = state.manifest.models.find(model => model.status !== 'pending');
  if (firstReady) selectModel(firstReady);
  openFile('agent.ts');
  document.querySelectorAll('[data-file]').forEach(button => button.onclick = () => openFile(button.dataset.file));
  $('#review-button').onclick = runReview;
  $('#save-file').onclick = saveFile;
  $('#connection-button').onclick = startRuntime;
  $('#send-button').onclick = sendChat;
  $('#node-button').onclick = () => { localNodeId(); renderNode(); appendLog('NODE', 'Local identity created. No network connection or private data was shared.'); };
  $('#case-button').onclick = () => {
    state.casefile = { id: `AIDE-${new Date().toISOString().slice(0, 10)}-${crypto.randomUUID().slice(0, 8)}`, evidence: [], created_at: new Date().toISOString(), boundary: 'private' };
    localStorage.setItem(`aide.case.${state.casefile.id}`, JSON.stringify(state.casefile));
    renderCasefile();
    appendLog('CASEFILE', `Created ${state.casefile.id}. Boundary: private. Evidence remains on this device.`);
  };
  $('#evidence-input').onchange = importEvidence;
  document.querySelectorAll('[data-community-tab]').forEach(button => button.onclick = () => renderCommunity(button.dataset.communityTab));
  $('#community-add').onclick = addCommunityIssue;
  $('#lsp-button').onclick = () => startTool('lsp', 'typescript', 'TypeScript LSP');
  $('#lsp-check').onclick = checkActiveFile;
  document.querySelectorAll('[data-lsp-action]').forEach(button => button.onclick = () => lspAction(button.dataset.lspAction));
  $('#dap-button').onclick = () => startTool('dap', 'python-debugpy', 'Python DAP');
  $('#debug-file').onclick = debugActiveFile;
  $('#debug-threads').onclick = refreshDebugThreads;
  $('#training-verify').onclick = () => trainingRequest('start', { id: 'verify-release', approved: true });
  $('#training-stop').onclick = () => trainingRequest('stop');
  $('#arena-button').onclick = compareModels;
  refreshTrainingStatus();
  setInterval(refreshTrainingStatus, 5000);
  loadCommunity();
  $('#input').onkeydown = event => { if (event.key === 'Enter') sendChat(); };
}

boot();