@phren/cli 0.0.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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,1387 @@
1
+ export function renderSkillUiEnhancementScript(authToken) {
2
+ return `(function() {
3
+ var _skillAuthToken = '${authToken}';
4
+ var _skillCurrent = null;
5
+ var _skillEditing = false;
6
+
7
+ function esc(s) {
8
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
9
+ }
10
+ function authUrl(base) {
11
+ return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_skillAuthToken);
12
+ }
13
+ function authBody(body) {
14
+ return body + (_skillAuthToken ? '&_auth=' + encodeURIComponent(_skillAuthToken) : '');
15
+ }
16
+ function fetchCsrfToken(cb) {
17
+ var url = '/api/csrf-token' + (_skillAuthToken ? '?_auth=' + encodeURIComponent(_skillAuthToken) : '');
18
+ fetch(url).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || null); }).catch(function() { cb(null); });
19
+ }
20
+ function renderSkillReader(content) {
21
+ var reader = document.getElementById('skills-reader');
22
+ if (!_skillCurrent || !reader) return;
23
+ var statusBadge = '<span class="badge ' + (_skillCurrent.enabled ? 'badge-on' : 'badge-off') + '" id="skill-enabled-badge">' + (_skillCurrent.enabled ? 'enabled' : 'disabled') + '</span>';
24
+ var toggleLabel = _skillCurrent.enabled ? 'Disable' : 'Enable';
25
+ reader.innerHTML =
26
+ '<div class="reader-toolbar">' +
27
+ '<span class="reader-title">' + esc(_skillCurrent.name) + '</span>' +
28
+ '<span class="reader-path">' + esc(_skillCurrent.path) + '</span>' +
29
+ statusBadge +
30
+ '<span id="skill-status"></span>' +
31
+ '<button class="btn btn-sm" data-action="phrenToggleSkill">' + toggleLabel + '</button>' +
32
+ '<button class="btn btn-sm" data-action="phrenEditSkill">Edit</button>' +
33
+ '</div>' +
34
+ '<div class="reader-content"><pre id="skill-pre">' + esc(content) + '</pre></div>';
35
+ }
36
+ function loadSkills(selectPath) {
37
+ fetch(authUrl('/api/skills')).then(function(r) { return r.json(); }).then(function(data) {
38
+ var list = document.getElementById('skills-list');
39
+ if (!list) return;
40
+ if (!data.length) {
41
+ list.innerHTML = '<div style="padding:40px 20px;color:var(--muted);text-align:center">No skills installed</div>';
42
+ return;
43
+ }
44
+ var bySource = {};
45
+ data.forEach(function(s) { (bySource[s.source] = bySource[s.source] || []).push(s); });
46
+ var html = '';
47
+ Object.keys(bySource).sort().forEach(function(src) {
48
+ html += '<div class="split-group-label">' + esc(src) + '</div>';
49
+ bySource[src].forEach(function(s) {
50
+ html += '<div class="split-item" data-path="' + esc(s.path) + '" data-name="' + esc(s.name) + '" data-source="' + esc(s.source) + '" data-enabled="' + (s.enabled ? 'true' : 'false') + '" data-action="phrenSelectSkillFromEl">' +
51
+ '<span>' + esc(s.name) + '</span>' +
52
+ '<span class="badge ' + (s.enabled ? 'badge-on' : 'badge-off') + '">' + (s.enabled ? 'enabled' : 'disabled') + '</span>' +
53
+ '</div>';
54
+ });
55
+ });
56
+ list.innerHTML = html;
57
+ if (selectPath) {
58
+ var current = list.querySelector('.split-item[data-path="' + CSS.escape(selectPath) + '"]');
59
+ if (current) current.click();
60
+ }
61
+ });
62
+ }
63
+ window.phrenSelectSkillFromEl = function(el) {
64
+ if (!el) return;
65
+ window.phrenSelectSkill(
66
+ el.getAttribute('data-path') || '',
67
+ el.getAttribute('data-name') || '',
68
+ el.getAttribute('data-source') || '',
69
+ el.getAttribute('data-enabled') === 'true',
70
+ el
71
+ );
72
+ };
73
+ window.phrenSelectSkill = function(filePath, name, source, enabled, el) {
74
+ if (_skillEditing && !confirm('Discard unsaved changes?')) return;
75
+ _skillEditing = false;
76
+ _skillCurrent = { path: filePath, name: name, source: source, enabled: enabled };
77
+ document.querySelectorAll('#skills-list .split-item').forEach(function(i) { i.classList.remove('selected'); });
78
+ if (el) el.classList.add('selected');
79
+ var reader = document.getElementById('skills-reader');
80
+ if (reader) reader.innerHTML = '<div class="reader-empty">Loading...</div>';
81
+ fetch(authUrl('/api/skill-content?path=' + encodeURIComponent(filePath))).then(function(r) { return r.json(); }).then(function(data) {
82
+ if (!data.ok) {
83
+ if (reader) reader.innerHTML = '<div class="reader-empty">' + esc(data.error || 'Error loading file') + '</div>';
84
+ return;
85
+ }
86
+ renderSkillReader(data.content);
87
+ });
88
+ };
89
+ window.phrenEditSkill = function() {
90
+ var pre = document.getElementById('skill-pre');
91
+ if (!pre || !_skillCurrent) return;
92
+ _skillEditing = true;
93
+ var content = pre.textContent || '';
94
+ var toolbar = document.querySelector('#skills-reader .reader-toolbar');
95
+ if (!toolbar) return;
96
+ Array.from(toolbar.querySelectorAll('.btn')).forEach(function(btn) { btn.remove(); });
97
+ toolbar.insertAdjacentHTML('beforeend', '<button class="btn btn-sm btn-primary" data-action="phrenSaveSkill">Save</button><button class="btn btn-sm" data-action="phrenCancelSkillEdit">Cancel</button>');
98
+ var ta = document.createElement('textarea');
99
+ ta.id = 'skill-textarea';
100
+ ta.value = content;
101
+ pre.replaceWith(ta);
102
+ ta.focus();
103
+ };
104
+ window.phrenCancelSkillEdit = function() {
105
+ _skillEditing = false;
106
+ if (_skillCurrent) window.phrenSelectSkill(_skillCurrent.path, _skillCurrent.name, _skillCurrent.source, _skillCurrent.enabled);
107
+ };
108
+ window.phrenSaveSkill = function() {
109
+ var ta = document.getElementById('skill-textarea');
110
+ if (!ta || !_skillCurrent) return;
111
+ fetchCsrfToken(function(csrfToken) {
112
+ var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';
113
+ fetch('/api/skill-save', {
114
+ method: 'POST',
115
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
116
+ body: authBody('path=' + encodeURIComponent(_skillCurrent.path) + '&content=' + encodeURIComponent(ta.value)) + csrfPart,
117
+ }).then(function(r) { return r.json(); }).then(function(data) {
118
+ var status = document.getElementById('skill-status');
119
+ if (status) {
120
+ status.textContent = data.ok ? 'Saved' : (data.error || 'Save failed');
121
+ status.className = data.ok ? 'text-success' : 'text-danger';
122
+ }
123
+ if (data.ok) {
124
+ _skillEditing = false;
125
+ renderSkillReader(ta.value);
126
+ }
127
+ });
128
+ });
129
+ };
130
+ window.phrenToggleSkill = function() {
131
+ if (!_skillCurrent) return;
132
+ fetchCsrfToken(function(csrfToken) {
133
+ var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';
134
+ var nextEnabled = !_skillCurrent.enabled;
135
+ fetch('/api/skill-toggle', {
136
+ method: 'POST',
137
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
138
+ body: authBody('project=' + encodeURIComponent(_skillCurrent.source) + '&name=' + encodeURIComponent(_skillCurrent.name) + '&enabled=' + encodeURIComponent(String(nextEnabled))) + csrfPart,
139
+ }).then(function(r) { return r.json(); }).then(function(data) {
140
+ if (!data.ok) return;
141
+ _skillCurrent.enabled = nextEnabled;
142
+ loadSkills(_skillCurrent.path);
143
+ window.phrenSelectSkill(_skillCurrent.path, _skillCurrent.name, _skillCurrent.source, _skillCurrent.enabled);
144
+ });
145
+ });
146
+ };
147
+ var baseSwitchTab = window.switchTab;
148
+ if (typeof baseSwitchTab === 'function') {
149
+ window.switchTab = function(tab) {
150
+ baseSwitchTab(tab);
151
+ if (tab === 'skills') setTimeout(function() { loadSkills(_skillCurrent && _skillCurrent.path); }, 0);
152
+ };
153
+ }
154
+ // Event delegation for dynamically generated skill UI buttons
155
+ document.addEventListener('click', function(e) {
156
+ var target = e.target;
157
+ if (!target || typeof target.closest !== 'function') return;
158
+ var actionEl = target.closest('[data-action]');
159
+ if (!actionEl) return;
160
+ var action = actionEl.getAttribute('data-action');
161
+ if (action === 'phrenToggleSkill') { phrenToggleSkill(); }
162
+ else if (action === 'phrenEditSkill') { phrenEditSkill(); }
163
+ else if (action === 'phrenSaveSkill') { phrenSaveSkill(); }
164
+ else if (action === 'phrenCancelSkillEdit') { phrenCancelSkillEdit(); }
165
+ else if (action === 'phrenSelectSkillFromEl') { phrenSelectSkillFromEl(actionEl); }
166
+ });
167
+ })();`;
168
+ }
169
+ export function renderProjectReferenceEnhancementScript(authToken) {
170
+ return `(function() {
171
+ var _referenceAuthToken = '${authToken}';
172
+ var _referenceState = {
173
+ project: '',
174
+ topicsData: null,
175
+ referenceData: null,
176
+ selectedType: '',
177
+ selectedKey: '',
178
+ editor: null
179
+ };
180
+
181
+ function esc(s) {
182
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
183
+ }
184
+ function authUrl(base) {
185
+ return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_referenceAuthToken);
186
+ }
187
+ function authBody(body) {
188
+ return body + (_referenceAuthToken ? '&_auth=' + encodeURIComponent(_referenceAuthToken) : '');
189
+ }
190
+ function fetchCsrfToken(cb) {
191
+ var url = '/api/csrf-token' + (_referenceAuthToken ? '?_auth=' + encodeURIComponent(_referenceAuthToken) : '');
192
+ fetch(url).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || null); }).catch(function() { cb(null); });
193
+ }
194
+ function currentProject() {
195
+ var selected = document.querySelector('.project-card.selected');
196
+ return selected ? (selected.getAttribute('data-project') || '') : '';
197
+ }
198
+ function findTopic(slug) {
199
+ var topics = (_referenceState.topicsData && _referenceState.topicsData.topics) || [];
200
+ return topics.find(function(topic) { return topic.slug === slug; }) || null;
201
+ }
202
+ function findTopicDoc(slug) {
203
+ var docs = (_referenceState.referenceData && _referenceState.referenceData.topicDocs) || [];
204
+ return docs.find(function(doc) { return doc.slug === slug; }) || null;
205
+ }
206
+ function setStatus(message, type) {
207
+ var el = document.getElementById('reference-status');
208
+ if (!el) return;
209
+ el.textContent = message || '';
210
+ el.className = 'reference-status' + (type ? ' ' + type : '');
211
+ if (message) setTimeout(function() {
212
+ var live = document.getElementById('reference-status');
213
+ if (live) { live.textContent = ''; live.className = 'reference-status'; }
214
+ }, 3000);
215
+ }
216
+ function loadJson(url) {
217
+ return fetch(authUrl(url)).then(function(r) { return r.json(); });
218
+ }
219
+ function readerToolbar(title, pathLabel, actionsHtml) {
220
+ return '<div class="reader-toolbar">' +
221
+ '<span class="reader-title">' + esc(title) + '</span>' +
222
+ '<span class="reader-path">' + esc(pathLabel || '') + '</span>' +
223
+ '<span id="reference-status" class="reference-status"></span>' +
224
+ (actionsHtml || '') +
225
+ '</div>';
226
+ }
227
+ function renderReferenceHome() {
228
+ var reader = document.getElementById('reference-reader');
229
+ if (!reader) return;
230
+ var topicsData = _referenceState.topicsData || { source: 'default', topics: [], suggestions: [] };
231
+ var banner = topicsData.source === 'default'
232
+ ? '<div class="reference-banner">This project is using starter topics. Customize them so archived findings match the project domain instead of generic web-dev buckets.</div>'
233
+ : '';
234
+ reader.innerHTML = readerToolbar('Reference Topics', _referenceState.project, '<button class="btn btn-sm" data-ref-action="addTopic">Add topic</button><button class="btn btn-sm" data-ref-action="reclassify">Reclassify archived findings</button>') +
235
+ '<div class="topic-empty">' +
236
+ banner +
237
+ '<p>Select a topic doc or a reference file from the sidebar. Topic definitions live in <code>topic-config.json</code> and archive docs live under <code>reference/topics/</code>.</p>' +
238
+ '</div>';
239
+ }
240
+ function renderTopicEditor(mode, topic, suggestion) {
241
+ var reader = document.getElementById('reference-reader');
242
+ if (!reader) return;
243
+ var source = topic || suggestion || { slug: '', label: '', description: '', keywords: [] };
244
+ var title = mode === 'edit' ? 'Edit topic' : 'Add topic';
245
+ reader.innerHTML = readerToolbar(title, _referenceState.project, '<button class="btn btn-sm" data-ref-action="cancelEditor">Cancel</button>') +
246
+ '<div class="reader-content">' +
247
+ '<form class="topic-editor" id="topic-editor-form">' +
248
+ '<label>Label<input id="topic-label-input" value="' + esc(source.label || '') + '" placeholder="Rendering" /></label>' +
249
+ '<label>Slug<input id="topic-slug-input" value="' + esc(source.slug || '') + '" placeholder="rendering" /></label>' +
250
+ '<label>Description<textarea id="topic-description-input" placeholder="What belongs in this topic?">' + esc(source.description || '') + '</textarea></label>' +
251
+ '<label>Keywords<input id="topic-keywords-input" value="' + esc((source.keywords || []).join(', ')) + '" placeholder="shader, frame, gpu, lighting" /></label>' +
252
+ '<div class="topic-editor-actions">' +
253
+ '<button class="btn btn-primary" type="submit">Save</button>' +
254
+ '<button class="btn btn-sm" type="button" data-ref-action="cancelEditor">Cancel</button>' +
255
+ '</div>' +
256
+ '</form>' +
257
+ '</div>';
258
+ }
259
+ function renderTopicSummary(slug) {
260
+ var topic = findTopic(slug);
261
+ var doc = findTopicDoc(slug);
262
+ var reader = document.getElementById('reference-reader');
263
+ if (!topic || !reader) return;
264
+ var actions = '<button class="btn btn-sm" data-ref-action="addTopic">Add topic</button>' +
265
+ '<button class="btn btn-sm" data-ref-action="reclassify">Reclassify archived findings</button>' +
266
+ '<button class="btn btn-sm" data-ref-action="editTopic" data-slug="' + esc(topic.slug) + '">Edit</button>';
267
+ if (topic.slug !== 'general') actions += '<button class="btn btn-sm" data-ref-action="deleteTopic" data-slug="' + esc(topic.slug) + '">Delete</button>';
268
+ if (!doc || !doc.exists) {
269
+ reader.innerHTML = readerToolbar(topic.label, 'reference/topics/' + topic.slug + '.md', actions) +
270
+ '<div class="topic-empty">' +
271
+ '<p>No archived entries have landed here yet. Saving the topic created the bucket, but it stays empty until findings are archived or legacy topic docs are reclassified.</p>' +
272
+ (topic.description ? '<p>' + esc(topic.description) + '</p>' : '') +
273
+ ((topic.keywords || []).length ? '<div class="topic-keywords">' + topic.keywords.map(function(keyword) { return '<span class="topic-keyword">' + esc(keyword) + '</span>'; }).join('') + '</div>' : '') +
274
+ '</div>';
275
+ return;
276
+ }
277
+ reader.innerHTML = readerToolbar(topic.label, doc.file, actions) + '<div class="reader-content"><div class="reader-empty">Loading...</div></div>';
278
+ loadJson('/api/project-reference-content?project=' + encodeURIComponent(_referenceState.project) + '&file=' + encodeURIComponent(doc.file)).then(function(data) {
279
+ var liveReader = document.getElementById('reference-reader');
280
+ if (!liveReader) return;
281
+ if (!data.ok) {
282
+ liveReader.innerHTML = readerToolbar(topic.label, doc.file, actions) + '<div class="reader-empty">' + esc(data.error || 'File not found') + '</div>';
283
+ return;
284
+ }
285
+ liveReader.innerHTML = readerToolbar(topic.label, doc.file, actions) + '<div class="reader-content"><pre>' + esc(data.content) + '</pre></div>';
286
+ });
287
+ }
288
+ function renderReferenceFile(file) {
289
+ var reader = document.getElementById('reference-reader');
290
+ if (!reader) return;
291
+ var actions = '<button class="btn btn-sm" data-ref-action="addTopic">Add topic</button><button class="btn btn-sm" data-ref-action="reclassify">Reclassify archived findings</button>';
292
+ reader.innerHTML = readerToolbar(file.title || file.file, file.file, actions) + '<div class="reader-content"><div class="reader-empty">Loading...</div></div>';
293
+ loadJson('/api/project-reference-content?project=' + encodeURIComponent(_referenceState.project) + '&file=' + encodeURIComponent(file.file)).then(function(data) {
294
+ var liveReader = document.getElementById('reference-reader');
295
+ if (!liveReader) return;
296
+ if (!data.ok) {
297
+ liveReader.innerHTML = readerToolbar(file.title || file.file, file.file, actions) + '<div class="reader-empty">' + esc(data.error || 'File not found') + '</div>';
298
+ return;
299
+ }
300
+ liveReader.innerHTML = readerToolbar(file.title || file.file, file.file, actions) + '<div class="reader-content"><pre>' + esc(data.content) + '</pre></div>';
301
+ });
302
+ }
303
+ function renderReferenceSidebar() {
304
+ var container = document.getElementById('project-content');
305
+ if (!container || !_referenceState.topicsData || !_referenceState.referenceData) return;
306
+ var topicsData = _referenceState.topicsData;
307
+ var referenceData = _referenceState.referenceData;
308
+ var topicRows = (topicsData.topics || []).map(function(topic) {
309
+ var doc = findTopicDoc(topic.slug);
310
+ var selected = _referenceState.selectedType === 'topic' && _referenceState.selectedKey === topic.slug ? ' selected' : '';
311
+ var meta = (doc && doc.exists ? doc.entryCount + ' entries' : 'empty bucket') + (topicsData.source === 'default' ? ' · starter' : '');
312
+ return '<div class="split-item' + selected + '" data-ref-action="selectTopic" data-slug="' + esc(topic.slug) + '">' +
313
+ '<div class="reference-item-main"><span class="reference-item-title">' + esc(topic.label) + '</span><span class="reference-item-meta">' + esc(meta) + '</span></div>' +
314
+ '</div>';
315
+ }).join('');
316
+ var suggestionRows = (topicsData.suggestions || []).length
317
+ ? topicsData.suggestions.map(function(suggestion) {
318
+ var confidencePct = Math.round((Number(suggestion.confidence || 0)) * 100);
319
+ var meta = suggestion.reason + (confidencePct > 0 ? ' · confidence ' + confidencePct + '%' : '');
320
+ var isPinned = suggestion.source === 'pinned';
321
+ return '<div class="split-item">' +
322
+ '<div class="reference-item-main"><span class="reference-item-title">' + esc(suggestion.label) + '</span><span class="reference-item-meta">' + esc(meta) + '</span></div>' +
323
+ (isPinned
324
+ ? '<button class="btn btn-sm reference-item-action" data-ref-action="unpinSuggestion" data-slug="' + esc(suggestion.slug) + '">Unpin</button>'
325
+ : '<button class="btn btn-sm reference-item-action" data-ref-action="pinSuggestion" data-slug="' + esc(suggestion.slug) + '">Pin</button><button class="btn btn-sm reference-item-action" data-ref-action="useSuggestion" data-slug="' + esc(suggestion.slug) + '">Use</button>') +
326
+ '</div>';
327
+ }).join('')
328
+ : '<div class="reference-sidebar-note">No topic suggestions right now.</div>';
329
+ var legacyByFile = {};
330
+ (topicsData.legacyDocs || []).forEach(function(doc) { legacyByFile[doc.file] = doc; });
331
+ var otherRows = (referenceData.otherDocs || []).length
332
+ ? referenceData.otherDocs.map(function(file) {
333
+ var selected = _referenceState.selectedType === 'file' && _referenceState.selectedKey === file.file ? ' selected' : '';
334
+ var legacy = legacyByFile[file.file];
335
+ var suffix = legacy ? (legacy.eligible ? ' · legacy topic doc' : ' · legacy skip: ' + legacy.reason) : '';
336
+ return '<div class="split-item' + selected + '" data-ref-action="selectFile" data-file="' + esc(file.file) + '">' +
337
+ '<div class="reference-item-main"><span class="reference-item-title">' + esc(file.title || file.file) + '</span><span class="reference-item-meta">' + esc(file.file + suffix) + '</span></div>' +
338
+ '</div>';
339
+ }).join('')
340
+ : '<div class="reference-sidebar-note">No other reference docs.</div>';
341
+ var banner = topicsData.source === 'default'
342
+ ? '<div class="reference-banner">Starter topics are active for this project. Add project-owned topics to make archives match the actual domain.</div>'
343
+ : '';
344
+ container.innerHTML = banner +
345
+ '<div class="split-view project-reference-shell">' +
346
+ '<div class="split-sidebar">' +
347
+ '<div class="reference-sidebar-toolbar"><button class="btn btn-sm" data-ref-action="addTopic">Add topic</button><button class="btn btn-sm" data-ref-action="reclassify">Reclassify</button></div>' +
348
+ '<div class="split-group-label">Topics</div>' +
349
+ topicRows +
350
+ '<div class="split-group-label">Suggested Topics</div>' +
351
+ suggestionRows +
352
+ '<div class="split-group-label">Other Reference Docs</div>' +
353
+ otherRows +
354
+ '</div>' +
355
+ '<div class="split-reader" id="reference-reader"></div>' +
356
+ '</div>';
357
+ if (_referenceState.editor) {
358
+ renderTopicEditor(_referenceState.editor.mode, _referenceState.editor.topic, _referenceState.editor.suggestion);
359
+ return;
360
+ }
361
+ if (_referenceState.selectedType === 'file') {
362
+ var selectedFile = (referenceData.otherDocs || []).find(function(file) { return file.file === _referenceState.selectedKey; });
363
+ if (selectedFile) {
364
+ renderReferenceFile(selectedFile);
365
+ return;
366
+ }
367
+ }
368
+ if (_referenceState.selectedType === 'topic' && findTopic(_referenceState.selectedKey)) {
369
+ renderTopicSummary(_referenceState.selectedKey);
370
+ return;
371
+ }
372
+ renderReferenceHome();
373
+ }
374
+ function loadReferenceState(nextType, nextKey) {
375
+ var project = currentProject();
376
+ if (!project) return;
377
+ _referenceState.project = project;
378
+ loadJson('/api/project-topics?project=' + encodeURIComponent(project)).then(function(topicsData) {
379
+ if (!topicsData.ok) throw new Error(topicsData.error || 'Failed to load topics');
380
+ return Promise.all([topicsData, loadJson('/api/project-reference-list?project=' + encodeURIComponent(project))]);
381
+ }).then(function(results) {
382
+ var topicsData = results[0];
383
+ var referenceData = results[1];
384
+ if (!referenceData.ok) throw new Error(referenceData.error || 'Failed to load reference docs');
385
+ _referenceState.topicsData = topicsData;
386
+ _referenceState.referenceData = referenceData;
387
+ if (nextType) _referenceState.selectedType = nextType;
388
+ if (nextKey !== undefined) _referenceState.selectedKey = nextKey;
389
+ renderReferenceSidebar();
390
+ }).catch(function(err) {
391
+ var container = document.getElementById('project-content');
392
+ if (container) container.innerHTML = '<div class="project-detail-empty">' + esc(err && err.message ? err.message : 'Failed to load reference data') + '</div>';
393
+ });
394
+ }
395
+ function saveTopics(nextTopics, onDone) {
396
+ fetchCsrfToken(function(csrfToken) {
397
+ var body = 'project=' + encodeURIComponent(_referenceState.project) + '&topics=' + encodeURIComponent(JSON.stringify(nextTopics));
398
+ if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
399
+ fetch('/api/project-topics/save', {
400
+ method: 'POST',
401
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
402
+ body: authBody(body)
403
+ }).then(function(r) { return r.json(); }).then(function(data) {
404
+ if (!data.ok) {
405
+ setStatus(data.error || 'Save failed', 'err');
406
+ return;
407
+ }
408
+ _referenceState.topicsData = data;
409
+ _referenceState.editor = null;
410
+ loadReferenceState('topic', onDone || _referenceState.selectedKey || 'general');
411
+ setStatus('Saved', 'ok');
412
+ }).catch(function() { setStatus('Save failed', 'err'); });
413
+ });
414
+ }
415
+ function collectEditorTopic() {
416
+ var label = document.getElementById('topic-label-input');
417
+ var slug = document.getElementById('topic-slug-input');
418
+ var description = document.getElementById('topic-description-input');
419
+ var keywords = document.getElementById('topic-keywords-input');
420
+ return {
421
+ slug: slug ? slug.value : '',
422
+ label: label ? label.value : '',
423
+ description: description ? description.value : '',
424
+ keywords: keywords ? keywords.value.split(',').map(function(item) { return item.trim(); }).filter(Boolean) : []
425
+ };
426
+ }
427
+ window.phrenLoadProjectReference = function() {
428
+ _referenceState.editor = null;
429
+ loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
430
+ };
431
+ window.phrenReferenceSelectTopic = function(slug) {
432
+ _referenceState.editor = null;
433
+ _referenceState.selectedType = 'topic';
434
+ _referenceState.selectedKey = slug;
435
+ renderReferenceSidebar();
436
+ };
437
+ window.phrenReferenceSelectFile = function(file) {
438
+ _referenceState.editor = null;
439
+ _referenceState.selectedType = 'file';
440
+ _referenceState.selectedKey = file;
441
+ renderReferenceSidebar();
442
+ };
443
+ window.phrenReferenceAddTopic = function() {
444
+ _referenceState.editor = { mode: 'add', topic: null, suggestion: null };
445
+ _referenceState.selectedType = '';
446
+ _referenceState.selectedKey = '';
447
+ renderReferenceSidebar();
448
+ };
449
+ window.phrenReferenceEditTopic = function(slug) {
450
+ var topic = findTopic(slug);
451
+ if (!topic) return;
452
+ _referenceState.editor = { mode: 'edit', topic: topic, suggestion: null };
453
+ renderReferenceSidebar();
454
+ };
455
+ window.phrenReferenceCancelEditor = function() {
456
+ _referenceState.editor = null;
457
+ renderReferenceSidebar();
458
+ };
459
+ window.phrenReferenceSaveTopic = function(e) {
460
+ e.preventDefault();
461
+ if (!_referenceState.topicsData) return;
462
+ var topic = collectEditorTopic();
463
+ var topics = (_referenceState.topicsData.topics || []).slice();
464
+ if (_referenceState.editor && _referenceState.editor.mode === 'edit' && _referenceState.editor.topic) {
465
+ topics = topics.map(function(item) { return item.slug === _referenceState.editor.topic.slug ? topic : item; });
466
+ } else {
467
+ topics.push(topic);
468
+ }
469
+ saveTopics(topics, topic.slug || 'general');
470
+ };
471
+ window.phrenReferenceDeleteTopic = function(slug) {
472
+ if (slug === 'general' || !_referenceState.topicsData) return;
473
+ var topics = (_referenceState.topicsData.topics || []).filter(function(topic) { return topic.slug !== slug; });
474
+ saveTopics(topics, 'general');
475
+ };
476
+ window.phrenReferenceUseSuggestion = function(slug) {
477
+ if (!_referenceState.topicsData) return;
478
+ var suggestion = (_referenceState.topicsData.suggestions || []).find(function(item) { return item.slug === slug; });
479
+ if (!suggestion) return;
480
+ var topics = (_referenceState.topicsData.topics || []).slice();
481
+ topics.push({
482
+ slug: suggestion.slug,
483
+ label: suggestion.label,
484
+ description: suggestion.description,
485
+ keywords: suggestion.keywords || []
486
+ });
487
+ saveTopics(topics, suggestion.slug);
488
+ };
489
+ window.phrenReferencePinSuggestion = function(slug) {
490
+ if (!_referenceState.topicsData) return;
491
+ var suggestion = (_referenceState.topicsData.suggestions || []).find(function(item) { return item.slug === slug; });
492
+ if (!suggestion) return;
493
+ fetchCsrfToken(function(csrfToken) {
494
+ var topicPayload = JSON.stringify({
495
+ slug: suggestion.slug,
496
+ label: suggestion.label,
497
+ description: suggestion.description,
498
+ keywords: suggestion.keywords || []
499
+ });
500
+ var body = 'project=' + encodeURIComponent(_referenceState.project) + '&topic=' + encodeURIComponent(topicPayload);
501
+ if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
502
+ fetch('/api/project-topics/pin', {
503
+ method: 'POST',
504
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
505
+ body: authBody(body)
506
+ }).then(function(r) { return r.json(); }).then(function(data) {
507
+ if (!data.ok) {
508
+ setStatus(data.error || 'Pin failed', 'err');
509
+ return;
510
+ }
511
+ _referenceState.topicsData = data;
512
+ loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
513
+ setStatus('Pinned', 'ok');
514
+ }).catch(function() { setStatus('Pin failed', 'err'); });
515
+ });
516
+ };
517
+ window.phrenReferenceUnpinSuggestion = function(slug) {
518
+ fetchCsrfToken(function(csrfToken) {
519
+ var body = 'project=' + encodeURIComponent(_referenceState.project) + '&slug=' + encodeURIComponent(slug || '');
520
+ if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
521
+ fetch('/api/project-topics/unpin', {
522
+ method: 'POST',
523
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
524
+ body: authBody(body)
525
+ }).then(function(r) { return r.json(); }).then(function(data) {
526
+ if (!data.ok) {
527
+ setStatus(data.error || 'Unpin failed', 'err');
528
+ return;
529
+ }
530
+ _referenceState.topicsData = data;
531
+ loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
532
+ setStatus('Unpinned', 'ok');
533
+ }).catch(function() { setStatus('Unpin failed', 'err'); });
534
+ });
535
+ };
536
+ window.phrenReferenceReclassify = function() {
537
+ fetchCsrfToken(function(csrfToken) {
538
+ var body = 'project=' + encodeURIComponent(_referenceState.project);
539
+ if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
540
+ fetch('/api/project-topics/reclassify', {
541
+ method: 'POST',
542
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
543
+ body: authBody(body)
544
+ }).then(function(r) { return r.json(); }).then(function(data) {
545
+ if (!data.ok) {
546
+ setStatus(data.error || 'Reclassify failed', 'err');
547
+ return;
548
+ }
549
+ loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
550
+ setStatus('Moved ' + (data.movedEntries || 0) + ' entries; skipped ' + ((data.skipped || []).length) + ' docs', 'ok');
551
+ }).catch(function() { setStatus('Reclassify failed', 'err'); });
552
+ });
553
+ };
554
+ // Event delegation for dynamically generated reference UI
555
+ document.addEventListener('click', function(e) {
556
+ var target = e.target;
557
+ if (!target || typeof target.closest !== 'function') return;
558
+ var actionEl = target.closest('[data-ref-action]');
559
+ if (!actionEl) return;
560
+ var action = actionEl.getAttribute('data-ref-action');
561
+ if (action === 'addTopic') { phrenReferenceAddTopic(); }
562
+ else if (action === 'reclassify') { phrenReferenceReclassify(); }
563
+ else if (action === 'cancelEditor') { phrenReferenceCancelEditor(); }
564
+ else if (action === 'editTopic') { phrenReferenceEditTopic(actionEl.getAttribute('data-slug')); }
565
+ else if (action === 'deleteTopic') { phrenReferenceDeleteTopic(actionEl.getAttribute('data-slug')); }
566
+ else if (action === 'selectTopic') { phrenReferenceSelectTopic(actionEl.getAttribute('data-slug')); }
567
+ else if (action === 'selectFile') { phrenReferenceSelectFile(actionEl.getAttribute('data-file')); }
568
+ else if (action === 'useSuggestion') { e.stopPropagation(); phrenReferenceUseSuggestion(actionEl.getAttribute('data-slug')); }
569
+ else if (action === 'pinSuggestion') { e.stopPropagation(); phrenReferencePinSuggestion(actionEl.getAttribute('data-slug')); }
570
+ else if (action === 'unpinSuggestion') { e.stopPropagation(); phrenReferenceUnpinSuggestion(actionEl.getAttribute('data-slug')); }
571
+ });
572
+ document.addEventListener('submit', function(e) {
573
+ var form = e.target;
574
+ if (form && form.id === 'topic-editor-form') {
575
+ e.preventDefault();
576
+ phrenReferenceSaveTopic(e);
577
+ }
578
+ });
579
+ })();`;
580
+ }
581
+ export function renderReviewQueueEditSyncScript() {
582
+ return `(function() {
583
+ function normalizeQueueText(raw) {
584
+ return String(raw == null ? '' : raw)
585
+ .replace(/\\r\\n?/g, '\\n')
586
+ .replace(/\\0/g, ' ')
587
+ .replace(/<!--[\\s\\S]*?-->/g, ' ')
588
+ .replace(/\\\\[nrt]/g, ' ')
589
+ .replace(/\\\\\"/g, '"')
590
+ .replace(/\\\\\\\\/g, '\\\\')
591
+ .replace(/\\n+/g, ' ')
592
+ .replace(/\\s+/g, ' ')
593
+ .trim();
594
+ }
595
+
596
+ function rebuildEditedQueueLine(line, newText) {
597
+ var dateMatch = String(line || '').match(/^- \\[(\\d{4}-\\d{2}-\\d{2})\\]/);
598
+ var confidenceMatch = String(line || '').match(/\\[confidence\\s+([01](?:\\.\\d+)?)\\]/i);
599
+ var normalizedText = normalizeQueueText(newText);
600
+ var date = dateMatch ? dateMatch[1] : new Date().toISOString().slice(0, 10);
601
+ var confidencePart = confidenceMatch
602
+ ? ' [confidence ' + Number(confidenceMatch[1]).toFixed(2) + ']'
603
+ : '';
604
+ return {
605
+ text: normalizedText,
606
+ line: '- [' + date + '] ' + normalizedText + confidencePart
607
+ };
608
+ }
609
+
610
+ function escapeHtml(text) {
611
+ return String(text)
612
+ .replace(/&/g, '&amp;')
613
+ .replace(/</g, '&lt;')
614
+ .replace(/>/g, '&gt;')
615
+ .replace(/"/g, '&quot;');
616
+ }
617
+
618
+ function syncEditedCard(card, project, nextLine, nextText) {
619
+ if (!card || !project || !nextLine) return;
620
+ card.setAttribute('data-key', project + '\\\\x00' + nextLine);
621
+ card.setAttribute('data-project', project);
622
+ var approveBtn = card.querySelector('.btn-approve');
623
+ if (approveBtn) {
624
+ approveBtn.setAttribute('data-project', project);
625
+ approveBtn.setAttribute('data-line', nextLine);
626
+ }
627
+ var rejectBtn = card.querySelector('.btn-reject');
628
+ if (rejectBtn) {
629
+ rejectBtn.setAttribute('data-project', project);
630
+ rejectBtn.setAttribute('data-line', nextLine);
631
+ }
632
+ var editForm = card.querySelector('.review-card-edit form');
633
+ if (editForm) {
634
+ editForm.setAttribute('data-project', project);
635
+ editForm.setAttribute('data-line', nextLine);
636
+ }
637
+ var editTextarea = card.querySelector('textarea[name="new_text"]');
638
+ if (editTextarea) editTextarea.value = nextText;
639
+ }
640
+
641
+ function maybeSyncEditedCard(card, project, line, newText, attemptsLeft) {
642
+ if (!card || !project) return;
643
+ var rebuilt = rebuildEditedQueueLine(line, newText);
644
+ var textEl = card.querySelector('.review-card-text');
645
+ var editSection = card.querySelector('.review-card-edit');
646
+ if (editSection && editSection.style.display === 'none') {
647
+ if (textEl) textEl.innerHTML = escapeHtml(rebuilt.text);
648
+ syncEditedCard(card, project, rebuilt.line, rebuilt.text);
649
+ return;
650
+ }
651
+ if (attemptsLeft > 0) {
652
+ setTimeout(function() {
653
+ maybeSyncEditedCard(card, project, line, newText, attemptsLeft - 1);
654
+ }, 150);
655
+ }
656
+ }
657
+
658
+ document.addEventListener('submit', function(event) {
659
+ var form = event.target;
660
+ if (!form || typeof form.getAttribute !== 'function' || typeof form.querySelector !== 'function') return;
661
+ if (!form.closest || !form.closest('.review-card-edit')) return;
662
+ var project = form.getAttribute('data-project') || '';
663
+ var line = form.getAttribute('data-line') || '';
664
+ var textarea = form.querySelector('textarea[name="new_text"]');
665
+ var newText = textarea ? textarea.value : '';
666
+ var card = form.closest('.review-card');
667
+ setTimeout(function() {
668
+ maybeSyncEditedCard(card, project, line, newText, 20);
669
+ }, 0);
670
+ }, true);
671
+ })();`;
672
+ }
673
+ export function renderTasksAndSettingsScript(authToken) {
674
+ return `(function() {
675
+ var _tsAuthToken = '${authToken}';
676
+ var _allTasks = [];
677
+
678
+ function tsAuthUrl(base) {
679
+ return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_tsAuthToken);
680
+ }
681
+
682
+ function esc(s) {
683
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
684
+ }
685
+
686
+ function priorityBadge(p) {
687
+ if (!p) return '';
688
+ var colors = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280' };
689
+ var color = colors[p] || '#6b7280';
690
+ return '<span class="task-priority-badge task-priority-' + esc(p) + '">' + esc(p) + '</span>';
691
+ }
692
+
693
+ function statusChip(section, checked) {
694
+ if (checked || section === 'Done') return '<span class="task-status-chip task-status-done" title="Done"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Done</span>';
695
+ if (section === 'Active') return '<span class="task-status-chip task-status-active" title="In Progress">In Progress</span>';
696
+ return '<span class="task-status-chip task-status-pending" title="Pending">Pending</span>';
697
+ }
698
+
699
+ function projectBadge(proj) {
700
+ return '<span class="task-project-badge">' + esc(proj) + '</span>';
701
+ }
702
+
703
+ function pinIndicator(pinned) {
704
+ if (!pinned) return '';
705
+ return '<span class="task-pin-indicator" title="Pinned"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9.828 2.172a2 2 0 0 1 2.828 0l1.172 1.172a2 2 0 0 1 0 2.828L12 8l-1.5 1.5.5 3.5L8 10l-3.5.5.5-3.5L3.5 5.5 2 4l2.172-1.828a2 2 0 0 1 2.828 0L8 3l1.828-.828z"/></svg></span>';
706
+ }
707
+
708
+ function githubBadge(issue, url) {
709
+ if (!issue || !url) return '';
710
+ return '<a href="' + esc(url) + '" target="_blank" rel="noopener noreferrer" class="task-github-badge" title="GitHub Issue #' + esc(String(issue)) + '"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>#' + esc(String(issue)) + '</a>';
711
+ }
712
+
713
+ function loadTasks() {
714
+ var url = _tsAuthToken ? tsAuthUrl('/api/tasks') : '/api/tasks';
715
+ fetch(url).then(function(r) { return r.json(); }).then(function(data) {
716
+ _allTasks = data.tasks || [];
717
+ populateTaskProjectFilter();
718
+ filterTasks();
719
+ }).catch(function(err) {
720
+ document.getElementById('tasks-list').innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">Failed to load tasks: ' + esc(String(err)) + '</div>';
721
+ });
722
+ }
723
+
724
+ window.completeTaskFromUi = function(project, item) {
725
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
726
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
727
+ var body = new URLSearchParams({ project: project, item: item });
728
+ if (csrfData.token) body.set('_csrf', csrfData.token);
729
+ var url = _tsAuthToken ? tsAuthUrl('/api/tasks/complete') : '/api/tasks/complete';
730
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
731
+ }).then(function(r) { return r.json(); }).then(function(data) {
732
+ if (data.ok) { loadTasks(); } else { alert(data.error || 'Failed to complete task'); }
733
+ }).catch(function(err) { alert('Error: ' + String(err)); });
734
+ };
735
+
736
+ window.removeTaskFromUi = function(project, item) {
737
+ if (!confirm('Delete this task?')) return;
738
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
739
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
740
+ var body = new URLSearchParams({ project: project, item: item });
741
+ if (csrfData.token) body.set('_csrf', csrfData.token);
742
+ var url = _tsAuthToken ? tsAuthUrl('/api/tasks/remove') : '/api/tasks/remove';
743
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
744
+ }).then(function(r) { return r.json(); }).then(function(data) {
745
+ if (data.ok) { loadTasks(); } else { alert(data.error || 'Failed to remove task'); }
746
+ }).catch(function(err) { alert('Error: ' + String(err)); });
747
+ };
748
+
749
+ window.addTaskFromUi = function(project) {
750
+ var input = document.getElementById('task-add-input-' + project);
751
+ if (!input || !input.value.trim()) return;
752
+ var item = input.value.trim();
753
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
754
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
755
+ var body = new URLSearchParams({ project: project, item: item });
756
+ if (csrfData.token) body.set('_csrf', csrfData.token);
757
+ var url = _tsAuthToken ? tsAuthUrl('/api/tasks/add') : '/api/tasks/add';
758
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
759
+ }).then(function(r) { return r.json(); }).then(function(data) {
760
+ if (data.ok) { input.value = ''; loadTasks(); } else { alert(data.error || 'Failed to add task'); }
761
+ }).catch(function(err) { alert('Error: ' + String(err)); });
762
+ };
763
+
764
+ window.toggleDoneSection = function(btn) {
765
+ var list = btn.nextElementSibling;
766
+ var arrow = btn.querySelector('.task-toggle-arrow');
767
+ if (!list) return;
768
+ var isOpen = list.style.display !== 'none';
769
+ list.style.display = isOpen ? 'none' : 'block';
770
+ if (arrow) arrow.textContent = isOpen ? '\u25B6' : '\u25BC';
771
+ };
772
+
773
+ function populateTaskProjectFilter() {
774
+ var projects = Array.from(new Set(_allTasks.map(function(t) { return t.project; }))).sort();
775
+ var sel = document.getElementById('tasks-filter-project');
776
+ if (!sel) return;
777
+ var html = '<option value="">All projects</option>';
778
+ projects.forEach(function(p) { html += '<option value="' + esc(p) + '">' + esc(p) + '</option>'; });
779
+ sel.innerHTML = html;
780
+ }
781
+
782
+ window.filterTasks = function() {
783
+ var projectFilter = (document.getElementById('tasks-filter-project') || {}).value || '';
784
+ var sectionFilter = (document.getElementById('tasks-filter-section') || {}).value || '';
785
+ var showDone = sectionFilter === 'Done';
786
+ var tasks = _allTasks.filter(function(t) {
787
+ if (projectFilter && t.project !== projectFilter) return false;
788
+ if (sectionFilter && t.section !== sectionFilter) return false;
789
+ if (!sectionFilter && t.section === 'Done') return false;
790
+ return true;
791
+ });
792
+ var doneTasks = showDone ? [] : _allTasks.filter(function(t) {
793
+ if (projectFilter && t.project !== projectFilter) return false;
794
+ return t.section === 'Done';
795
+ });
796
+
797
+ var activeCount = tasks.filter(function(t) { return t.section !== 'Done'; }).length;
798
+ var countEl = document.getElementById('tasks-count');
799
+ if (countEl) countEl.textContent = activeCount + ' active' + (doneTasks.length ? ', ' + doneTasks.length + ' done' : '');
800
+
801
+ var container = document.getElementById('tasks-list');
802
+ if (!container) return;
803
+ if (!tasks.length && !doneTasks.length) {
804
+ container.innerHTML = '<div class="task-empty-state"><svg viewBox="0 0 48 48" width="64" height="64" style="display:block;margin:0 auto 16px"><ellipse cx="24" cy="24" rx="16" ry="15" fill="#7B68AE" opacity="0.25"/><ellipse cx="24" cy="24" rx="12" ry="11.5" fill="#7B68AE" opacity="0.4"/><circle cx="19" cy="22" r="1.5" fill="#2D2255"/><circle cx="29" cy="22" r="1.5" fill="#2D2255"/><path d="M21 28c1 1.2 2.5 1.5 3.5 1.3 1-.2 2-1 2.5-1.3" stroke="#2D2255" stroke-width="1" fill="none" stroke-linecap="round"/></svg><div style="font-size:var(--text-md);font-weight:600;color:var(--ink);margin-bottom:4px">Nothing to do!</div><div style="color:var(--muted);font-size:var(--text-sm)">Add a task to get started.</div></div>';
805
+ return;
806
+ }
807
+
808
+ // Group by priority within sections
809
+ var priorityOrder = { high: 0, medium: 1, low: 2 };
810
+ function sortByPriority(a, b) {
811
+ var pa = priorityOrder[a.priority] !== undefined ? priorityOrder[a.priority] : 1;
812
+ var pb = priorityOrder[b.priority] !== undefined ? priorityOrder[b.priority] : 1;
813
+ if (a.pinned && !b.pinned) return -1;
814
+ if (!a.pinned && b.pinned) return 1;
815
+ return pa - pb;
816
+ }
817
+
818
+ // Separate into priority groups
819
+ var high = tasks.filter(function(t) { return t.priority === 'high' && t.section !== 'Done'; }).sort(sortByPriority);
820
+ var medium = tasks.filter(function(t) { return t.priority === 'medium' && t.section !== 'Done'; }).sort(sortByPriority);
821
+ var low = tasks.filter(function(t) { return t.priority === 'low' && t.section !== 'Done'; }).sort(sortByPriority);
822
+ var noPriority = tasks.filter(function(t) { return !t.priority && t.section !== 'Done'; }).sort(sortByPriority);
823
+ var doneVisible = tasks.filter(function(t) { return t.section === 'Done'; });
824
+
825
+ function renderTaskCard(t) {
826
+ var borderClass = t.priority === 'high' ? ' task-card-high' : t.priority === 'medium' ? ' task-card-medium' : t.priority === 'low' ? ' task-card-low' : '';
827
+ var doneClass = (t.section === 'Done' || t.checked) ? ' task-card-done' : '';
828
+ var html = '<div class="task-card' + borderClass + doneClass + '">';
829
+ html += '<div class="task-card-top">';
830
+ html += statusChip(t.section, t.checked);
831
+ html += projectBadge(t.project);
832
+ html += pinIndicator(t.pinned);
833
+ html += githubBadge(t.githubIssue, t.githubUrl);
834
+ html += priorityBadge(t.priority);
835
+ html += '</div>';
836
+ html += '<div class="task-card-body">';
837
+ html += '<span class="task-card-text">' + esc(t.line) + '</span>';
838
+ if (t.context) html += '<span class="task-card-context">' + esc(t.context) + '</span>';
839
+ html += '</div>';
840
+ html += '<div class="task-card-actions">';
841
+ if (t.section !== 'Done') {
842
+ html += '<button class="task-done-btn" onclick="completeTaskFromUi(\'' + esc(t.project).replace(/'/g, "\\'") + '\', \'' + esc(t.line).replace(/'/g, "\\'") + '\')" title="Mark done"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Done</button>';
843
+ }
844
+ html += '<button class="task-remove-btn" onclick="removeTaskFromUi(\'' + esc(t.project).replace(/'/g, "\\'") + '\', \'' + esc(t.line).replace(/'/g, "\\'") + '\')" title="Delete task" style="background:none;border:1px solid var(--border);border-radius:var(--radius-sm);padding:2px 8px;cursor:pointer;color:var(--muted);font-size:var(--text-xs)"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>';
845
+ html += '</div>';
846
+ html += '</div>';
847
+ return html;
848
+ }
849
+
850
+ var html = '';
851
+
852
+ // Add task input at top
853
+ var projects = projectFilter ? [projectFilter] : Array.from(new Set(_allTasks.map(function(t) { return t.project; }))).sort();
854
+ projects.forEach(function(proj) {
855
+ html += '<div class="task-add-bar">';
856
+ html += '<input id="task-add-input-' + esc(proj) + '" type="text" class="task-add-input" placeholder="Add a task to ' + esc(proj) + '\u2026" onkeydown="if(event.key===\'Enter\')addTaskFromUi(\'' + esc(proj).replace(/'/g, "\\'") + '\')">';
857
+ html += '<button class="task-add-btn" onclick="addTaskFromUi(\'' + esc(proj).replace(/'/g, "\\'") + '\')"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> Add</button>';
858
+ html += '</div>';
859
+ });
860
+
861
+ // Priority sections
862
+ function renderSection(title, items, icon) {
863
+ if (!items.length) return '';
864
+ var shtml = '<div class="task-priority-section">';
865
+ shtml += '<div class="task-section-header"><span class="task-section-icon">' + icon + '</span> ' + title + ' <span class="task-section-count">' + items.length + '</span></div>';
866
+ shtml += '<div class="task-card-grid">';
867
+ items.forEach(function(t) { shtml += renderTaskCard(t); });
868
+ shtml += '</div></div>';
869
+ return shtml;
870
+ }
871
+
872
+ html += renderSection('High Priority', high, '<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="#ef4444"/></svg>');
873
+ html += renderSection('Medium Priority', medium, '<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="#f59e0b"/></svg>');
874
+ html += renderSection('Low Priority', low, '<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="#6b7280"/></svg>');
875
+ if (noPriority.length) {
876
+ html += renderSection('Tasks', noPriority, '<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="var(--accent)"/></svg>');
877
+ }
878
+
879
+ // Done section (collapsible)
880
+ var allDone = showDone ? doneVisible : doneTasks;
881
+ if (allDone.length) {
882
+ html += '<div class="task-done-section">';
883
+ html += '<button class="task-done-toggle" onclick="toggleDoneSection(this)">';
884
+ html += '<span class="task-toggle-arrow">\u25B6</span> Completed <span class="task-section-count">' + allDone.length + '</span></button>';
885
+ html += '<div class="task-done-list" style="display:none">';
886
+ html += '<div class="task-card-grid">';
887
+ allDone.forEach(function(t) { html += renderTaskCard(t); });
888
+ html += '</div></div></div>';
889
+ }
890
+
891
+ container.innerHTML = html;
892
+ };
893
+
894
+ function setSettingsStatus(message, type) {
895
+ var el = document.getElementById('settings-status-inline');
896
+ if (!el) return;
897
+ el.textContent = message || '';
898
+ el.className = 'settings-status-inline' + (type ? ' ' + type : '');
899
+ }
900
+
901
+ function findingUiToStorage(level) {
902
+ var map = { high: 'aggressive', medium: 'balanced', low: 'conservative', minimal: 'minimal' };
903
+ return map[level] || 'balanced';
904
+ }
905
+
906
+ function findingStorageToUi(level) {
907
+ var map = { aggressive: 'high', balanced: 'medium', conservative: 'low', minimal: 'minimal' };
908
+ return map[level] || 'medium';
909
+ }
910
+
911
+ function postSettings(endpoint, payload, okMessage) {
912
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
913
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
914
+ var body = new URLSearchParams(payload || {});
915
+ if (csrfData.token) body.set('_csrf', csrfData.token);
916
+ var url = _tsAuthToken ? tsAuthUrl(endpoint) : endpoint;
917
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
918
+ }).then(function(r) { return r.json(); }).then(function(data) {
919
+ if (!data.ok) {
920
+ setSettingsStatus(data.error || 'Settings update failed', 'err');
921
+ return;
922
+ }
923
+ _settingsLoaded = false;
924
+ loadSettings();
925
+ setSettingsStatus(okMessage || 'Settings updated', 'ok');
926
+ }).catch(function(err) {
927
+ setSettingsStatus('Settings update failed: ' + String(err), 'err');
928
+ });
929
+ }
930
+
931
+ function loadSettings() {
932
+ var url = _tsAuthToken ? tsAuthUrl('/api/settings') : '/api/settings';
933
+ fetch(url).then(function(r) { return r.json(); }).then(function(data) {
934
+ if (!data.ok) {
935
+ setSettingsStatus(data.error || 'Failed to load settings', 'err');
936
+ return;
937
+ }
938
+
939
+ var findingDescriptions = {
940
+ high: 'Capture findings proactively, including minor observations.',
941
+ medium: 'Capture findings that are likely useful.',
942
+ low: 'Capture findings only when clearly significant.',
943
+ minimal: 'Only capture explicitly flagged findings.'
944
+ };
945
+
946
+ var findingsEl = document.getElementById('settings-findings');
947
+ if (findingsEl) {
948
+ var fsUi = findingStorageToUi(data.findingSensitivity || 'balanced');
949
+ var findingsHtml = '';
950
+ findingsHtml += '<div class="settings-control">';
951
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Finding sensitivity</span></div>';
952
+ findingsHtml += '<div class="settings-chip-row">';
953
+ ['high', 'medium', 'low', 'minimal'].forEach(function(level) {
954
+ var active = level === fsUi ? ' active' : '';
955
+ findingsHtml += '<button data-ts-action="setFindingSensitivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
956
+ });
957
+ findingsHtml += '</div>';
958
+ findingsHtml += '<div class="settings-control-note" id="settings-fs-desc">' + esc(findingDescriptions[fsUi] || '') + '</div>';
959
+ findingsHtml += '</div>';
960
+ findingsHtml += '<div class="settings-control">';
961
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Auto-capture</span>';
962
+ findingsHtml += '<button data-ts-action="toggleAutoCapture" data-enabled="' + (data.autoCaptureEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.autoCaptureEnabled ? ' active' : '') + '">' + (data.autoCaptureEnabled ? 'On' : 'Off') + '</button></div>';
963
+ findingsHtml += '<div class="settings-control-note">Turn automatic finding capture on or off.</div>';
964
+ findingsHtml += '</div>';
965
+ findingsHtml += '<div class="settings-control">';
966
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Consolidation threshold</span><span class="badge">' + esc(String(data.consolidationEntryThreshold || 25)) + ' entries</span></div>';
967
+ findingsHtml += '<div class="settings-control-note">Consolidation is also recommended after 60+ days with at least 10 new entries.</div>';
968
+ findingsHtml += '</div>';
969
+ findingsEl.innerHTML = findingsHtml;
970
+ }
971
+
972
+ var behaviorEl = document.getElementById('settings-behavior');
973
+ if (behaviorEl) {
974
+ var taskMode = data.taskMode === 'suggest' ? 'manual' : (data.taskMode || 'auto');
975
+ var proactivity = data.proactivity || 'high';
976
+ var behaviorHtml = '';
977
+ behaviorHtml += '<div class="settings-control">';
978
+ behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Task mode</span></div>';
979
+ behaviorHtml += '<div class="settings-chip-row">';
980
+ ['auto', 'manual', 'off'].forEach(function(mode) {
981
+ var active = mode === taskMode ? ' active' : '';
982
+ behaviorHtml += '<button data-ts-action="setTaskMode" data-mode="' + esc(mode) + '" class="settings-chip' + active + '">' + esc(mode) + '</button>';
983
+ });
984
+ behaviorHtml += '</div></div>';
985
+ behaviorHtml += '<div class="settings-control">';
986
+ behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Proactivity level</span></div>';
987
+ behaviorHtml += '<div class="settings-chip-row">';
988
+ ['high', 'medium', 'low'].forEach(function(level) {
989
+ var active = level === proactivity ? ' active' : '';
990
+ behaviorHtml += '<button data-ts-action="setProactivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
991
+ });
992
+ behaviorHtml += '</div></div>';
993
+ behaviorEl.innerHTML = behaviorHtml;
994
+ }
995
+
996
+ var integrationsEl = document.getElementById('settings-integrations');
997
+ if (integrationsEl) {
998
+ var tools = Array.isArray(data.hookTools) ? data.hookTools : [];
999
+ var html = '';
1000
+ html += '<div class="settings-control-header" style="margin-bottom:10px"><span class="settings-control-label">Global MCP</span>';
1001
+ html += '<button data-ts-action="toggleMcpEnabled" data-enabled="' + (data.mcpEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.mcpEnabled ? ' active' : '') + '">' + (data.mcpEnabled ? 'On' : 'Off') + '</button></div>';
1002
+ html += '<table class="settings-integrations-table"><thead><tr><th>Tool</th><th>Hooks</th><th>MCP</th><th>Control</th></tr></thead><tbody>';
1003
+ tools.forEach(function(tool) {
1004
+ var hooksOn = !!tool.enabled;
1005
+ var mcpOn = !!data.mcpEnabled;
1006
+ html += '<tr>';
1007
+ html += '<td><span class="settings-tool">' + esc(tool.tool) + '</span></td>';
1008
+ html += '<td><span class="settings-indicator ' + (hooksOn ? 'on' : 'off') + '"></span>' + (hooksOn ? 'enabled' : 'disabled') + '</td>';
1009
+ html += '<td><span class="settings-indicator ' + (mcpOn ? 'on' : 'off') + '"></span>' + (mcpOn ? 'enabled' : 'disabled') + '</td>';
1010
+ html += '<td><button data-ts-action="toggleIntegrationTool" data-tool="' + esc(tool.tool) + '" class="settings-chip' + (hooksOn ? ' active' : '') + '">' + (hooksOn ? 'Disable' : 'Enable') + '</button></td>';
1011
+ html += '</tr>';
1012
+ });
1013
+ html += '</tbody></table>';
1014
+ integrationsEl.innerHTML = html;
1015
+ }
1016
+ }).catch(function(err) {
1017
+ setSettingsStatus('Failed to load settings: ' + String(err), 'err');
1018
+ });
1019
+ }
1020
+
1021
+ // Hook into switchTab to lazy-load
1022
+ var _origSwitchTab = window.switchTab;
1023
+ var _tasksLoaded = false;
1024
+ var _settingsLoaded = false;
1025
+ var _sessionsLoaded = false;
1026
+ window.switchTab = function(tab) {
1027
+ if (typeof _origSwitchTab === 'function') _origSwitchTab(tab);
1028
+ if (tab === 'tasks' && !_tasksLoaded) { _tasksLoaded = true; loadTasks(); }
1029
+ if (tab === 'settings' && !_settingsLoaded) { _settingsLoaded = true; loadSettings(); }
1030
+ if (tab === 'sessions' && !_sessionsLoaded) { _sessionsLoaded = true; loadSessions(); }
1031
+ };
1032
+
1033
+ var _sessionsData = [];
1034
+ window.loadSessions = function() {
1035
+ var projectFilter = document.getElementById('sessions-filter-project');
1036
+ var project = projectFilter ? projectFilter.value : '';
1037
+ var apiUrl = _tsAuthToken ? tsAuthUrl('/api/sessions') : '/api/sessions';
1038
+ if (project) apiUrl += (apiUrl.includes('?') ? '&' : '?') + 'project=' + encodeURIComponent(project);
1039
+ loadJson(apiUrl).then(function(data) {
1040
+ if (!data.ok) throw new Error(data.error || 'Failed to load sessions');
1041
+ _sessionsData = data.sessions || [];
1042
+ renderSessionsList(_sessionsData);
1043
+ // Populate project filter
1044
+ if (projectFilter && projectFilter.options.length <= 1) {
1045
+ var projects = {};
1046
+ _sessionsData.forEach(function(s) { if (s.project) projects[s.project] = true; });
1047
+ Object.keys(projects).sort().forEach(function(p) {
1048
+ var opt = document.createElement('option');
1049
+ opt.value = p; opt.textContent = p;
1050
+ projectFilter.appendChild(opt);
1051
+ });
1052
+ }
1053
+ }).catch(function(err) {
1054
+ var list = document.getElementById('sessions-list');
1055
+ if (list) list.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">' + esc(err.message) + '</div>';
1056
+ });
1057
+ };
1058
+
1059
+ function renderSessionsList(sessions) {
1060
+ var list = document.getElementById('sessions-list');
1061
+ var detail = document.getElementById('session-detail');
1062
+ var countEl = document.getElementById('sessions-count');
1063
+ if (detail) detail.style.display = 'none';
1064
+ if (countEl) countEl.textContent = sessions.length + ' session(s)';
1065
+ if (!list) return;
1066
+ if (sessions.length === 0) {
1067
+ list.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center"><svg viewBox="0 0 32 32" width="32" height="32" style="display:block;margin:0 auto 12px"><ellipse cx="16" cy="16" rx="10" ry="9.5" fill="#7B68AE" opacity="0.4"/><path d="M12 15l1-1 1 1-1 1z" fill="#2D2255"/><path d="M18 15l1-1 1 1-1 1z" fill="#2D2255"/><path d="M15 18.5c0.3-0.3 0.7-0.3 1 0" stroke="#2D2255" stroke-width="0.6" fill="none"/></svg>No sessions found.</div>';
1068
+ return;
1069
+ }
1070
+ list.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">' +
1071
+ '<thead><tr style="border-bottom:1px solid var(--border);text-align:left">' +
1072
+ '<th style="padding:8px">Session</th><th style="padding:8px">Project</th><th style="padding:8px">Date</th>' +
1073
+ '<th style="padding:8px">Duration</th><th style="padding:8px">Findings</th><th style="padding:8px">Status</th></tr></thead><tbody>' +
1074
+ sessions.map(function(s) {
1075
+ var id = s.sessionId.slice(0, 8);
1076
+ var date = (s.startedAt || '').slice(0, 16).replace('T', ' ');
1077
+ var dur = s.durationMins != null ? s.durationMins + 'm' : '—';
1078
+ var status = s.status === 'active' ? '<span style="color:var(--green)">● active</span>' : 'ended';
1079
+ var summarySnip = s.summary ? '<div class="text-muted" style="font-size:var(--text-xs);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(s.summary.slice(0, 80)) + '</div>' : '';
1080
+ return '<tr style="border-bottom:1px solid var(--border);cursor:pointer" data-ts-action="showSessionDetail" data-session-id="' + esc(s.sessionId) + '">' +
1081
+ '<td style="padding:8px;font-family:monospace">' + esc(id) + summarySnip + '</td>' +
1082
+ '<td style="padding:8px">' + esc(s.project || '—') + '</td>' +
1083
+ '<td style="padding:8px">' + esc(date) + '</td>' +
1084
+ '<td style="padding:8px">' + esc(dur) + '</td>' +
1085
+ '<td style="padding:8px">' + (s.findingsAdded || 0) + '</td>' +
1086
+ '<td style="padding:8px">' + status + '</td></tr>';
1087
+ }).join('') + '</tbody></table>';
1088
+ }
1089
+
1090
+ window.showSessionDetail = function(sessionId) {
1091
+ var apiUrl = _tsAuthToken ? tsAuthUrl('/api/sessions') : '/api/sessions';
1092
+ apiUrl += (apiUrl.includes('?') ? '&' : '?') + 'sessionId=' + encodeURIComponent(sessionId);
1093
+ var list = document.getElementById('sessions-list');
1094
+ var detail = document.getElementById('session-detail');
1095
+ if (list) list.style.display = 'none';
1096
+ if (detail) { detail.style.display = 'block'; detail.innerHTML = '<div style="padding:20px;color:var(--muted)">Loading session...</div>'; }
1097
+ loadJson(apiUrl).then(function(data) {
1098
+ if (!data.ok) throw new Error(data.error || 'Session not found');
1099
+ var s = data.session;
1100
+ var findings = data.findings || [];
1101
+ var tasks = data.tasks || [];
1102
+ var date = (s.startedAt || '').slice(0, 16).replace('T', ' ');
1103
+ var dur = s.durationMins != null ? s.durationMins + ' min' : '—';
1104
+ var html = '<div style="margin-bottom:12px"><button class="btn btn-sm" data-ts-action="backToSessionsList">← Back</button></div>' +
1105
+ '<div class="card" style="margin-bottom:16px"><div class="card-body">' +
1106
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px">' +
1107
+ '<div><strong>Session</strong><div class="text-muted" style="font-family:monospace">' + esc(s.sessionId.slice(0, 8)) + '</div></div>' +
1108
+ '<div><strong>Project</strong><div class="text-muted">' + esc(s.project || '—') + '</div></div>' +
1109
+ '<div><strong>Date</strong><div class="text-muted">' + esc(date) + '</div></div>' +
1110
+ '<div><strong>Duration</strong><div class="text-muted">' + esc(dur) + '</div></div>' +
1111
+ '<div><strong>Findings</strong><div class="text-muted">' + findings.length + '</div></div>' +
1112
+ '<div><strong>Tasks</strong><div class="text-muted">' + tasks.length + '</div></div>' +
1113
+ '<div><strong>Status</strong><div class="text-muted">' + esc(s.status) + '</div></div>' +
1114
+ '</div>' +
1115
+ (s.summary ? '<div style="margin-top:12px;padding:12px;background:var(--bg);border-radius:var(--radius-sm)"><strong>Summary</strong><div style="margin-top:4px">' + esc(s.summary) + '</div></div>' : '') +
1116
+ '</div></div>';
1117
+ if (findings.length > 0) {
1118
+ html += '<div class="card" style="margin-bottom:16px"><div class="card-header"><h3>Findings (' + findings.length + ')</h3></div><div class="card-body"><ul style="margin:0;padding-left:20px">';
1119
+ findings.forEach(function(f) { html += '<li style="margin-bottom:4px"><span class="text-muted">[' + esc(f.project) + ']</span> ' + esc(f.text) + '</li>'; });
1120
+ html += '</ul></div></div>';
1121
+ }
1122
+ if (tasks.length > 0) {
1123
+ html += '<div class="card" style="margin-bottom:16px"><div class="card-header"><h3>Tasks (' + tasks.length + ')</h3></div><div class="card-body"><ul style="margin:0;padding-left:20px">';
1124
+ tasks.forEach(function(t) { html += '<li style="margin-bottom:4px"><span class="text-muted">[' + esc(t.project) + '/' + esc(t.section) + ']</span> ' + esc(t.text) + '</li>'; });
1125
+ html += '</ul></div></div>';
1126
+ }
1127
+ if (detail) detail.innerHTML = html;
1128
+ }).catch(function(err) {
1129
+ if (detail) detail.innerHTML = '<div style="padding:20px;color:var(--muted)">' + esc(err.message) + '</div>';
1130
+ });
1131
+ };
1132
+
1133
+ window.backToSessionsList = function() {
1134
+ var list = document.getElementById('sessions-list');
1135
+ var detail = document.getElementById('session-detail');
1136
+ if (list) list.style.display = 'block';
1137
+ if (detail) detail.style.display = 'none';
1138
+ };
1139
+
1140
+ // Event delegation for dynamically generated tasks/settings/sessions UI
1141
+ document.addEventListener('click', function(e) {
1142
+ var target = e.target;
1143
+ if (!target || typeof target.closest !== 'function') return;
1144
+ var actionEl = target.closest('[data-ts-action]');
1145
+ if (!actionEl) return;
1146
+ var action = actionEl.getAttribute('data-ts-action');
1147
+ if (action === 'setFindingSensitivity') { setFindingSensitivity(actionEl.getAttribute('data-level')); }
1148
+ else if (action === 'toggleAutoCapture') { setAutoCapture(actionEl.getAttribute('data-enabled') !== 'true'); }
1149
+ else if (action === 'setTaskMode') { setTaskMode(actionEl.getAttribute('data-mode')); }
1150
+ else if (action === 'setProactivity') { setProactivity(actionEl.getAttribute('data-level')); }
1151
+ else if (action === 'toggleMcpEnabled') { setMcpEnabled(actionEl.getAttribute('data-enabled') !== 'true'); }
1152
+ else if (action === 'toggleIntegrationTool') { toggleIntegrationTool(actionEl.getAttribute('data-tool')); }
1153
+ else if (action === 'showSessionDetail') { showSessionDetail(actionEl.getAttribute('data-session-id')); }
1154
+ else if (action === 'backToSessionsList') { backToSessionsList(); }
1155
+ });
1156
+
1157
+ window.setFindingSensitivity = function(level) {
1158
+ var descriptions = {
1159
+ high: 'Capture findings proactively, including minor observations.',
1160
+ medium: 'Capture findings that are likely useful.',
1161
+ low: 'Capture findings only when clearly significant.',
1162
+ minimal: 'Only capture explicitly flagged findings.'
1163
+ };
1164
+ postSettings('/api/settings/finding-sensitivity', { value: findingUiToStorage(level || 'medium') }, 'Finding sensitivity updated.');
1165
+ var desc = document.getElementById('settings-fs-desc');
1166
+ if (desc) desc.textContent = descriptions[level] || level;
1167
+ };
1168
+
1169
+ window.setAutoCapture = function(enabled) {
1170
+ postSettings('/api/settings/auto-capture', { enabled: String(!!enabled) }, 'Auto-capture updated.');
1171
+ };
1172
+
1173
+ window.setTaskMode = function(mode) {
1174
+ postSettings('/api/settings/task-mode', { value: String(mode || 'auto') }, 'Task mode updated.');
1175
+ };
1176
+
1177
+ window.setProactivity = function(level) {
1178
+ postSettings('/api/settings/proactivity', { value: String(level || 'high') }, 'Proactivity updated.');
1179
+ };
1180
+
1181
+ window.setMcpEnabled = function(enabled) {
1182
+ postSettings('/api/settings/mcp-enabled', { enabled: String(!!enabled) }, 'MCP setting updated.');
1183
+ };
1184
+
1185
+ window.toggleIntegrationTool = function(tool) {
1186
+ postSettings('/api/hook-toggle', { tool: String(tool || '') }, 'Integration updated.');
1187
+ };
1188
+ })();`;
1189
+ }
1190
+ export function renderSearchScript(authToken) {
1191
+ return `(function() {
1192
+ var _searchAuthToken = ${JSON.stringify(authToken)};
1193
+ var _searchProjectsLoaded = false;
1194
+
1195
+ function searchAuthUrl(path) {
1196
+ return _searchAuthToken ? path + (path.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(_searchAuthToken) : path;
1197
+ }
1198
+
1199
+ function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
1200
+
1201
+ function doSearch() {
1202
+ var q = document.getElementById('search-query').value.trim();
1203
+ if (!q) return;
1204
+ var project = document.getElementById('search-project-filter').value;
1205
+ var type = document.getElementById('search-type-filter').value;
1206
+ var statusEl = document.getElementById('search-status');
1207
+ var resultsEl = document.getElementById('search-results');
1208
+ statusEl.textContent = 'Searching...';
1209
+ resultsEl.innerHTML = '';
1210
+
1211
+ var url = '/api/search?q=' + encodeURIComponent(q) + '&limit=20';
1212
+ if (project) url += '&project=' + encodeURIComponent(project);
1213
+ if (type) url += '&type=' + encodeURIComponent(type);
1214
+
1215
+ fetch(searchAuthUrl(url)).then(function(r) { return r.json(); }).then(function(data) {
1216
+ if (!data.ok) {
1217
+ statusEl.textContent = '';
1218
+ resultsEl.innerHTML = '<div style="padding:24px;color:var(--muted);text-align:center">' + esc(data.error || 'Search failed') + '</div>';
1219
+ return;
1220
+ }
1221
+ var lines = data.results || [];
1222
+ if (!lines.length) {
1223
+ statusEl.textContent = 'No results.';
1224
+ resultsEl.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">No results for \\u201c' + esc(q) + '\\u201d</div>';
1225
+ return;
1226
+ }
1227
+
1228
+ // Parse lines into result cards: lines starting with [ are headers, others are content
1229
+ var cards = [];
1230
+ var current = null;
1231
+ for (var i = 0; i < lines.length; i++) {
1232
+ var line = lines[i];
1233
+ if (!line.trim()) continue;
1234
+ if (line.startsWith('[') && line.indexOf(']') > 0) {
1235
+ if (current) cards.push(current);
1236
+ var bracket = line.indexOf(']');
1237
+ var source = line.slice(1, bracket);
1238
+ var meta = line.slice(bracket + 1).trim();
1239
+ current = { source: source, meta: meta, snippets: [] };
1240
+ } else if (line === '(keyword fallback)') {
1241
+ // skip
1242
+ } else if (current) {
1243
+ current.snippets.push(line);
1244
+ } else {
1245
+ // orphan line, show as standalone
1246
+ cards.push({ source: '', meta: '', snippets: [line] });
1247
+ }
1248
+ }
1249
+ if (current) cards.push(current);
1250
+
1251
+ statusEl.textContent = cards.length + ' result(s)';
1252
+ var html = '';
1253
+ for (var c = 0; c < cards.length; c++) {
1254
+ var card = cards[c];
1255
+ html += '<div class="card" style="margin-bottom:8px">';
1256
+ html += '<div class="card-header" style="padding:10px 14px">';
1257
+ if (card.source) {
1258
+ html += '<span style="font-weight:500;font-size:var(--text-sm)">' + esc(card.source) + '</span>';
1259
+ }
1260
+ if (card.meta) {
1261
+ html += '<span class="text-muted" style="font-size:var(--text-xs);margin-left:8px">' + esc(card.meta) + '</span>';
1262
+ }
1263
+ html += '</div>';
1264
+ if (card.snippets.length) {
1265
+ html += '<div class="card-body" style="padding:10px 14px;font-size:var(--text-sm);white-space:pre-wrap;color:var(--ink-secondary)">';
1266
+ html += esc(card.snippets.join('\\n'));
1267
+ html += '</div>';
1268
+ }
1269
+ html += '</div>';
1270
+ }
1271
+ resultsEl.innerHTML = html;
1272
+ }).catch(function(err) {
1273
+ statusEl.textContent = '';
1274
+ resultsEl.innerHTML = '<div style="padding:24px;color:var(--muted);text-align:center">Search error: ' + esc(String(err)) + '</div>';
1275
+ });
1276
+ }
1277
+
1278
+ // Populate project filter
1279
+ function loadSearchProjects() {
1280
+ if (_searchProjectsLoaded) return;
1281
+ _searchProjectsLoaded = true;
1282
+ fetch(searchAuthUrl('/api/projects')).then(function(r) { return r.json(); }).then(function(data) {
1283
+ if (!data.ok) return;
1284
+ var sel = document.getElementById('search-project-filter');
1285
+ (data.projects || []).forEach(function(p) {
1286
+ var opt = document.createElement('option');
1287
+ opt.value = p.name; opt.textContent = p.name;
1288
+ sel.appendChild(opt);
1289
+ });
1290
+ }).catch(function() {});
1291
+ }
1292
+
1293
+ // Wire up events
1294
+ var searchBtn = document.getElementById('search-btn');
1295
+ var searchInput = document.getElementById('search-query');
1296
+ if (searchBtn) searchBtn.addEventListener('click', doSearch);
1297
+ if (searchInput) searchInput.addEventListener('keydown', function(e) {
1298
+ if (e.key === 'Enter') doSearch();
1299
+ });
1300
+
1301
+ // Hook into switchTab to lazy-load project filter
1302
+ var _searchOrigSwitchTab = window.switchTab;
1303
+ window.switchTab = function(tab) {
1304
+ if (typeof _searchOrigSwitchTab === 'function') _searchOrigSwitchTab(tab);
1305
+ if (tab === 'search') {
1306
+ loadSearchProjects();
1307
+ setTimeout(function() { var el = document.getElementById('search-query'); if (el) el.focus(); }, 50);
1308
+ }
1309
+ };
1310
+ })();`;
1311
+ }
1312
+ export function renderEventWiringScript() {
1313
+ return `(function() {
1314
+ // --- Navigation tabs ---
1315
+ document.querySelectorAll('.nav-item[data-tab]').forEach(function(btn) {
1316
+ btn.addEventListener('click', function() { switchTab(btn.getAttribute('data-tab')); });
1317
+ });
1318
+
1319
+ // --- Header buttons ---
1320
+ var themeBtn = document.getElementById('theme-toggle');
1321
+ if (themeBtn) themeBtn.addEventListener('click', function() { toggleTheme(); });
1322
+
1323
+ var cmdpalOpenBtn = document.getElementById('cmdpal-open-btn');
1324
+ if (cmdpalOpenBtn) {
1325
+ cmdpalOpenBtn.addEventListener('click', function() { openCmdPal(); });
1326
+ cmdpalOpenBtn.addEventListener('mouseover', function() { this.style.color='var(--ink)'; this.style.borderColor='var(--muted)'; });
1327
+ cmdpalOpenBtn.addEventListener('mouseout', function() { this.style.color='var(--muted)'; this.style.borderColor='var(--border)'; });
1328
+ }
1329
+
1330
+ // --- Projects search ---
1331
+ var projectsSearch = document.getElementById('projects-search');
1332
+ if (projectsSearch) projectsSearch.addEventListener('input', function() { filterProjects(this.value); });
1333
+
1334
+ // --- Review filters ---
1335
+ var reviewFilterProject = document.getElementById('review-filter-project');
1336
+ if (reviewFilterProject) reviewFilterProject.addEventListener('change', function() { filterReviewCards(); });
1337
+ var reviewFilterMachine = document.getElementById('review-filter-machine');
1338
+ if (reviewFilterMachine) reviewFilterMachine.addEventListener('change', function() { filterReviewCards(); });
1339
+ var reviewFilterModel = document.getElementById('review-filter-model');
1340
+ if (reviewFilterModel) reviewFilterModel.addEventListener('change', function() { filterReviewCards(); });
1341
+
1342
+ var highlightBtn = document.getElementById('highlight-only-btn');
1343
+ if (highlightBtn) highlightBtn.addEventListener('click', function() { toggleHighlightOnly(this); });
1344
+
1345
+ var selectAllCheckbox = document.getElementById('review-select-all-checkbox');
1346
+ if (selectAllCheckbox) selectAllCheckbox.addEventListener('change', function() { toggleSelectAll(this.checked); });
1347
+
1348
+ // --- Graph controls ---
1349
+ var graphZoomIn = document.getElementById('graph-zoom-in');
1350
+ if (graphZoomIn) graphZoomIn.addEventListener('click', function() { graphZoom(1.2); });
1351
+ var graphZoomOut = document.getElementById('graph-zoom-out');
1352
+ if (graphZoomOut) graphZoomOut.addEventListener('click', function() { graphZoom(0.8); });
1353
+ var graphResetBtn = document.getElementById('graph-reset');
1354
+ if (graphResetBtn) graphResetBtn.addEventListener('click', function() { graphReset(); });
1355
+
1356
+ // --- Tasks filters ---
1357
+ var tasksFilterProject = document.getElementById('tasks-filter-project');
1358
+ if (tasksFilterProject) tasksFilterProject.addEventListener('change', function() { filterTasks(); });
1359
+ var tasksFilterSection = document.getElementById('tasks-filter-section');
1360
+ if (tasksFilterSection) tasksFilterSection.addEventListener('change', function() { filterTasks(); });
1361
+
1362
+ // --- Sessions filter ---
1363
+ var sessionsFilterProject = document.getElementById('sessions-filter-project');
1364
+ if (sessionsFilterProject) sessionsFilterProject.addEventListener('change', function() { loadSessions(); });
1365
+
1366
+ // --- Batch bar ---
1367
+ var batchApproveBtn = document.getElementById('batch-approve-btn');
1368
+ if (batchApproveBtn) batchApproveBtn.addEventListener('click', function() { batchAction('approve'); });
1369
+ var batchRejectBtn = document.getElementById('batch-reject-btn');
1370
+ if (batchRejectBtn) batchRejectBtn.addEventListener('click', function() { batchAction('reject'); });
1371
+ var batchTagSelect = document.getElementById('batch-tag-select');
1372
+ if (batchTagSelect) batchTagSelect.addEventListener('change', function() { if(this.value){batchActionByTag(this.value,'approve');this.value='';} });
1373
+ var batchCancelBtn = document.getElementById('batch-cancel-btn');
1374
+ if (batchCancelBtn) batchCancelBtn.addEventListener('click', function() { clearBatchSelection(); });
1375
+
1376
+ // --- Command palette ---
1377
+ var cmdpal = document.getElementById('cmdpal');
1378
+ if (cmdpal) cmdpal.addEventListener('click', function(e) { closeCmdPal(e); });
1379
+ var cmdpalBox = document.getElementById('cmdpal-box');
1380
+ if (cmdpalBox) cmdpalBox.addEventListener('click', function(e) { e.stopPropagation(); });
1381
+ var cmdpalInput = document.getElementById('cmdpal-input');
1382
+ if (cmdpalInput) {
1383
+ cmdpalInput.addEventListener('input', function() { cmdpalSearch(this.value); });
1384
+ cmdpalInput.addEventListener('keydown', function(e) { cmdpalKey(e); });
1385
+ }
1386
+ })();`;
1387
+ }