@cluesmith/codev 1.1.1 → 1.2.1

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.
Files changed (41) hide show
  1. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  2. package/dist/agent-farm/commands/spawn.js +3 -0
  3. package/dist/agent-farm/commands/spawn.js.map +1 -1
  4. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  5. package/dist/agent-farm/commands/start.js +1 -0
  6. package/dist/agent-farm/commands/start.js.map +1 -1
  7. package/dist/agent-farm/servers/dashboard-server.js +12 -0
  8. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +15 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt.d.ts.map +1 -1
  13. package/dist/commands/adopt.js +27 -6
  14. package/dist/commands/adopt.js.map +1 -1
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +59 -3
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/import.d.ts +16 -0
  19. package/dist/commands/import.d.ts.map +1 -0
  20. package/dist/commands/import.js +278 -0
  21. package/dist/commands/import.js.map +1 -0
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +27 -6
  24. package/dist/commands/init.js.map +1 -1
  25. package/package.json +4 -3
  26. package/skeleton/DEPENDENCIES.md +1 -0
  27. package/skeleton/docs/commands/overview.md +1 -0
  28. package/skeleton/maintain/.gitkeep +2 -0
  29. package/skeleton/protocols/maintain/protocol.md +288 -21
  30. package/skeleton/protocols/maintain/templates/maintenance-run.md +64 -0
  31. package/skeleton/protocols/spider/protocol.md +2 -2
  32. package/skeleton/resources/workflow-reference.md +13 -0
  33. package/skeleton/roles/architect.md +185 -134
  34. package/skeleton/templates/lessons-learned.md +28 -0
  35. package/templates/dashboard-split.html +2984 -0
  36. package/templates/dashboard.html +149 -0
  37. package/templates/open.html +1109 -0
  38. package/templates/tower.html +1032 -0
  39. package/skeleton/agents/architecture-documenter.md +0 -189
  40. package/skeleton/agents/codev-updater.md +0 -277
  41. package/skeleton/agents/spider-protocol-updater.md +0 -118
@@ -0,0 +1,1109 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Annotate: {{FILE}}</title>
6
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
7
+ <!-- Markdown preview dependencies (loaded only for .md files) -->
8
+ <script>
9
+ if ({{IS_MARKDOWN}}) {
10
+ document.write('<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"><\/script>');
11
+ document.write('<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"><\/script>');
12
+ }
13
+ </script>
14
+ <style>
15
+ * { box-sizing: border-box; margin: 0; padding: 0; }
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
+ background: #1a1a1a;
19
+ color: #fff;
20
+ min-height: 100vh;
21
+ }
22
+ .header {
23
+ padding: 15px 20px;
24
+ background: #2a2a2a;
25
+ border-bottom: 1px solid #333;
26
+ position: sticky;
27
+ top: 0;
28
+ z-index: 100;
29
+ }
30
+ .header h1.path { font-size: 14px; font-weight: 500; font-family: monospace; color: #fff; }
31
+ .header .subtitle { color: #888; font-size: 12px; margin-top: 4px; }
32
+ .header .actions { margin-top: 10px; display: flex; align-items: center; gap: 8px; }
33
+ .header .btn {
34
+ padding: 6px 12px; border-radius: 4px; border: none; cursor: pointer;
35
+ font-size: 12px;
36
+ }
37
+ .btn-primary { background: #3b82f6; color: #fff; }
38
+ .btn-secondary { background: #444; color: #fff; }
39
+ .btn-success { background: #22c55e; color: #fff; }
40
+ .btn:hover { opacity: 0.9; }
41
+
42
+ /* Unsaved indicator */
43
+ #unsavedIndicator {
44
+ color: #fbbf24;
45
+ font-size: 12px;
46
+ display: none;
47
+ }
48
+ #unsavedIndicator::before {
49
+ content: '●';
50
+ margin-right: 4px;
51
+ }
52
+
53
+ /* Grid layout: each row has line number + code, guaranteeing alignment */
54
+ .content {
55
+ display: grid;
56
+ grid-template-columns: auto 1fr;
57
+ padding: 15px 0;
58
+ }
59
+ .line-num {
60
+ background: #252525;
61
+ color: #666;
62
+ text-align: right;
63
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
64
+ font-size: 13px;
65
+ user-select: none;
66
+ border-right: 1px solid #333;
67
+ padding: 0 12px 0 8px;
68
+ cursor: pointer;
69
+ line-height: 1.5;
70
+ min-width: 50px;
71
+ position: sticky;
72
+ left: 0;
73
+ }
74
+ .line-num:hover { background: #333; color: #fff; }
75
+ .line-num.has-review {
76
+ background: rgba(250, 204, 21, 0.2);
77
+ color: #facc15;
78
+ }
79
+ .code-line {
80
+ background: #1a1a1a;
81
+ padding: 0 15px;
82
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
83
+ font-size: 13px;
84
+ line-height: 1.5;
85
+ white-space: pre-wrap;
86
+ word-wrap: break-word;
87
+ }
88
+ /* Hybrid markdown: syntax visible but muted, content styled */
89
+ /* All text uses same font size for consistent line heights in annotation view */
90
+ .md-syntax { color: #555; } /* Muted syntax markers */
91
+ .md-h1, .md-h2, .md-h3, .md-h4, .md-h5, .md-h6 { font-weight: bold; color: #c678dd; }
92
+ .md-bold { font-weight: bold; color: #e5c07b; }
93
+ .md-italic { font-style: italic; color: #98c379; }
94
+ .md-code { color: #e06c75; background: #2c313a; padding: 1px 4px; border-radius: 3px; }
95
+ .md-link-text { color: #61afef; text-decoration: underline; }
96
+ .md-link-url { color: #555; font-size: 0.9em; }
97
+ .md-bullet { color: #56b6c2; }
98
+ .md-blockquote { color: #abb2bf; border-left: 3px solid #555; padding-left: 10px; }
99
+ .md-fence { color: #555; }
100
+ .md-codeblock { color: #98c379; background: #2c313a; display: block; }
101
+
102
+ /* Markdown Preview Container */
103
+ #preview-container {
104
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
105
+ line-height: 1.6;
106
+ color: #fff;
107
+ max-width: 900px;
108
+ margin: 0 auto;
109
+ }
110
+
111
+ #preview-container h1,
112
+ #preview-container h2 {
113
+ border-bottom: 1px solid #333;
114
+ padding-bottom: 0.3em;
115
+ margin-top: 1.5em;
116
+ margin-bottom: 0.5em;
117
+ }
118
+
119
+ #preview-container h1 { font-size: 2em; }
120
+ #preview-container h2 { font-size: 1.5em; }
121
+ #preview-container h3 { font-size: 1.25em; margin-top: 1em; }
122
+
123
+ #preview-container code {
124
+ background: #2c313a;
125
+ padding: 0.2em 0.4em;
126
+ border-radius: 3px;
127
+ font-size: 0.9em;
128
+ }
129
+
130
+ #preview-container pre {
131
+ background: #252525;
132
+ padding: 16px;
133
+ overflow: auto;
134
+ border-radius: 6px;
135
+ margin: 1em 0;
136
+ }
137
+
138
+ #preview-container pre code {
139
+ background: none;
140
+ padding: 0;
141
+ }
142
+
143
+ #preview-container table {
144
+ border-collapse: collapse;
145
+ width: 100%;
146
+ margin: 1em 0;
147
+ }
148
+
149
+ #preview-container th,
150
+ #preview-container td {
151
+ border: 1px solid #333;
152
+ padding: 8px 12px;
153
+ text-align: left;
154
+ }
155
+
156
+ #preview-container th {
157
+ background: #2a2a2a;
158
+ }
159
+
160
+ #preview-container tr:nth-child(even) {
161
+ background: #252525;
162
+ }
163
+
164
+ #preview-container a {
165
+ color: #3b82f6;
166
+ text-decoration: underline;
167
+ }
168
+
169
+ #preview-container ul,
170
+ #preview-container ol {
171
+ padding-left: 2em;
172
+ margin: 1em 0;
173
+ }
174
+
175
+ #preview-container blockquote {
176
+ border-left: 4px solid #333;
177
+ padding-left: 1em;
178
+ margin: 1em 0;
179
+ color: #888;
180
+ }
181
+
182
+ /* Editor textarea */
183
+ #editor {
184
+ display: none;
185
+ width: 100%;
186
+ height: calc(100vh - 80px);
187
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
188
+ font-size: 13px;
189
+ line-height: 1.5;
190
+ padding: 15px;
191
+ border: none;
192
+ resize: none;
193
+ background: #1a1a1a;
194
+ color: #d4d4d4;
195
+ outline: none;
196
+ tab-size: 2;
197
+ }
198
+ #editor:focus {
199
+ outline: none;
200
+ }
201
+
202
+ /* Notification toast */
203
+ .notification {
204
+ position: fixed;
205
+ bottom: 20px;
206
+ right: 20px;
207
+ padding: 12px 20px;
208
+ background: #22c55e;
209
+ color: white;
210
+ border-radius: 6px;
211
+ font-size: 14px;
212
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
213
+ z-index: 300;
214
+ animation: slideIn 0.3s ease;
215
+ }
216
+ .notification.error {
217
+ background: #ef4444;
218
+ }
219
+ @keyframes slideIn {
220
+ from { transform: translateX(100%); opacity: 0; }
221
+ to { transform: translateX(0); opacity: 1; }
222
+ }
223
+
224
+ .review-line {
225
+ background: rgba(250, 204, 21, 0.1);
226
+ display: block;
227
+ border-left: 3px solid #facc15;
228
+ padding-left: 5px;
229
+ margin-left: -8px;
230
+ }
231
+
232
+ /* Comment Dialog - positioned popup */
233
+ .overlay {
234
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
235
+ background: rgba(0,0,0,0.3);
236
+ display: none;
237
+ z-index: 200;
238
+ }
239
+ .overlay.active { display: block; }
240
+
241
+ .dialog {
242
+ position: absolute;
243
+ background: #2a2a2a;
244
+ padding: 20px;
245
+ border-radius: 8px;
246
+ width: 700px;
247
+ max-width: 90vw;
248
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
249
+ border: 1px solid #444;
250
+ }
251
+ .dialog h3 { font-size: 16px; margin-bottom: 5px; }
252
+ .dialog .line-info { color: #888; font-size: 13px; margin-bottom: 15px; }
253
+ .dialog .line-preview {
254
+ background: #1a1a1a; padding: 10px; border-radius: 4px;
255
+ font-family: monospace; font-size: 12px; margin-bottom: 15px;
256
+ overflow-x: auto; white-space: pre;
257
+ }
258
+ .dialog textarea {
259
+ width: 100%; height: 100px;
260
+ background: #1a1a1a; border: 1px solid #444;
261
+ color: #fff; padding: 10px; border-radius: 4px;
262
+ font-family: inherit; font-size: 14px;
263
+ resize: vertical;
264
+ }
265
+ .dialog textarea:focus { outline: none; border-color: #3b82f6; }
266
+ .dialog .actions { margin-top: 15px; text-align: right; }
267
+ .dialog .btn { margin-left: 8px; }
268
+ .btn-danger { background: #ef4444; color: #fff; }
269
+
270
+ /* Annotations list */
271
+ .annotations-panel {
272
+ position: fixed; right: 0; top: 0; bottom: 0; width: 300px;
273
+ background: #252525; border-left: 1px solid #333;
274
+ transform: translateX(100%);
275
+ transition: transform 0.2s;
276
+ z-index: 150;
277
+ overflow-y: auto;
278
+ }
279
+ .annotations-panel.open { transform: translateX(0); }
280
+ .annotations-panel h3 { padding: 15px; border-bottom: 1px solid #333; font-size: 14px; }
281
+ .annotation-item {
282
+ padding: 12px 15px; border-bottom: 1px solid #333; cursor: pointer;
283
+ }
284
+ .annotation-item:hover { background: #2a2a2a; }
285
+ .annotation-item .line { color: #3b82f6; font-size: 12px; }
286
+ .annotation-item .text { font-size: 13px; margin-top: 4px; }
287
+ </style>
288
+ </head>
289
+ <body data-builder="{{BUILDER_ID}}" data-file="{{FILE_PATH}}" data-lang="{{LANG}}">
290
+ <div class="header">
291
+ <h1 class="path">{{FILE_PATH}}</h1>
292
+ <div class="subtitle">Click on a line number to leave an annotation.</div>
293
+ <div class="actions">
294
+ <button id="reloadBtn" class="btn btn-secondary" onclick="reloadFile()" title="Reload from disk" aria-label="Reload file from disk">↻</button>
295
+ <button id="togglePreviewBtn" class="btn btn-secondary" style="display: none;" title="Toggle Preview (Cmd+Shift+P)">
296
+ <span id="toggle-icon">👁</span> <span id="toggle-text">Preview</span>
297
+ </button>
298
+ <button id="editBtn" class="btn btn-primary" onclick="toggleEditMode()">Switch to Editing</button>
299
+ <button id="saveBtn" class="btn btn-success" onclick="saveEdit()" style="display: none;">Save</button>
300
+ <button id="cancelBtn" class="btn btn-secondary" onclick="cancelEdit()" style="display: none;">Cancel</button>
301
+ <span id="unsavedIndicator">Unsaved changes</span>
302
+ </div>
303
+ </div>
304
+
305
+ <div class="content" id="viewMode">
306
+ <!-- Grid: alternating line-num and code-line elements -->
307
+ </div>
308
+
309
+ <!-- Markdown preview mode -->
310
+ <div id="preview-container" style="display: none; padding: 20px; overflow: auto; height: calc(100vh - 80px);"></div>
311
+
312
+ <!-- Editor mode -->
313
+ <textarea id="editor" spellcheck="false"></textarea>
314
+
315
+ <!-- Comment Dialog -->
316
+ <div class="overlay" id="overlay">
317
+ <div class="dialog">
318
+ <h3 id="dialogTitle">Add Review Comment</h3>
319
+ <div class="line-info">Line <span id="dialogLine"></span></div>
320
+ <div class="line-preview" id="dialogPreview"></div>
321
+ <textarea id="commentText" placeholder="Enter your review comment..."></textarea>
322
+ <div class="hint" style="color: #666; font-size: 12px; margin-top: 8px;">Tip: Press Enter 3 times to submit</div>
323
+ <div class="actions">
324
+ <button class="btn btn-secondary" onclick="closeDialog()">Cancel</button>
325
+ <button class="btn btn-danger" onclick="resolveComment()" id="resolveBtn" style="display:none">Resolve</button>
326
+ <button class="btn btn-primary" onclick="saveComment()">Add Comment</button>
327
+ </div>
328
+ </div>
329
+ </div>
330
+
331
+ <!-- Annotations Panel -->
332
+ <div class="annotations-panel" id="annotationsPanel">
333
+ <h3>Review Comments</h3>
334
+ <div id="annotationsList"></div>
335
+ </div>
336
+
337
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
338
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
339
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
340
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
341
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
342
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script>
343
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
344
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
345
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
346
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js"></script>
347
+
348
+ <script>
349
+ // State
350
+ let fileLines = [];
351
+ let currentLine = null;
352
+ let isEditing = false; // For comment dialog editing
353
+ let consecutiveEnters = 0; // Track triple-enter to submit
354
+
355
+ // Edit mode state
356
+ let editMode = false;
357
+ let originalContent = '';
358
+ let currentContent = '';
359
+ let hasUnsavedChanges = false;
360
+
361
+ // Markdown preview state
362
+ const isMarkdownFile = {{IS_MARKDOWN}};
363
+ let isPreviewMode = false;
364
+
365
+ // Comment patterns by file extension
366
+ const COMMENT_PATTERNS = {
367
+ js: { prefix: '// REVIEW', regex: /^(\s*)\/\/\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
368
+ ts: { prefix: '// REVIEW', regex: /^(\s*)\/\/\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
369
+ jsx: { prefix: '// REVIEW', regex: /^(\s*)\/\/\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
370
+ tsx: { prefix: '// REVIEW', regex: /^(\s*)\/\/\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
371
+ py: { prefix: '# REVIEW', regex: /^(\s*)#\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
372
+ sh: { prefix: '# REVIEW', regex: /^(\s*)#\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
373
+ bash: { prefix: '# REVIEW', regex: /^(\s*)#\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
374
+ yaml: { prefix: '# REVIEW', regex: /^(\s*)#\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
375
+ yml: { prefix: '# REVIEW', regex: /^(\s*)#\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
376
+ md: { prefix: '<!-- REVIEW', suffix: ' -->', regex: /^(\s*)<!--\s*REVIEW(\(@\w+\))?:\s*(.*?)\s*-->$/ },
377
+ html: { prefix: '<!-- REVIEW', suffix: ' -->', regex: /^(\s*)<!--\s*REVIEW(\(@\w+\))?:\s*(.*?)\s*-->$/ },
378
+ css: { prefix: '/* REVIEW', suffix: ' */', regex: /^(\s*)\/\*\s*REVIEW(\(@\w+\))?:\s*(.*?)\s*\*\/$/ },
379
+ };
380
+
381
+ // Get file extension and language
382
+ const filePath = document.body.dataset.file;
383
+ const ext = filePath.split('.').pop().toLowerCase();
384
+ const lang = document.body.dataset.lang || ext;
385
+ const commentStyle = COMMENT_PATTERNS[ext] || COMMENT_PATTERNS.js;
386
+
387
+ // Initialize
388
+ function init(content) {
389
+ currentContent = content;
390
+ originalContent = content;
391
+ fileLines = content.split('\n');
392
+ renderFile();
393
+ updateAnnotationsList();
394
+ initPreviewToggle();
395
+ }
396
+
397
+ // Initialize preview toggle for markdown files
398
+ function initPreviewToggle() {
399
+ const togglePreviewBtn = document.getElementById('togglePreviewBtn');
400
+ if (isMarkdownFile && togglePreviewBtn) {
401
+ togglePreviewBtn.style.display = 'inline-block';
402
+ togglePreviewBtn.addEventListener('click', togglePreviewMode);
403
+ }
404
+ }
405
+
406
+ // Toggle between annotated view and preview mode
407
+ function togglePreviewMode() {
408
+ // Don't allow preview toggle while in textarea edit mode
409
+ if (editMode) {
410
+ return;
411
+ }
412
+
413
+ const viewMode = document.getElementById('viewMode');
414
+ const previewContainer = document.getElementById('preview-container');
415
+ const editBtn = document.getElementById('editBtn');
416
+ const toggleIcon = document.getElementById('toggle-icon');
417
+ const toggleText = document.getElementById('toggle-text');
418
+
419
+ // Capture scroll position as percentage before switching
420
+ const scrollPercent = document.documentElement.scrollHeight > 0
421
+ ? window.scrollY / document.documentElement.scrollHeight
422
+ : 0;
423
+
424
+ isPreviewMode = !isPreviewMode;
425
+
426
+ if (isPreviewMode) {
427
+ // Switch to Preview mode: hide viewMode, show preview
428
+ renderPreview();
429
+ viewMode.style.display = 'none';
430
+ previewContainer.style.display = 'block';
431
+ toggleIcon.textContent = '📝';
432
+ toggleText.textContent = 'Annotate';
433
+ } else {
434
+ // Switch back to annotated view
435
+ viewMode.style.display = 'grid';
436
+ previewContainer.style.display = 'none';
437
+ toggleIcon.textContent = '👁';
438
+ toggleText.textContent = 'Preview';
439
+ }
440
+
441
+ // Restore approximate scroll position after content is rendered
442
+ requestAnimationFrame(() => {
443
+ window.scrollTo(0, scrollPercent * document.documentElement.scrollHeight);
444
+ });
445
+ }
446
+
447
+ // Configure marked.js with secure link renderer (runs once when marked is loaded)
448
+ function configureMarked() {
449
+ if (typeof marked !== 'undefined' && !marked._configured) {
450
+ marked.use({
451
+ renderer: {
452
+ link(href, title, text) {
453
+ const titleAttr = title ? ` title="${title}"` : '';
454
+ return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
455
+ }
456
+ }
457
+ });
458
+ marked._configured = true;
459
+ }
460
+ }
461
+
462
+ // Render markdown preview with DOMPurify sanitization
463
+ function renderPreview() {
464
+ const previewContainer = document.getElementById('preview-container');
465
+
466
+ // Check if marked and DOMPurify are available
467
+ if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
468
+ previewContainer.innerHTML = '<p style="color: #ff6b6b;">Error: Markdown libraries not loaded. Please refresh the page.</p>';
469
+ return;
470
+ }
471
+
472
+ // Configure marked.js on first use
473
+ configureMarked();
474
+
475
+ // Parse markdown and sanitize HTML
476
+ const rawHtml = marked.parse(currentContent);
477
+ const cleanHtml = DOMPurify.sanitize(rawHtml);
478
+ previewContainer.innerHTML = cleanHtml;
479
+
480
+ // Highlight code blocks with Prism.js
481
+ previewContainer.querySelectorAll('pre code').forEach((block) => {
482
+ // Add language class if detected from code fence
483
+ const langMatch = block.className.match(/language-(\w+)/);
484
+ if (langMatch) {
485
+ block.parentElement.classList.add(`language-${langMatch[1]}`);
486
+ }
487
+ Prism.highlightElement(block);
488
+ });
489
+ }
490
+
491
+ function renderFile() {
492
+ const viewMode = document.getElementById('viewMode');
493
+
494
+ // Render code with syntax highlighting - highlight LINE BY LINE
495
+ const isMarkdown = lang === 'markdown' || lang === 'md';
496
+ const prismLang = Prism.languages[lang] || Prism.languages.plaintext;
497
+
498
+ // Reset markdown state before rendering
499
+ if (isMarkdown) resetMarkdownState();
500
+
501
+ // For markdown: two-pass table rendering
502
+ let tableMap = new Map();
503
+ if (isMarkdown) {
504
+ const codeBlockRanges = findCodeBlockRanges(fileLines);
505
+ const tables = identifyTables(fileLines, codeBlockRanges);
506
+ tableMap = buildTableMap(tables);
507
+ }
508
+
509
+ // Generate grid rows: each row has [line-num, code-line]
510
+ const gridHtml = fileLines.map((line, i) => {
511
+ let lineToRender = line;
512
+
513
+ // For markdown tables: pad cells for alignment
514
+ if (isMarkdown) {
515
+ const tableInfo = tableMap.get(i);
516
+ if (tableInfo) {
517
+ const isSep = isSeparatorRow(line);
518
+ lineToRender = renderTableRow(line, tableInfo.columns, isSep);
519
+ }
520
+ }
521
+
522
+ // Use custom hybrid renderer for markdown
523
+ const highlighted = isMarkdown ? renderMarkdownLine(lineToRender) : Prism.highlight(lineToRender, prismLang, lang);
524
+ const isReview = commentStyle.regex.test(line);
525
+ const codeContent = isReview ? `<span class="review-line">${highlighted}</span>` : highlighted;
526
+
527
+ const hasReviewClass = isReview ? 'has-review' : '';
528
+ return `<span class="line-num ${hasReviewClass}" onclick="openDialog(${i}, event)">${i + 1}</span><div class="code-line" data-line="${i}">${codeContent || '&nbsp;'}</div>`;
529
+ }).join('');
530
+
531
+ viewMode.innerHTML = gridHtml;
532
+ }
533
+
534
+ function openDialog(lineIndex, event) {
535
+ currentLine = lineIndex;
536
+ const line = fileLines[lineIndex];
537
+ const match = line.match(commentStyle.regex);
538
+
539
+ document.getElementById('dialogLine').textContent = lineIndex + 1;
540
+
541
+ // Show prior 3 lines + current line
542
+ const startLine = Math.max(0, lineIndex - 3);
543
+ const previewLines = [];
544
+ for (let i = startLine; i <= lineIndex; i++) {
545
+ const lineNum = (i + 1).toString().padStart(4, ' ');
546
+ const marker = i === lineIndex ? '→' : ' ';
547
+ previewLines.push(`${lineNum}${marker} ${fileLines[i]}`);
548
+ }
549
+ document.getElementById('dialogPreview').textContent = previewLines.join('\n');
550
+
551
+ if (match) {
552
+ // Editing existing comment
553
+ isEditing = true;
554
+ document.getElementById('dialogTitle').textContent = 'Edit Review Comment';
555
+ document.getElementById('commentText').value = match[3] || '';
556
+ document.getElementById('resolveBtn').style.display = 'inline-block';
557
+ } else {
558
+ // Adding new comment
559
+ isEditing = false;
560
+ document.getElementById('dialogTitle').textContent = 'Add Review Comment';
561
+ document.getElementById('commentText').value = '';
562
+ document.getElementById('resolveBtn').style.display = 'none';
563
+ }
564
+
565
+ // Position dialog next to click
566
+ const dialog = document.querySelector('.dialog');
567
+ const rect = event.target.getBoundingClientRect();
568
+ const dialogWidth = 700;
569
+ const dialogHeight = 350;
570
+
571
+ // Position to the right of the line number, with some padding
572
+ let left = rect.right + 10;
573
+ let top = rect.top;
574
+
575
+ // Keep dialog in viewport
576
+ if (left + dialogWidth > window.innerWidth) {
577
+ left = window.innerWidth - dialogWidth - 20;
578
+ }
579
+ if (top + dialogHeight > window.innerHeight) {
580
+ top = window.innerHeight - dialogHeight - 20;
581
+ }
582
+ if (top < 60) top = 60; // Below header
583
+
584
+ dialog.style.left = left + 'px';
585
+ dialog.style.top = top + 'px';
586
+
587
+ document.getElementById('overlay').classList.add('active');
588
+ document.getElementById('commentText').focus();
589
+ }
590
+
591
+ function closeDialog() {
592
+ document.getElementById('overlay').classList.remove('active');
593
+ currentLine = null;
594
+ isEditing = false;
595
+ }
596
+
597
+ // Open dialog from annotation panel (center it since no click position)
598
+ function openDialogFromPanel(lineIndex) {
599
+ // Create a fake event positioned at the line number
600
+ // Exclude continuation spans (..) so indexing matches fileLines
601
+ const lineNums = document.querySelectorAll('.line-num:not(.continuation)');
602
+ if (lineNums[lineIndex]) {
603
+ const fakeEvent = { target: lineNums[lineIndex] };
604
+ openDialog(lineIndex, fakeEvent);
605
+ // Scroll to the line
606
+ lineNums[lineIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
607
+ }
608
+ }
609
+
610
+ function saveComment() {
611
+ const text = document.getElementById('commentText').value.trim();
612
+ if (!text) return;
613
+
614
+ const line = fileLines[currentLine];
615
+ const indent = line.match(/^(\s*)/)[1];
616
+
617
+ let commentLine = indent + commentStyle.prefix + '(@architect): ' + text;
618
+ if (commentStyle.suffix) {
619
+ commentLine += commentStyle.suffix;
620
+ }
621
+
622
+ if (isEditing) {
623
+ // Replace existing comment
624
+ fileLines[currentLine] = commentLine;
625
+ } else {
626
+ // Insert new comment BELOW the current line (so the annotated line keeps its number)
627
+ fileLines.splice(currentLine + 1, 0, commentLine);
628
+ }
629
+
630
+ renderFile();
631
+ updateAnnotationsList();
632
+ closeDialog();
633
+ saveFile(); // Auto-save
634
+ }
635
+
636
+ function resolveComment() {
637
+ if (currentLine !== null && isEditing) {
638
+ fileLines.splice(currentLine, 1);
639
+ renderFile();
640
+ updateAnnotationsList();
641
+ closeDialog();
642
+ saveFile(); // Auto-save
643
+ }
644
+ }
645
+
646
+ function updateAnnotationsList() {
647
+ const list = document.getElementById('annotationsList');
648
+ const annotations = [];
649
+
650
+ fileLines.forEach((line, i) => {
651
+ const match = line.match(commentStyle.regex);
652
+ if (match) {
653
+ annotations.push({
654
+ line: i + 1,
655
+ author: match[2] || '',
656
+ text: match[3] || ''
657
+ });
658
+ }
659
+ });
660
+
661
+ if (annotations.length === 0) {
662
+ list.innerHTML = '<div style="padding: 15px; color: #666;">No review comments</div>';
663
+ } else {
664
+ list.innerHTML = annotations.map(a => `
665
+ <div class="annotation-item" onclick="openDialogFromPanel(${a.line - 1})">
666
+ <div class="line">Line ${a.line} ${a.author}</div>
667
+ <div class="text">${escapeHtml(a.text)}</div>
668
+ </div>
669
+ `).join('');
670
+ }
671
+ }
672
+
673
+ function toggleAnnotations() {
674
+ document.getElementById('annotationsPanel').classList.toggle('open');
675
+ }
676
+
677
+ function escapeHtml(text) {
678
+ const div = document.createElement('div');
679
+ div.textContent = text;
680
+ return div.innerHTML;
681
+ }
682
+
683
+ // Hybrid markdown: syntax visible but muted, content styled
684
+ // State for multi-line constructs
685
+ let inCodeBlock = false;
686
+
687
+ function renderMarkdownLine(line) {
688
+ // Handle code block fences
689
+ const fenceMatch = line.match(/^(\s*)(```+)(\w*)$/);
690
+ if (fenceMatch) {
691
+ inCodeBlock = !inCodeBlock;
692
+ return `<span class="md-fence">${escapeHtml(line)}</span>`;
693
+ }
694
+
695
+ // Inside code block - render as code, no markdown processing
696
+ if (inCodeBlock) {
697
+ return `<span class="md-codeblock">${escapeHtml(line)}</span>`;
698
+ }
699
+
700
+ let html = escapeHtml(line);
701
+
702
+ // Headers: # visible but muted, content styled
703
+ const headerMatch = html.match(/^(#{1,6})\s+(.*)$/);
704
+ if (headerMatch) {
705
+ const level = headerMatch[1].length;
706
+ const hashes = headerMatch[1];
707
+ const content = headerMatch[2];
708
+ return `<span class="md-syntax">${hashes}</span> <span class="md-h${level}">${content}</span>`;
709
+ }
710
+
711
+ // List items: - or * visible but styled
712
+ html = html.replace(/^(\s*)([-*])\s+/, '$1<span class="md-bullet">$2</span> ');
713
+ html = html.replace(/^(\s*)(\d+\.)\s+/, '$1<span class="md-bullet">$2</span> ');
714
+
715
+ // Bold: ** visible but muted, content bold
716
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<span class="md-syntax">**</span><span class="md-bold">$1</span><span class="md-syntax">**</span>');
717
+
718
+ // Italic: * visible but muted, content italic (but not inside **)
719
+ html = html.replace(/(?<!\*)(?<!\<span class="md-syntax"\>)\*([^*]+)\*(?!\*)/g, '<span class="md-syntax">*</span><span class="md-italic">$1</span><span class="md-syntax">*</span>');
720
+
721
+ // Inline code: ` visible but muted, content styled
722
+ html = html.replace(/`([^`]+)`/g, '<span class="md-syntax">`</span><span class="md-code">$1</span><span class="md-syntax">`</span>');
723
+
724
+ // Links: [text](url) - brackets muted, text styled, url muted
725
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="md-syntax">[</span><span class="md-link-text">$1</span><span class="md-syntax">](</span><span class="md-link-url">$2</span><span class="md-syntax">)</span>');
726
+
727
+ // Blockquote: > visible but muted
728
+ html = html.replace(/^(\s*)(&gt;)\s*(.*)$/, '$1<span class="md-syntax">$2</span> <span class="md-blockquote">$3</span>');
729
+
730
+ return html;
731
+ }
732
+
733
+ // Reset code block state when re-rendering
734
+ function resetMarkdownState() {
735
+ inCodeBlock = false;
736
+ }
737
+
738
+ // ==================== Table Alignment Functions ====================
739
+
740
+ // Find code block ranges to exclude from table processing
741
+ function findCodeBlockRanges(lines) {
742
+ const ranges = [];
743
+ let start = null;
744
+
745
+ lines.forEach((line, i) => {
746
+ if (/^(\s*)(```+|~~~+)/.test(line)) {
747
+ if (start === null) {
748
+ start = i;
749
+ } else {
750
+ ranges.push({ start, end: i });
751
+ start = null;
752
+ }
753
+ }
754
+ });
755
+
756
+ // Handle unclosed code block (extends to end of file)
757
+ if (start !== null) {
758
+ ranges.push({ start, end: lines.length - 1 });
759
+ }
760
+
761
+ return ranges;
762
+ }
763
+
764
+ // Check if line is inside a code block
765
+ function isInsideCodeBlock(lineNum, ranges) {
766
+ return ranges.some(r => lineNum >= r.start && lineNum <= r.end);
767
+ }
768
+
769
+ // Check if line is a table separator row (e.g., |---|---|)
770
+ function isSeparatorRow(line) {
771
+ // Must contain at least one dash and be mostly dashes, pipes, colons, and spaces
772
+ return /^\|?[\s:|-]+\|?$/.test(line) && line.includes('-');
773
+ }
774
+
775
+ // Parse cells from a table row, handling leading/trailing pipes
776
+ function parseTableCells(line) {
777
+ // Split by | (not escaped \|)
778
+ const parts = line.split(/(?<!\\)\|/);
779
+
780
+ // Remove empty first/last elements from leading/trailing pipes
781
+ if (parts.length > 0 && parts[0].trim() === '') parts.shift();
782
+ if (parts.length > 0 && parts[parts.length - 1].trim() === '') parts.pop();
783
+
784
+ return parts;
785
+ }
786
+
787
+ // Pad a separator cell preserving alignment markers (:---:, :---, ---:)
788
+ function padSeparatorCell(cell, width) {
789
+ const trimmed = cell.trim();
790
+ const leftColon = trimmed.startsWith(':');
791
+ const rightColon = trimmed.endsWith(':');
792
+
793
+ // Calculate dash count: width minus colons
794
+ const dashCount = width - (leftColon ? 1 : 0) - (rightColon ? 1 : 0);
795
+
796
+ return (leftColon ? ':' : '') + '-'.repeat(Math.max(1, dashCount)) + (rightColon ? ':' : '');
797
+ }
798
+
799
+ // Pad a regular cell to target width
800
+ function padCell(cell, width) {
801
+ const trimmed = cell.trim();
802
+ return trimmed.padEnd(width);
803
+ }
804
+
805
+ // Identify tables in the document and compute column widths
806
+ function identifyTables(lines, codeBlockRanges) {
807
+ const tables = [];
808
+
809
+ for (let i = 0; i < lines.length - 1; i++) {
810
+ // Skip lines inside code blocks
811
+ if (isInsideCodeBlock(i, codeBlockRanges)) continue;
812
+
813
+ const line = lines[i];
814
+ const nextLine = lines[i + 1];
815
+
816
+ // Look for header + separator pattern
817
+ if (line.includes('|') && isSeparatorRow(nextLine) && !isInsideCodeBlock(i + 1, codeBlockRanges)) {
818
+ const table = { startLine: i, endLine: i + 1, columns: [] };
819
+
820
+ // Extend table to include all following rows with |
821
+ let j = i + 2;
822
+ while (j < lines.length && lines[j].includes('|') && !isInsideCodeBlock(j, codeBlockRanges)) {
823
+ table.endLine = j;
824
+ j++;
825
+ }
826
+
827
+ // Compute column widths across all rows
828
+ for (let row = table.startLine; row <= table.endLine; row++) {
829
+ const cells = parseTableCells(lines[row]);
830
+ cells.forEach((cell, col) => {
831
+ const cellWidth = cell.trim().length;
832
+ table.columns[col] = Math.max(table.columns[col] || 0, cellWidth);
833
+ });
834
+ }
835
+
836
+ tables.push(table);
837
+ i = table.endLine; // Skip past this table
838
+ }
839
+ }
840
+
841
+ return tables;
842
+ }
843
+
844
+ // Build a map: lineNumber -> tableInfo (or undefined)
845
+ function buildTableMap(tables) {
846
+ const map = new Map();
847
+ for (const table of tables) {
848
+ for (let i = table.startLine; i <= table.endLine; i++) {
849
+ map.set(i, table);
850
+ }
851
+ }
852
+ return map;
853
+ }
854
+
855
+ // Render a table row with padded cells
856
+ function renderTableRow(line, columnWidths, isSeparator) {
857
+ // Preserve original indentation for nested tables (in lists, blockquotes)
858
+ const indent = line.match(/^(\s*)/)[1];
859
+ const trimmedLine = line.trimStart();
860
+
861
+ const cells = parseTableCells(line);
862
+ const hasLeadingPipe = trimmedLine.startsWith('|');
863
+ const hasTrailingPipe = line.trimEnd().endsWith('|');
864
+
865
+ const paddedCells = cells.map((cell, i) => {
866
+ const targetWidth = columnWidths[i] || cell.trim().length;
867
+ if (isSeparator) {
868
+ return padSeparatorCell(cell, targetWidth);
869
+ } else {
870
+ return padCell(cell, targetWidth);
871
+ }
872
+ });
873
+
874
+ // Reconstruct line with pipes, preserving indentation
875
+ let result = indent;
876
+ if (hasLeadingPipe) result += '| ';
877
+ result += paddedCells.join(' | ');
878
+ if (hasTrailingPipe) result += ' |';
879
+
880
+ return result;
881
+ }
882
+
883
+ async function saveFile() {
884
+ try {
885
+ const response = await fetch('/save', {
886
+ method: 'POST',
887
+ headers: { 'Content-Type': 'application/json' },
888
+ body: JSON.stringify({
889
+ file: filePath,
890
+ content: fileLines.join('\n')
891
+ })
892
+ });
893
+
894
+ if (!response.ok) {
895
+ console.error('Failed to save:', await response.text());
896
+ }
897
+ } catch (err) {
898
+ console.error('Failed to save:', err.message);
899
+ }
900
+ }
901
+
902
+ // Reload file from disk (after external edits)
903
+ function reloadFile() {
904
+ // Warn if there are unsaved changes
905
+ if (hasUnsavedChanges) {
906
+ if (!confirm('You have unsaved changes. Reload anyway?')) {
907
+ return;
908
+ }
909
+ }
910
+
911
+ // Reload the page - server re-reads file on each request
912
+ window.location.reload();
913
+ }
914
+
915
+ // ==================== Edit Mode Functions ====================
916
+
917
+ async function toggleEditMode() {
918
+ const viewMode = document.getElementById('viewMode');
919
+ const editor = document.getElementById('editor');
920
+ const editBtn = document.getElementById('editBtn');
921
+ const saveBtn = document.getElementById('saveBtn');
922
+ const cancelBtn = document.getElementById('cancelBtn');
923
+ const previewContainer = document.getElementById('preview-container');
924
+
925
+ const subtitle = document.querySelector('.subtitle');
926
+
927
+ // If in preview mode, exit preview first
928
+ if (isPreviewMode) {
929
+ togglePreviewMode();
930
+ }
931
+
932
+ if (!editMode) {
933
+ // Enter edit mode
934
+ // Capture scroll position from view mode
935
+ const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
936
+
937
+ currentContent = fileLines.join('\n');
938
+ // Note: originalContent is set only in init() and after successful saves
939
+ // to ensure Cancel always reverts to the true disk state
940
+ editor.value = currentContent;
941
+ viewMode.style.display = 'none';
942
+ editor.style.display = 'block';
943
+ editBtn.textContent = 'Switch to Annotate';
944
+ subtitle.textContent = 'Edit the file directly by clicking and typing.';
945
+ saveBtn.style.display = 'inline-block';
946
+ cancelBtn.style.display = 'inline-block';
947
+ editMode = true;
948
+
949
+ // Apply scroll position to editor
950
+ editor.scrollTop = scrollTop;
951
+ editor.focus();
952
+ } else {
953
+ // Exit to view mode - auto-save changes per spec
954
+ // Capture scroll position from editor
955
+ const scrollTop = editor.scrollTop;
956
+
957
+ // Auto-save if there are changes (spec requires: "Edit text, click View → changes saved")
958
+ if (hasUnsavedChanges) {
959
+ await saveEdit();
960
+ }
961
+
962
+ currentContent = editor.value;
963
+ fileLines = currentContent.split('\n');
964
+ editor.style.display = 'none';
965
+ viewMode.style.display = 'grid';
966
+ editBtn.textContent = 'Switch to Editing';
967
+ subtitle.textContent = 'Click on a line number to leave an annotation.';
968
+ saveBtn.style.display = 'none';
969
+ cancelBtn.style.display = 'none';
970
+ editMode = false;
971
+
972
+ renderFile();
973
+ updateAnnotationsList();
974
+
975
+ // Apply scroll position to document
976
+ requestAnimationFrame(() => {
977
+ document.documentElement.scrollTop = scrollTop;
978
+ document.body.scrollTop = scrollTop;
979
+ });
980
+ }
981
+ }
982
+
983
+ async function saveEdit() {
984
+ const editor = document.getElementById('editor');
985
+ const content = editor.value;
986
+
987
+ try {
988
+ const response = await fetch('/save', {
989
+ method: 'POST',
990
+ headers: { 'Content-Type': 'application/json' },
991
+ body: JSON.stringify({ file: filePath, content })
992
+ });
993
+
994
+ if (response.ok) {
995
+ currentContent = content;
996
+ originalContent = content;
997
+ fileLines = content.split('\n');
998
+ hasUnsavedChanges = false;
999
+ updateUnsavedIndicator();
1000
+ showNotification('File saved!');
1001
+ } else {
1002
+ const errorText = await response.text();
1003
+ throw new Error(errorText || 'Save failed');
1004
+ }
1005
+ } catch (err) {
1006
+ showNotification('Error saving file: ' + err.message, 'error');
1007
+ }
1008
+ }
1009
+
1010
+ function cancelEdit() {
1011
+ const editor = document.getElementById('editor');
1012
+ editor.value = originalContent;
1013
+ currentContent = originalContent;
1014
+ fileLines = originalContent.split('\n');
1015
+ hasUnsavedChanges = false;
1016
+ updateUnsavedIndicator();
1017
+ toggleEditMode(); // Back to view mode
1018
+ }
1019
+
1020
+ function updateUnsavedIndicator() {
1021
+ const indicator = document.getElementById('unsavedIndicator');
1022
+ indicator.style.display = hasUnsavedChanges ? 'inline' : 'none';
1023
+ }
1024
+
1025
+ function showNotification(message, type = 'success') {
1026
+ // Remove existing notification
1027
+ const existing = document.querySelector('.notification');
1028
+ if (existing) existing.remove();
1029
+
1030
+ const notification = document.createElement('div');
1031
+ notification.className = 'notification' + (type === 'error' ? ' error' : '');
1032
+ notification.textContent = message;
1033
+ document.body.appendChild(notification);
1034
+
1035
+ setTimeout(() => notification.remove(), 3000);
1036
+ }
1037
+
1038
+ // Track unsaved changes in editor
1039
+ document.getElementById('editor').addEventListener('input', () => {
1040
+ hasUnsavedChanges = document.getElementById('editor').value !== originalContent;
1041
+ updateUnsavedIndicator();
1042
+ });
1043
+
1044
+ // Tab key handling for indentation
1045
+ document.getElementById('editor').addEventListener('keydown', (e) => {
1046
+ if (e.key === 'Tab') {
1047
+ e.preventDefault();
1048
+ const editor = e.target;
1049
+ const start = editor.selectionStart;
1050
+ const end = editor.selectionEnd;
1051
+
1052
+ // Insert 2 spaces (or could use \t)
1053
+ editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
1054
+ editor.selectionStart = editor.selectionEnd = start + 2;
1055
+
1056
+ // Trigger input event to update unsaved indicator
1057
+ editor.dispatchEvent(new Event('input'));
1058
+ }
1059
+ });
1060
+
1061
+ // Handle keyboard shortcuts
1062
+ document.addEventListener('keydown', (e) => {
1063
+ if (e.key === 'Escape') {
1064
+ closeDialog();
1065
+ }
1066
+
1067
+ // Cmd/Ctrl+S to save
1068
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
1069
+ e.preventDefault();
1070
+ if (editMode) {
1071
+ saveEdit();
1072
+ }
1073
+ }
1074
+
1075
+ // Cmd/Ctrl+Shift+P to toggle preview (markdown files only)
1076
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'p' || e.key === 'P')) {
1077
+ if (isMarkdownFile) {
1078
+ e.preventDefault();
1079
+ togglePreviewMode();
1080
+ }
1081
+ }
1082
+ });
1083
+
1084
+ // Warn before leaving with unsaved changes
1085
+ window.addEventListener('beforeunload', (e) => {
1086
+ if (hasUnsavedChanges) {
1087
+ e.preventDefault();
1088
+ e.returnValue = '';
1089
+ }
1090
+ });
1091
+
1092
+ // Triple-enter to submit annotation
1093
+ document.getElementById('commentText').addEventListener('keydown', (e) => {
1094
+ if (e.key === 'Enter' && !e.shiftKey) {
1095
+ consecutiveEnters++;
1096
+ if (consecutiveEnters >= 3) {
1097
+ e.preventDefault();
1098
+ consecutiveEnters = 0;
1099
+ saveComment();
1100
+ }
1101
+ } else {
1102
+ consecutiveEnters = 0;
1103
+ }
1104
+ });
1105
+
1106
+ // FILE_CONTENT will be injected by the server
1107
+ </script>
1108
+ </body>
1109
+ </html>