@phren/cli 0.0.53 → 0.0.55

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.
@@ -443,6 +443,14 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
443
443
  .map((ref) => ref.scoreKey)
444
444
  .filter((key) => Boolean(key))
445
445
  .sort();
446
+ // Only include entities that link to at least one visible project
447
+ const linkedProjects = new Set();
448
+ for (const ref of refs) {
449
+ if (ref.project && projectSet.has(ref.project))
450
+ linkedProjects.add(ref.project);
451
+ }
452
+ if (linkedProjects.size === 0)
453
+ continue; // skip orphan entities from other profiles/stores
446
454
  const nodeId = `entity:${stableId("entity", type, name)}`;
447
455
  nodes.push({
448
456
  id: nodeId,
@@ -457,12 +465,6 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
457
465
  entityType: type,
458
466
  refDocs: refs,
459
467
  });
460
- // Link fragment to each project it appears in
461
- const linkedProjects = new Set();
462
- for (const ref of refs) {
463
- if (ref.project && projectSet.has(ref.project))
464
- linkedProjects.add(ref.project);
465
- }
466
468
  for (const proj of linkedProjects) {
467
469
  links.push({ source: nodeId, target: proj });
468
470
  }
@@ -1,7 +1,7 @@
1
1
  import { WEB_UI_STYLES, renderWebUiScript } from "./assets.js";
2
2
  import { renderGraphScript } from "./graph.js";
3
3
  import { PROJECT_REFERENCE_UI_STYLES, REVIEW_UI_STYLES, SETTINGS_TAB_UI_STYLES, TASK_UI_STYLES } from "./styles.js";
4
- import { renderSharedWebUiHelpers, renderSkillUiEnhancementScript, renderProjectReferenceEnhancementScript, renderTasksAndSettingsScript, renderSearchScript, renderEventWiringScript, renderGraphHostScript, } from "./scripts.js";
4
+ import { renderSharedWebUiHelpers, renderSkillUiEnhancementScript, renderProfileSwitcherScript, renderProjectReferenceEnhancementScript, renderTasksAndSettingsScript, renderReviewQueueKeyboardScript, renderSearchScript, renderEventWiringScript, renderGraphHostScript, } from "./scripts.js";
5
5
  function h(s) {
6
6
  return s
7
7
  .replace(/&/g, "&")
@@ -203,6 +203,7 @@ ${REVIEW_UI_STYLES}
203
203
  <button id="graph-zoom-in" title="Zoom in">+</button>
204
204
  <button id="graph-zoom-out" title="Zoom out">-</button>
205
205
  <button id="graph-reset" title="Reset view">R</button>
206
+ <button id="graph-reset-layout" title="Re-run layout">L</button>
206
207
  </div>
207
208
  <div class="graph-filters">
208
209
  <div class="graph-filter" id="graph-filter"></div>
@@ -276,6 +277,18 @@ ${REVIEW_UI_STYLES}
276
277
  <div id="settings-scope-note" style="font-size:var(--text-sm);color:var(--muted)">Showing global settings. Select a project to view and edit per-project overrides.</div>
277
278
  </div>
278
279
  </section>
280
+ <section class="settings-section" style="border-top:3px solid color-mix(in srgb, var(--blue) 45%, var(--border))">
281
+ <div class="settings-section-header">Profile</div>
282
+ <div class="settings-section-body">
283
+ <div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:12px">
284
+ <label for="profile-select" style="display:block;margin-bottom:6px;color:var(--ink)">Active Profile:</label>
285
+ <select id="profile-select" style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 10px;background:var(--surface);color:var(--ink);font-size:var(--text-sm);font-family:var(--font);width:100%;max-width:200px">
286
+ <option>Loading profiles...</option>
287
+ </select>
288
+ </div>
289
+ <div id="profile-status" style="font-size:var(--text-sm);color:var(--muted)"></div>
290
+ </div>
291
+ </section>
279
292
  <section id="settings-project-info-section" class="settings-section" style="display:none;border-top:3px solid color-mix(in srgb, var(--accent) 45%, var(--border))">
280
293
  <div class="settings-section-header">Project Info</div>
281
294
  <div class="settings-section-body">
@@ -312,6 +325,12 @@ ${REVIEW_UI_STYLES}
312
325
  <div id="settings-integrations" style="color:var(--muted)">Loading...</div>
313
326
  </div>
314
327
  </section>
328
+ <section class="settings-section settings-section-stores">
329
+ <div class="settings-section-header">Stores</div>
330
+ <div class="settings-section-body">
331
+ <div id="settings-stores" style="color:var(--muted)">Loading...</div>
332
+ </div>
333
+ </section>
315
334
  </div>
316
335
  </div>
317
336
  </div>
@@ -341,12 +360,18 @@ ${renderSharedWebUiHelpers(authToken || "")}
341
360
  ${renderSkillUiEnhancementScript(h(authToken || ""))}
342
361
  </script>
343
362
  <script${nonceAttr}>
363
+ ${renderProfileSwitcherScript(h(authToken || ""))}
364
+ </script>
365
+ <script${nonceAttr}>
344
366
  ${renderProjectReferenceEnhancementScript(h(authToken || ""))}
345
367
  </script>
346
368
  <script${nonceAttr}>
347
369
  ${renderTasksAndSettingsScript(authToken || "")}
348
370
  </script>
349
371
  <script${nonceAttr}>
372
+ ${renderReviewQueueKeyboardScript(authToken || "")}
373
+ </script>
374
+ <script${nonceAttr}>
350
375
  ${renderSearchScript(authToken || "")}
351
376
  </script>
352
377
  <script${nonceAttr}>
@@ -28,6 +28,62 @@ export function renderSharedWebUiHelpers(authToken) {
28
28
  };
29
29
  })();`;
30
30
  }
31
+ export function renderProfileSwitcherScript(_authToken) {
32
+ return `(function() {
33
+ var esc = window._phrenEsc;
34
+ var authUrl = window._phrenAuthUrl;
35
+
36
+ function loadProfiles() {
37
+ fetch(authUrl('/api/profiles')).then(function(r) { return r.json(); }).then(function(data) {
38
+ var select = document.getElementById('profile-select');
39
+ if (!select) return;
40
+ if (!data.ok || !data.profiles) {
41
+ select.innerHTML = '<option>Error loading profiles</option>';
42
+ return;
43
+ }
44
+ var html = '';
45
+ data.profiles.forEach(function(p) {
46
+ var selected = p.name === data.activeProfile ? ' selected' : '';
47
+ html += '<option value="' + esc(p.name) + '"' + selected + '>' + esc(p.name) + '</option>';
48
+ });
49
+ select.innerHTML = html;
50
+ select.onchange = function() { switchProfile(this.value); };
51
+ }).catch(function(err) {
52
+ var select = document.getElementById('profile-select');
53
+ if (select) select.innerHTML = '<option>Error loading</option>';
54
+ });
55
+ }
56
+
57
+ function switchProfile(profileName) {
58
+ if (!profileName) return;
59
+ var status = document.getElementById('profile-status');
60
+ if (status) status.textContent = 'Switching...';
61
+ fetch(authUrl('/api/profile'), {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
64
+ body: window._phrenAuthBody('profile=' + encodeURIComponent(profileName))
65
+ }).then(function(r) { return r.json(); }).then(function(data) {
66
+ if (data.ok) {
67
+ if (status) status.textContent = 'Reloading...';
68
+ setTimeout(function() { location.reload(); }, 500);
69
+ } else {
70
+ if (status) status.textContent = 'Error: ' + (data.error || 'Unknown');
71
+ }
72
+ }).catch(function(err) {
73
+ if (status) status.textContent = 'Error loading';
74
+ });
75
+ }
76
+
77
+ window.phrenLoadProfiles = loadProfiles;
78
+ window.phrenSwitchProfile = switchProfile;
79
+
80
+ if (document.readyState === 'loading') {
81
+ document.addEventListener('DOMContentLoaded', loadProfiles);
82
+ } else {
83
+ loadProfiles();
84
+ }
85
+ })();`;
86
+ }
31
87
  export function renderSkillUiEnhancementScript(_authToken) {
32
88
  return `(function() {
33
89
  var _skillCurrent = null;
@@ -879,6 +935,48 @@ export function renderTasksAndSettingsScript(authToken) {
879
935
  });
880
936
  }
881
937
 
938
+ function postGlobalRetention(field, value, clearField) {
939
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
940
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
941
+ var payload = { field: field, value: value || '', clear: clearField ? 'true' : 'false', globalUpdate: 'true' };
942
+ var body = new URLSearchParams(payload);
943
+ if (csrfData.token) body.set('_csrf', csrfData.token);
944
+ var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
945
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
946
+ }).then(function(r) { return r.json(); }).then(function(data) {
947
+ if (!data.ok) {
948
+ setSettingsStatus(data.error || 'Failed to update retention policy', 'err');
949
+ return;
950
+ }
951
+ _settingsLoaded = false;
952
+ loadSettings();
953
+ setSettingsStatus('Retention policy updated', 'ok');
954
+ }).catch(function(err) {
955
+ setSettingsStatus('Failed: ' + String(err), 'err');
956
+ });
957
+ }
958
+
959
+ function postGlobalWorkflow(field, value, clearField) {
960
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
961
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
962
+ var payload = { field: field, value: value || '', clear: clearField ? 'true' : 'false', globalUpdate: 'true' };
963
+ var body = new URLSearchParams(payload);
964
+ if (csrfData.token) body.set('_csrf', csrfData.token);
965
+ var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
966
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
967
+ }).then(function(r) { return r.json(); }).then(function(data) {
968
+ if (!data.ok) {
969
+ setSettingsStatus(data.error || 'Failed to update workflow policy', 'err');
970
+ return;
971
+ }
972
+ _settingsLoaded = false;
973
+ loadSettings();
974
+ setSettingsStatus('Workflow policy updated', 'ok');
975
+ }).catch(function(err) {
976
+ setSettingsStatus('Failed: ' + String(err), 'err');
977
+ });
978
+ }
979
+
882
980
  function loadSettings() {
883
981
  var selectedProject = getSettingsProject();
884
982
  var baseUrl = '/api/settings';
@@ -1058,12 +1156,11 @@ export function renderTasksAndSettingsScript(authToken) {
1058
1156
  }
1059
1157
  retHtml += '</div>';
1060
1158
  if (note) retHtml += '<div class="settings-control-note">' + esc(note) + '</div>';
1061
- if (isProject) {
1062
- retHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1063
- '<input type="number" id="ret-input-' + esc(field) + '" value="' + esc(String(value != null ? value : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1064
- '<button data-ts-action="setProjectRetention" data-field="' + esc(field) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
1065
- '</div>';
1066
- }
1159
+ // Show editable inputs for both global and per-project
1160
+ retHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1161
+ '<input type="number" id="ret-input-' + esc(field) + '" value="' + esc(String(value != null ? value : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1162
+ '<button data-ts-action="' + (isProject ? 'setProjectRetention' : 'setGlobalRetention') + '" data-field="' + esc(field) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
1163
+ '</div>';
1067
1164
  retHtml += '</div>';
1068
1165
  }
1069
1166
  retRow('TTL days', 'ttlDays', ret.ttlDays, 'Memories older than this are eligible for pruning.');
@@ -1085,14 +1182,11 @@ export function renderTasksAndSettingsScript(authToken) {
1085
1182
  if (isProject && lctOverride) {
1086
1183
  wfHtml += '<button data-ts-action="clearProjectOverride" data-field="lowConfidenceThreshold" class="settings-chip" style="font-size:11px">Clear</button>';
1087
1184
  }
1088
- if (isProject) {
1089
- wfHtml += '</div><div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1090
- '<input type="number" id="wf-input-lowConfidenceThreshold" min="0" max="1" step="0.05" value="' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1091
- '<button data-ts-action="setProjectWorkflow" data-field="lowConfidenceThreshold" class="settings-chip active" style="font-size:11px">Set</button>' +
1092
- '</div>';
1093
- } else {
1094
- wfHtml += '</div>';
1095
- }
1185
+ // Show editable inputs for both global and per-project
1186
+ wfHtml += '</div><div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1187
+ '<input type="number" id="wf-input-lowConfidenceThreshold" min="0" max="1" step="0.05" value="' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1188
+ '<button data-ts-action="' + (isProject ? 'setProjectWorkflow' : 'setGlobalWorkflow') + '" data-field="lowConfidenceThreshold" class="settings-chip active" style="font-size:11px">Set</button>' +
1189
+ '</div>';
1096
1190
  wfHtml += '<div class="settings-control-note">Memories below this confidence score are flagged for review.</div></div>';
1097
1191
  wfHtml += '<div class="settings-control">';
1098
1192
  wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Risky sections</span>' + sourceBadge(riskySectionsOverride);
@@ -1123,11 +1217,62 @@ export function renderTasksAndSettingsScript(authToken) {
1123
1217
  } else if (integrationsEl && isProject) {
1124
1218
  integrationsEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">Integration settings are global — switch to Global scope to edit them.</div>';
1125
1219
  }
1220
+
1221
+ // Load stores if on global scope
1222
+ if (!isProject) {
1223
+ loadStores();
1224
+ }
1126
1225
  }).catch(function(err) {
1127
1226
  setSettingsStatus('Failed to load settings: ' + String(err), 'err');
1128
1227
  });
1129
1228
  }
1130
1229
 
1230
+ function loadStores() {
1231
+ var storesEl = document.getElementById('settings-stores');
1232
+ if (!storesEl) return;
1233
+ var url = _tsAuthToken ? tsAuthUrl('/api/stores') : '/api/stores';
1234
+ fetch(url).then(function(r) { return r.json(); }).then(function(data) {
1235
+ if (!data.ok || !Array.isArray(data.stores)) {
1236
+ storesEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">No team stores configured.</div>';
1237
+ return;
1238
+ }
1239
+ if (data.stores.length === 0) {
1240
+ storesEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">No team stores configured.</div>';
1241
+ return;
1242
+ }
1243
+ var html = '';
1244
+ data.stores.forEach(function(store) {
1245
+ html += '<div class="settings-control" style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;margin-bottom:12px">';
1246
+ html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">';
1247
+ html += '<div>';
1248
+ html += '<div style="font-weight:600;color:var(--ink)">' + esc(store.name) + '</div>';
1249
+ html += '<div style="font-size:var(--text-sm);color:var(--muted)">Role: ' + esc(store.role) + '</div>';
1250
+ html += '</div>';
1251
+ html += '</div>';
1252
+ html += '<div style="font-size:var(--text-xs);color:var(--muted);margin-bottom:10px;font-family:var(--mono);word-break:break-all">' + esc(store.path) + '</div>';
1253
+ html += '<div style="font-size:var(--text-sm);color:var(--ink);margin-bottom:6px;font-weight:500">Projects</div>';
1254
+ if (Array.isArray(store.availableProjects) && store.availableProjects.length > 0) {
1255
+ var subscribed = Array.isArray(store.subscribedProjects) ? store.subscribedProjects : [];
1256
+ html += '<div style="display:flex;flex-direction:column;gap:6px">';
1257
+ store.availableProjects.forEach(function(proj) {
1258
+ var isSubscribed = subscribed.indexOf(proj) !== -1;
1259
+ html += '<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--text-sm)">';
1260
+ html += '<input type="checkbox" class="store-project-checkbox" data-store="' + esc(store.name) + '" data-project="' + esc(proj) + '" ' + (isSubscribed ? 'checked' : '') + ' style="cursor:pointer;width:16px;height:16px">';
1261
+ html += '<span>' + esc(proj) + '</span>';
1262
+ html += '</label>';
1263
+ });
1264
+ html += '</div>';
1265
+ } else {
1266
+ html += '<div style="color:var(--muted);font-size:var(--text-sm)">No projects available</div>';
1267
+ }
1268
+ html += '</div>';
1269
+ });
1270
+ storesEl.innerHTML = html;
1271
+ }).catch(function(err) {
1272
+ storesEl.innerHTML = '<div style="color:var(--error);font-size:var(--text-sm)">Failed to load stores: ' + esc(String(err)) + '</div>';
1273
+ });
1274
+ }
1275
+
1131
1276
  // Hook into switchTab to lazy-load
1132
1277
  var _origSwitchTab = window.switchTab;
1133
1278
  var _tasksLoaded = false;
@@ -1138,6 +1283,43 @@ export function renderTasksAndSettingsScript(authToken) {
1138
1283
  if (tab === 'settings' && !_settingsLoaded) { _settingsLoaded = true; loadSettings(); }
1139
1284
  };
1140
1285
 
1286
+ // Handle store project checkbox changes
1287
+ document.addEventListener('change', function(e) {
1288
+ var target = e.target;
1289
+ if (target && target.classList && target.classList.contains('store-project-checkbox')) {
1290
+ var storeName = target.getAttribute('data-store');
1291
+ var projectName = target.getAttribute('data-project');
1292
+ var isChecked = target.checked;
1293
+ if (storeName && projectName) {
1294
+ handleStoreProjectToggle(storeName, projectName, isChecked);
1295
+ }
1296
+ }
1297
+ });
1298
+
1299
+ function handleStoreProjectToggle(storeName, projectName, isSubscribing) {
1300
+ var endpoint = isSubscribing ? '/api/stores/subscribe' : '/api/stores/unsubscribe';
1301
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
1302
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
1303
+ var payload = { store: storeName, projects: [projectName] };
1304
+ var body = new URLSearchParams();
1305
+ body.set('store', storeName);
1306
+ body.set('projects', JSON.stringify([projectName]));
1307
+ if (csrfData.token) body.set('_csrf', csrfData.token);
1308
+ var url = _tsAuthToken ? tsAuthUrl(endpoint) : endpoint;
1309
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
1310
+ }).then(function(r) { return r.json(); }).then(function(data) {
1311
+ if (!data.ok) {
1312
+ setSettingsStatus((isSubscribing ? 'Subscribe' : 'Unsubscribe') + ' failed: ' + (data.error || ''), 'err');
1313
+ loadStores();
1314
+ return;
1315
+ }
1316
+ setSettingsStatus((isSubscribing ? 'Subscribed to' : 'Unsubscribed from') + ' ' + projectName, 'ok');
1317
+ loadStores();
1318
+ }).catch(function(err) {
1319
+ setSettingsStatus('Request failed: ' + String(err), 'err');
1320
+ });
1321
+ }
1322
+
1141
1323
  // Event delegation for dynamically generated tasks/settings UI
1142
1324
  document.addEventListener('click', function(e) {
1143
1325
  var target = e.target;
@@ -1175,6 +1357,25 @@ export function renderTasksAndSettingsScript(authToken) {
1175
1357
  var val = inputEl ? inputEl.value : '';
1176
1358
  postProjectOverride(proj, field, val, false);
1177
1359
  }
1360
+ else if (action === 'setGlobalRetention') {
1361
+ var field = actionEl.getAttribute('data-field') || '';
1362
+ var inputEl = document.getElementById('ret-input-' + field);
1363
+ var val = inputEl ? inputEl.value : '';
1364
+ postGlobalRetention(field, val);
1365
+ }
1366
+ else if (action === 'setProjectRetention') {
1367
+ var proj = getSettingsProject();
1368
+ var field = actionEl.getAttribute('data-field') || '';
1369
+ var inputEl = document.getElementById('ret-input-' + field);
1370
+ var val = inputEl ? inputEl.value : '';
1371
+ postProjectOverride(proj, field, val, false);
1372
+ }
1373
+ else if (action === 'setGlobalWorkflow') {
1374
+ var field = actionEl.getAttribute('data-field') || '';
1375
+ var inputEl = document.getElementById('wf-input-' + field);
1376
+ var val = inputEl ? inputEl.value : '';
1377
+ postGlobalWorkflow(field, val);
1378
+ }
1178
1379
  else if (action === 'setProjectWorkflow') {
1179
1380
  var proj = getSettingsProject();
1180
1381
  var field = actionEl.getAttribute('data-field') || '';
@@ -1466,6 +1667,8 @@ export function renderEventWiringScript() {
1466
1667
  if (graphZoomOut) graphZoomOut.addEventListener('click', function() { graphZoom(0.8); });
1467
1668
  var graphResetBtn = document.getElementById('graph-reset');
1468
1669
  if (graphResetBtn) graphResetBtn.addEventListener('click', function() { graphReset(); });
1670
+ var graphResetLayoutBtn = document.getElementById('graph-reset-layout');
1671
+ if (graphResetLayoutBtn) graphResetLayoutBtn.addEventListener('click', function() { if (typeof graphResetLayout === 'function') graphResetLayout(); });
1469
1672
 
1470
1673
  // --- Tasks filters ---
1471
1674
  var tasksFilterProject = document.getElementById('tasks-filter-project');
@@ -1830,7 +2033,7 @@ export function renderGraphHostScript() {
1830
2033
  if (priorityEl) updates.priority = priorityEl.value;
1831
2034
  graphRequest('/api/tasks/update', 'POST', {
1832
2035
  project: currentNode.projectName,
1833
- item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
2036
+ item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
1834
2037
  text: updates.text,
1835
2038
  section: updates.section || '',
1836
2039
  priority: updates.priority || ''
@@ -1867,7 +2070,7 @@ export function renderGraphHostScript() {
1867
2070
  if (currentNode.kind === 'task') {
1868
2071
  graphRequest('/api/tasks/remove', 'POST', {
1869
2072
  project: currentNode.projectName,
1870
- item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
2073
+ item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
1871
2074
  }).then(function(result) {
1872
2075
  if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Delete failed');
1873
2076
  graphToast('Task removed', 'ok');
@@ -1882,7 +2085,7 @@ export function renderGraphHostScript() {
1882
2085
  if (!currentNode) return;
1883
2086
  graphRequest('/api/tasks/complete', 'POST', {
1884
2087
  project: currentNode.projectName,
1885
- item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
2088
+ item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
1886
2089
  }).then(function(result) {
1887
2090
  if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
1888
2091
  graphToast('Task completed', 'ok');
@@ -1896,7 +2099,7 @@ export function renderGraphHostScript() {
1896
2099
  if (!currentNode) return;
1897
2100
  graphRequest('/api/tasks/update', 'POST', {
1898
2101
  project: currentNode.projectName,
1899
- item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
2102
+ item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
1900
2103
  section: section
1901
2104
  }).then(function(result) {
1902
2105
  if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
@@ -2007,3 +2210,87 @@ export function renderGraphHostScript() {
2007
2210
  }
2008
2211
  })();`;
2009
2212
  }
2213
+ export function renderReviewQueueKeyboardScript(_authToken) {
2214
+ return `(function() {
2215
+ var _reviewHighlightIndex = -1;
2216
+ var _currentTab = '';
2217
+ var esc = window._phrenEsc;
2218
+ var authUrl = window._phrenAuthUrl;
2219
+ var authBody = window._phrenAuthBody;
2220
+ var fetchCsrfToken = window._phrenFetchCsrfToken;
2221
+
2222
+ function updateReviewHighlight() {
2223
+ var cards = document.querySelectorAll('#review-cards-list [data-review-card]');
2224
+ cards.forEach(function(card, i) {
2225
+ if (i === _reviewHighlightIndex) {
2226
+ card.classList.add('review-card-highlight');
2227
+ card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
2228
+ } else {
2229
+ card.classList.remove('review-card-highlight');
2230
+ }
2231
+ });
2232
+ }
2233
+
2234
+ function getApproveButton(card) {
2235
+ return card ? card.querySelector('[data-review-type="approve"]') : null;
2236
+ }
2237
+
2238
+ function getRejectButton(card) {
2239
+ return card ? card.querySelector('[data-review-type="reject"]') : null;
2240
+ }
2241
+
2242
+ function triggerCardAction(card, action) {
2243
+ if (!card) return;
2244
+ if (action === 'approve') {
2245
+ var approveBtn = getApproveButton(card);
2246
+ if (approveBtn) approveBtn.click();
2247
+ } else if (action === 'reject') {
2248
+ var rejectBtn = getRejectButton(card);
2249
+ if (rejectBtn) rejectBtn.click();
2250
+ }
2251
+ }
2252
+
2253
+ var baseWindowSwitchTab = window.switchTab;
2254
+ if (typeof baseWindowSwitchTab === 'function') {
2255
+ window.switchTab = function(tab) {
2256
+ baseWindowSwitchTab(tab);
2257
+ _currentTab = tab;
2258
+ if (tab === 'review') {
2259
+ _reviewHighlightIndex = -1;
2260
+ setTimeout(function() { updateReviewHighlight(); }, 100);
2261
+ }
2262
+ };
2263
+ }
2264
+
2265
+ document.addEventListener('keydown', function(e) {
2266
+ if (_currentTab !== 'review') return;
2267
+ var cards = document.querySelectorAll('#review-cards-list [data-review-card]');
2268
+ if (cards.length === 0) return;
2269
+
2270
+ if (e.key === 'j' || e.key === 'ArrowDown') {
2271
+ e.preventDefault();
2272
+ _reviewHighlightIndex = Math.min(_reviewHighlightIndex + 1, cards.length - 1);
2273
+ updateReviewHighlight();
2274
+ } else if (e.key === 'k' || e.key === 'ArrowUp') {
2275
+ e.preventDefault();
2276
+ _reviewHighlightIndex = Math.max(_reviewHighlightIndex - 1, -1);
2277
+ updateReviewHighlight();
2278
+ } else if (e.key === 'a') {
2279
+ e.preventDefault();
2280
+ if (_reviewHighlightIndex >= 0) {
2281
+ triggerCardAction(cards[_reviewHighlightIndex], 'approve');
2282
+ }
2283
+ } else if (e.key === 'd') {
2284
+ e.preventDefault();
2285
+ if (_reviewHighlightIndex >= 0) {
2286
+ triggerCardAction(cards[_reviewHighlightIndex], 'reject');
2287
+ }
2288
+ } else if (e.key === 'Enter') {
2289
+ e.preventDefault();
2290
+ if (_reviewHighlightIndex >= 0) {
2291
+ cards[_reviewHighlightIndex].click();
2292
+ }
2293
+ }
2294
+ });
2295
+ })();`;
2296
+ }