@cluesmith/codev 1.1.0 → 1.2.0

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