@phren/cli 0.0.16 → 0.0.18
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.
- package/mcp/dist/capabilities/cli.js +1 -1
- package/mcp/dist/capabilities/mcp.js +1 -1
- package/mcp/dist/capabilities/vscode.js +1 -1
- package/mcp/dist/capabilities/web-ui.js +1 -1
- package/mcp/dist/cli-hooks-session.js +3 -3
- package/mcp/dist/cli-namespaces.js +1 -1
- package/mcp/dist/data-tasks.js +1 -1
- package/mcp/dist/entrypoint.js +1 -1
- package/mcp/dist/governance-policy.js +76 -32
- package/mcp/dist/governance-rbac.js +152 -0
- package/mcp/dist/init-preferences.js +1 -1
- package/mcp/dist/init-setup.js +7 -7
- package/mcp/dist/init.js +2 -2
- package/mcp/dist/link-checksums.js +1 -1
- package/mcp/dist/link-doctor.js +1 -1
- package/mcp/dist/mcp-config.js +102 -2
- package/mcp/dist/mcp-finding.js +16 -0
- package/mcp/dist/mcp-search.js +44 -5
- package/mcp/dist/mcp-session.js +4 -0
- package/mcp/dist/mcp-tasks.js +22 -0
- package/mcp/dist/memory-ui-assets.js +2 -2
- package/mcp/dist/memory-ui-page.js +29 -48
- package/mcp/dist/memory-ui-scripts.js +225 -78
- package/mcp/dist/memory-ui-server.js +58 -2
- package/mcp/dist/memory-ui-styles.js +102 -0
- package/mcp/dist/phren-core.js +1 -1
- package/mcp/dist/phren-paths.js +2 -2
- package/mcp/dist/proactivity.js +47 -0
- package/mcp/dist/profile-store.js +100 -1
- package/mcp/dist/shared-index.js +2 -2
- package/mcp/dist/shared-retrieval.js +42 -1
- package/mcp/dist/shell-input.js +1 -1
- package/package.json +1 -1
|
@@ -86,55 +86,29 @@ ${TASK_UI_STYLES}
|
|
|
86
86
|
|
|
87
87
|
<!-- ── Review Tab ────────────────────────────────────────── -->
|
|
88
88
|
<div id="tab-review" class="tab-content">
|
|
89
|
-
<div class="
|
|
90
|
-
<div class="card-header"><h2>Sync State</h2></div>
|
|
91
|
-
<div class="card-body">
|
|
92
|
-
<div id="sync-state-summary" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;font-size:var(--text-base)">
|
|
93
|
-
<div><strong>Auto-save</strong><div class="text-muted">${h(sync.autoSaveStatus || "n/a")}</div></div>
|
|
94
|
-
<div><strong>Last pull</strong><div class="text-muted">${h(sync.lastPullStatus || "n/a")} ${h(sync.lastPullAt || "")}</div></div>
|
|
95
|
-
<div><strong>Last push</strong><div class="text-muted">${h(sync.lastPushStatus || "n/a")} ${h(sync.lastPushAt || "")}</div></div>
|
|
96
|
-
<div><strong>Unsynced commits</strong><div class="text-muted">${h(String(sync.unsyncedCommits || 0))}</div></div>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
<details class="review-help" style="margin-bottom:16px">
|
|
101
|
-
<summary>Help: How the Review Queue works</summary>
|
|
102
|
-
<dl>
|
|
103
|
-
<dt>What is the Review Queue?</dt>
|
|
104
|
-
<dd>Fragments flagged by governance for human review. Items accumulate here when <code>phren maintain govern</code> is run.</dd>
|
|
105
|
-
<dt>Can I approve, reject, or edit items here?</dt>
|
|
106
|
-
<dd>Yes. Each review card has <strong>Approve</strong>, <strong>Reject</strong>, and <strong>Edit</strong> buttons. Approve accepts the fragment, Reject removes it, and Edit lets you revise the text before accepting. You can also use batch actions to approve or reject multiple items at once.</dd>
|
|
107
|
-
<dt>How do I clear items?</dt>
|
|
108
|
-
<dd>Approve or reject items directly in the UI, or use maintenance flows such as <code>phren maintain prune</code>.</dd>
|
|
109
|
-
<dt>Is this automatic?</dt>
|
|
110
|
-
<dd>No. Agents do not auto-accept review-queue items.</dd>
|
|
111
|
-
<dt>How do items get here?</dt>
|
|
112
|
-
<dd><code>phren maintain govern</code> flags stale or low-confidence fragments for review.</dd>
|
|
113
|
-
<dt>How to reduce noise?</dt>
|
|
114
|
-
<dd>Run <code>phren maintain prune</code> to auto-remove expired items without manual review.</dd>
|
|
115
|
-
</dl>
|
|
116
|
-
</details>
|
|
117
|
-
|
|
118
|
-
<p style="font-size:var(--text-sm);color:var(--muted);margin-bottom:12px;letter-spacing:-0.01em">Fragments flagged for review. Approve, reject, or edit items directly from this tab.</p>
|
|
119
|
-
|
|
120
|
-
<div id="review-summary-banner" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center"></div>
|
|
121
|
-
|
|
122
|
-
<div class="review-filters" id="review-filters" style="display:none">
|
|
89
|
+
<div class="review-toolbar" id="review-filters" style="display:none">
|
|
123
90
|
<select id="review-filter-project">
|
|
124
91
|
<option value="">All projects</option>
|
|
125
92
|
</select>
|
|
126
|
-
<
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
93
|
+
<label class="review-flagged-toggle" id="review-flagged-toggle">
|
|
94
|
+
<input type="checkbox" id="highlight-only-btn" />
|
|
95
|
+
<span>Flagged only</span>
|
|
96
|
+
</label>
|
|
97
|
+
<span id="review-filter-count" class="text-muted" style="font-size:var(--text-sm);margin-left:auto"></span>
|
|
98
|
+
<label id="review-select-all" style="display:none;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--muted);cursor:pointer;user-select:none">
|
|
99
|
+
<input type="checkbox" onchange="toggleSelectAll(this.checked)" style="width:14px;height:14px;cursor:pointer;accent-color:var(--accent)" />
|
|
100
|
+
Select all
|
|
101
|
+
</label>
|
|
102
|
+
<span id="review-sync-status" class="review-sync-dot" title="Sync status">
|
|
103
|
+
<span class="review-sync-indicator" id="review-sync-indicator"></span>
|
|
104
|
+
</span>
|
|
134
105
|
</div>
|
|
135
106
|
|
|
136
|
-
<div id="
|
|
137
|
-
<span
|
|
107
|
+
<div id="batch-bar" class="batch-bar">
|
|
108
|
+
<span id="batch-count" class="batch-bar-count"></span>
|
|
109
|
+
<button class="btn btn-sm btn-approve" onclick="batchAction('approve')">Approve selected</button>
|
|
110
|
+
<button class="btn btn-sm btn-reject" onclick="batchAction('reject')">Reject selected</button>
|
|
111
|
+
<button class="btn btn-sm" onclick="clearBatchSelection()">Clear</button>
|
|
138
112
|
</div>
|
|
139
113
|
|
|
140
114
|
<div class="review-cards" id="review-cards-list">
|
|
@@ -158,9 +132,10 @@ ${TASK_UI_STYLES}
|
|
|
158
132
|
<div style="max-width:720px;margin:0 auto">
|
|
159
133
|
<div style="display:flex;gap:8px;margin-bottom:16px">
|
|
160
134
|
<input type="text" id="search-query" placeholder="Search fragments, findings, tasks..." style="flex:1;border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 12px;background:var(--surface);color:var(--ink);font-size:var(--text-base);font-family:var(--font);outline:none" />
|
|
161
|
-
<
|
|
162
|
-
<
|
|
163
|
-
|
|
135
|
+
<div id="search-project-wrap" style="position:relative">
|
|
136
|
+
<button id="search-project-btn" type="button" onclick="window._phrenToggleProjectDropdown()" 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);cursor:pointer;font-family:var(--font);min-width:120px;text-align:left;white-space:nowrap">All projects</button>
|
|
137
|
+
<div id="search-project-dropdown" style="display:none;position:absolute;top:100%;left:0;z-index:50;margin-top:4px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:0 4px 12px rgba(0,0,0,.15);max-height:240px;overflow-y:auto;min-width:160px"></div>
|
|
138
|
+
</div>
|
|
164
139
|
<select id="search-type-filter" 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)">
|
|
165
140
|
<option value="">All types</option>
|
|
166
141
|
<option value="finding">Findings</option>
|
|
@@ -224,7 +199,7 @@ ${TASK_UI_STYLES}
|
|
|
224
199
|
<div style="padding:20px;color:var(--muted)">Loading...</div>
|
|
225
200
|
</div>
|
|
226
201
|
<div class="split-reader" id="hooks-reader">
|
|
227
|
-
<div class="reader-empty">Select a hook config to view its contents.</div>
|
|
202
|
+
<div class="reader-empty">Select a hook config to view its contents.<br/><span style="font-size:var(--text-sm);color:var(--muted);margin-top:8px;display:inline-block">Per-project hooks can also be configured in Settings > [project name].</span></div>
|
|
228
203
|
</div>
|
|
229
204
|
</div>
|
|
230
205
|
</div>
|
|
@@ -277,6 +252,12 @@ ${TASK_UI_STYLES}
|
|
|
277
252
|
<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>
|
|
278
253
|
</div>
|
|
279
254
|
</section>
|
|
255
|
+
<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))">
|
|
256
|
+
<div class="settings-section-header">Project Info</div>
|
|
257
|
+
<div class="settings-section-body">
|
|
258
|
+
<div id="settings-project-info" style="color:var(--muted)"></div>
|
|
259
|
+
</div>
|
|
260
|
+
</section>
|
|
280
261
|
<section class="settings-section settings-section-findings">
|
|
281
262
|
<div class="settings-section-header">Findings</div>
|
|
282
263
|
<div class="settings-section-body">
|
|
@@ -605,6 +605,8 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
605
605
|
return fetch(url).then(function(r) { return r.json(); });
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
+
var _taskViewMode = 'list';
|
|
609
|
+
|
|
608
610
|
function priorityBadge(p) {
|
|
609
611
|
if (!p) return '';
|
|
610
612
|
var colors = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280' };
|
|
@@ -612,6 +614,12 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
612
614
|
return '<span class="task-priority-badge task-priority-' + esc(p) + '">' + esc(p) + '</span>';
|
|
613
615
|
}
|
|
614
616
|
|
|
617
|
+
function sessionBadge(sessionId) {
|
|
618
|
+
if (!sessionId) return '';
|
|
619
|
+
var short = sessionId.length > 8 ? sessionId.slice(0, 8) : sessionId;
|
|
620
|
+
return '<span class="task-session-badge" title="Session ' + esc(sessionId) + '">' + esc(short) + '</span>';
|
|
621
|
+
}
|
|
622
|
+
|
|
615
623
|
function statusChip(section, checked) {
|
|
616
624
|
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>';
|
|
617
625
|
if (section === 'Active') return '<span class="task-status-chip task-status-active" title="In Progress">In Progress</span>';
|
|
@@ -731,10 +739,16 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
731
739
|
// Group by priority within sections
|
|
732
740
|
var priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
733
741
|
function sortByPriority(a, b) {
|
|
734
|
-
|
|
735
|
-
var
|
|
742
|
+
// Unchecked before checked
|
|
743
|
+
var aChecked = a.checked || a.section === 'Done' ? 1 : 0;
|
|
744
|
+
var bChecked = b.checked || b.section === 'Done' ? 1 : 0;
|
|
745
|
+
if (aChecked !== bChecked) return aChecked - bChecked;
|
|
746
|
+
// Pinned first
|
|
736
747
|
if (a.pinned && !b.pinned) return -1;
|
|
737
748
|
if (!a.pinned && b.pinned) return 1;
|
|
749
|
+
// Then by priority
|
|
750
|
+
var pa = priorityOrder[a.priority] !== undefined ? priorityOrder[a.priority] : 1;
|
|
751
|
+
var pb = priorityOrder[b.priority] !== undefined ? priorityOrder[b.priority] : 1;
|
|
738
752
|
return pa - pb;
|
|
739
753
|
}
|
|
740
754
|
|
|
@@ -756,6 +770,7 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
756
770
|
html += pinIndicator(t.pinned);
|
|
757
771
|
html += githubBadge(t.githubIssue, t.githubUrl);
|
|
758
772
|
html += priorityBadge(t.priority);
|
|
773
|
+
html += sessionBadge(t.sessionId);
|
|
759
774
|
html += '</div>';
|
|
760
775
|
html += '<div class="task-card-body">';
|
|
761
776
|
html += '<span class="task-card-text">' + esc(t.line) + '</span>';
|
|
@@ -773,6 +788,30 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
773
788
|
|
|
774
789
|
var html = '';
|
|
775
790
|
|
|
791
|
+
// Summary bar
|
|
792
|
+
var allActive = _allTasks.filter(function(t) { return t.section !== 'Done' && !t.checked; });
|
|
793
|
+
var highCount = allActive.filter(function(t) { return t.priority === 'high'; }).length;
|
|
794
|
+
var medCount = allActive.filter(function(t) { return t.priority === 'medium'; }).length;
|
|
795
|
+
var lowCount = allActive.filter(function(t) { return t.priority === 'low'; }).length;
|
|
796
|
+
var projectCounts = {};
|
|
797
|
+
allActive.forEach(function(t) { projectCounts[t.project] = (projectCounts[t.project] || 0) + 1; });
|
|
798
|
+
var topProjects = Object.keys(projectCounts).sort(function(a, b) { return projectCounts[b] - projectCounts[a]; }).slice(0, 3);
|
|
799
|
+
html += '<div class="task-summary-bar">';
|
|
800
|
+
html += '<span class="task-summary-total">' + allActive.length + ' active</span>';
|
|
801
|
+
if (highCount) html += '<span class="task-summary-pill task-summary-high">' + highCount + ' high</span>';
|
|
802
|
+
if (medCount) html += '<span class="task-summary-pill task-summary-medium">' + medCount + ' medium</span>';
|
|
803
|
+
if (lowCount) html += '<span class="task-summary-pill task-summary-low">' + lowCount + ' low</span>';
|
|
804
|
+
if (topProjects.length) {
|
|
805
|
+
html += '<span class="task-summary-projects">';
|
|
806
|
+
topProjects.forEach(function(p) { html += '<span class="task-summary-project">' + esc(p) + ' (' + projectCounts[p] + ')</span>'; });
|
|
807
|
+
html += '</span>';
|
|
808
|
+
}
|
|
809
|
+
html += '<span class="task-view-toggle" style="margin-left:auto">';
|
|
810
|
+
html += '<button class="task-view-btn' + (_taskViewMode === 'list' ? ' active' : '') + '" data-ts-action="setTaskView" data-mode="list" title="List view"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>';
|
|
811
|
+
html += '<button class="task-view-btn' + (_taskViewMode === 'compact' ? ' active' : '') + '" data-ts-action="setTaskView" data-mode="compact" title="Compact view"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><rect x="1" y="2" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="2" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="1" y="9" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="9" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/></svg></button>';
|
|
812
|
+
html += '</span>';
|
|
813
|
+
html += '</div>';
|
|
814
|
+
|
|
776
815
|
// Add task input at top (only when a specific project is selected)
|
|
777
816
|
var projects = projectFilter ? [projectFilter] : [];
|
|
778
817
|
projects.forEach(function(proj) {
|
|
@@ -785,9 +824,10 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
785
824
|
// Priority sections
|
|
786
825
|
function renderSection(title, items, icon) {
|
|
787
826
|
if (!items.length) return '';
|
|
827
|
+
var gridClass = _taskViewMode === 'compact' ? 'task-card-grid task-card-grid-compact' : 'task-card-grid';
|
|
788
828
|
var shtml = '<div class="task-priority-section">';
|
|
789
829
|
shtml += '<div class="task-section-header"><span class="task-section-icon">' + icon + '</span> ' + title + ' <span class="task-section-count">' + items.length + '</span></div>';
|
|
790
|
-
shtml += '<div class="
|
|
830
|
+
shtml += '<div class="' + gridClass + '">';
|
|
791
831
|
items.forEach(function(t) { shtml += renderTaskCard(t); });
|
|
792
832
|
shtml += '</div></div>';
|
|
793
833
|
return shtml;
|
|
@@ -934,6 +974,35 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
934
974
|
|
|
935
975
|
var isProject = Boolean(selectedProject);
|
|
936
976
|
|
|
977
|
+
// Render project info section
|
|
978
|
+
var infoSection = document.getElementById('settings-project-info-section');
|
|
979
|
+
var infoEl = document.getElementById('settings-project-info');
|
|
980
|
+
if (infoSection && infoEl) {
|
|
981
|
+
if (isProject && data.projectInfo) {
|
|
982
|
+
var pi = data.projectInfo;
|
|
983
|
+
infoSection.style.display = '';
|
|
984
|
+
var infoHtml = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;font-size:var(--text-sm)">';
|
|
985
|
+
infoHtml += '<div><strong style="color:var(--ink)">Disk path</strong><div class="text-muted" style="font-family:var(--mono);font-size:var(--text-xs);word-break:break-all">' + esc(pi.diskPath) + '</div></div>';
|
|
986
|
+
infoHtml += '<div><strong style="color:var(--ink)">Ownership</strong><div class="text-muted">' + esc(pi.ownership) + '</div></div>';
|
|
987
|
+
infoHtml += '<div><strong style="color:var(--ink)">Config file</strong><div class="text-muted" style="font-family:var(--mono);font-size:var(--text-xs);word-break:break-all">' + esc(pi.configFile) + '</div>' + (pi.configExists ? '<span style="color:var(--green);font-size:var(--text-xs)">exists</span>' : '<span style="color:var(--muted);font-size:var(--text-xs)">not created</span>') + '</div>';
|
|
988
|
+
infoHtml += '<div><strong style="color:var(--ink)">Findings</strong><div class="text-muted">' + pi.findingCount + ' entries</div></div>';
|
|
989
|
+
infoHtml += '<div><strong style="color:var(--ink)">Tasks</strong><div class="text-muted">' + pi.taskCount + ' in queue</div></div>';
|
|
990
|
+
infoHtml += '</div>';
|
|
991
|
+
var files = [];
|
|
992
|
+
if (pi.hasFindings) files.push('FINDINGS.md');
|
|
993
|
+
if (pi.hasTasks) files.push('tasks.md');
|
|
994
|
+
if (pi.hasSummary) files.push('summary.md');
|
|
995
|
+
if (pi.hasClaudeMd) files.push('CLAUDE.md');
|
|
996
|
+
if (files.length) {
|
|
997
|
+
infoHtml += '<div style="margin-top:10px;font-size:var(--text-xs);color:var(--muted)">Files: ' + files.map(function(f) { return '<span class="badge" style="margin-right:4px">' + esc(f) + '</span>'; }).join('') + '</div>';
|
|
998
|
+
}
|
|
999
|
+
infoEl.innerHTML = infoHtml;
|
|
1000
|
+
} else {
|
|
1001
|
+
infoSection.style.display = 'none';
|
|
1002
|
+
infoEl.innerHTML = '';
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
937
1006
|
function sourceBadge(isOverride) {
|
|
938
1007
|
if (!isProject) return '';
|
|
939
1008
|
return isOverride
|
|
@@ -1149,21 +1218,21 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1149
1218
|
}
|
|
1150
1219
|
list.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">' +
|
|
1151
1220
|
'<thead><tr style="border-bottom:1px solid var(--border);text-align:left">' +
|
|
1152
|
-
'<th style="padding:8px">Session</th><th style="padding:8px">Project</th><th style="padding:8px">
|
|
1153
|
-
'<th style="padding:8px">
|
|
1221
|
+
'<th style="padding:8px">Session</th><th style="padding:8px">Project</th><th style="padding:8px">Started</th>' +
|
|
1222
|
+
'<th style="padding:8px">Ended</th><th style="padding:8px">Duration</th><th style="padding:8px">Findings</th></tr></thead><tbody>' +
|
|
1154
1223
|
sessions.map(function(s) {
|
|
1155
1224
|
var id = s.sessionId.slice(0, 8);
|
|
1156
|
-
var
|
|
1225
|
+
var startDate = (s.startedAt || '').slice(0, 16).replace('T', ' ');
|
|
1226
|
+
var endDate = s.endedAt ? (s.endedAt || '').slice(0, 16).replace('T', ' ') : '<span style="color:var(--green)">active</span>';
|
|
1157
1227
|
var dur = s.durationMins != null ? s.durationMins + 'm' : '—';
|
|
1158
|
-
var status = s.status === 'active' ? '<span style="color:var(--green)">● active</span>' : 'ended';
|
|
1159
1228
|
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>' : '';
|
|
1160
1229
|
return '<tr style="border-bottom:1px solid var(--border);cursor:pointer" data-ts-action="showSessionDetail" data-session-id="' + esc(s.sessionId) + '">' +
|
|
1161
1230
|
'<td style="padding:8px;font-family:monospace">' + esc(id) + summarySnip + '</td>' +
|
|
1162
1231
|
'<td style="padding:8px">' + esc(s.project || '—') + '</td>' +
|
|
1163
|
-
'<td style="padding:8px">' + esc(
|
|
1232
|
+
'<td style="padding:8px">' + esc(startDate) + '</td>' +
|
|
1233
|
+
'<td style="padding:8px">' + endDate + '</td>' +
|
|
1164
1234
|
'<td style="padding:8px">' + esc(dur) + '</td>' +
|
|
1165
|
-
'<td style="padding:8px">' + (s.findingsAdded || 0) + '</td>'
|
|
1166
|
-
'<td style="padding:8px">' + status + '</td></tr>';
|
|
1235
|
+
'<td style="padding:8px">' + (s.findingsAdded || 0) + '</td></tr>';
|
|
1167
1236
|
}).join('') + '</tbody></table>';
|
|
1168
1237
|
}
|
|
1169
1238
|
|
|
@@ -1228,6 +1297,7 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1228
1297
|
else if (action === 'completeTask') { completeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1229
1298
|
else if (action === 'removeTask') { removeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1230
1299
|
else if (action === 'addTask') { addTaskFromUi(actionEl.getAttribute('data-project')); }
|
|
1300
|
+
else if (action === 'setTaskView') { _taskViewMode = actionEl.getAttribute('data-mode') || 'list'; filterTasks(); }
|
|
1231
1301
|
else if (action === 'setFindingSensitivity') { setFindingSensitivity(actionEl.getAttribute('data-level')); }
|
|
1232
1302
|
else if (action === 'toggleAutoCapture') { setAutoCapture(actionEl.getAttribute('data-enabled') !== 'true'); }
|
|
1233
1303
|
else if (action === 'setTaskMode') { setTaskMode(actionEl.getAttribute('data-mode')); }
|
|
@@ -1318,94 +1388,161 @@ export function renderSearchScript(authToken) {
|
|
|
1318
1388
|
|
|
1319
1389
|
var esc = window._phrenEsc;
|
|
1320
1390
|
|
|
1391
|
+
function relativeDate(iso) {
|
|
1392
|
+
if (!iso) return '';
|
|
1393
|
+
var d = new Date(iso);
|
|
1394
|
+
var now = new Date();
|
|
1395
|
+
var diff = now.getTime() - d.getTime();
|
|
1396
|
+
var days = Math.floor(diff / 86400000);
|
|
1397
|
+
if (days < 1) return 'today';
|
|
1398
|
+
if (days === 1) return '1d ago';
|
|
1399
|
+
if (days < 7) return days + 'd ago';
|
|
1400
|
+
if (days < 30) return Math.floor(days / 7) + 'w ago';
|
|
1401
|
+
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
1402
|
+
return months[d.getMonth()] + ' ' + d.getDate();
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Multi-select project filter
|
|
1406
|
+
var _selectedProjects = [];
|
|
1407
|
+
function getSelectedProjects() { return _selectedProjects.slice(); }
|
|
1408
|
+
function toggleProjectFilter(name) {
|
|
1409
|
+
var idx = _selectedProjects.indexOf(name);
|
|
1410
|
+
if (idx === -1) _selectedProjects.push(name);
|
|
1411
|
+
else _selectedProjects.splice(idx, 1);
|
|
1412
|
+
renderProjectFilterLabel();
|
|
1413
|
+
renderProjectFilterChecks();
|
|
1414
|
+
}
|
|
1415
|
+
function renderProjectFilterLabel() {
|
|
1416
|
+
var btn = document.getElementById('search-project-btn');
|
|
1417
|
+
if (!btn) return;
|
|
1418
|
+
if (!_selectedProjects.length) btn.textContent = 'All projects';
|
|
1419
|
+
else if (_selectedProjects.length === 1) btn.textContent = _selectedProjects[0];
|
|
1420
|
+
else btn.textContent = _selectedProjects.length + ' projects';
|
|
1421
|
+
}
|
|
1422
|
+
function renderProjectFilterChecks() {
|
|
1423
|
+
var items = document.querySelectorAll('#search-project-dropdown input[type=checkbox]');
|
|
1424
|
+
for (var i = 0; i < items.length; i++) {
|
|
1425
|
+
items[i].checked = _selectedProjects.indexOf(items[i].value) !== -1;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
window._phrenToggleProjectDropdown = function() {
|
|
1429
|
+
var dd = document.getElementById('search-project-dropdown');
|
|
1430
|
+
if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';
|
|
1431
|
+
};
|
|
1432
|
+
document.addEventListener('click', function(e) {
|
|
1433
|
+
var wrap = document.getElementById('search-project-wrap');
|
|
1434
|
+
var dd = document.getElementById('search-project-dropdown');
|
|
1435
|
+
if (wrap && dd && !wrap.contains(e.target)) dd.style.display = 'none';
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
function parseResults(lines) {
|
|
1439
|
+
var cards = [];
|
|
1440
|
+
var current = null;
|
|
1441
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1442
|
+
var line = lines[i];
|
|
1443
|
+
if (!line.trim()) continue;
|
|
1444
|
+
if (line.startsWith('[') && line.indexOf(']') > 0) {
|
|
1445
|
+
if (current) cards.push(current);
|
|
1446
|
+
var bracket = line.indexOf(']');
|
|
1447
|
+
var source = line.slice(1, bracket);
|
|
1448
|
+
var meta = line.slice(bracket + 1).trim();
|
|
1449
|
+
current = { source: source, meta: meta, snippets: [] };
|
|
1450
|
+
} else if (line === '(keyword fallback)') {
|
|
1451
|
+
// skip
|
|
1452
|
+
} else if (current) {
|
|
1453
|
+
current.snippets.push(line);
|
|
1454
|
+
} else {
|
|
1455
|
+
cards.push({ source: '', meta: '', snippets: [line] });
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (current) cards.push(current);
|
|
1459
|
+
return cards;
|
|
1460
|
+
}
|
|
1461
|
+
function renderCards(cards) {
|
|
1462
|
+
var html = '';
|
|
1463
|
+
for (var c = 0; c < cards.length; c++) {
|
|
1464
|
+
var card = cards[c];
|
|
1465
|
+
html += '<div class="card" style="margin-bottom:8px">';
|
|
1466
|
+
html += '<div class="card-header" style="padding:10px 14px;display:flex;align-items:center">';
|
|
1467
|
+
if (card.source) {
|
|
1468
|
+
html += '<span style="font-weight:500;font-size:var(--text-sm)">' + esc(card.source) + '</span>';
|
|
1469
|
+
}
|
|
1470
|
+
if (card.meta) {
|
|
1471
|
+
html += '<span class="text-muted" style="font-size:var(--text-xs);margin-left:8px">' + esc(card.meta) + '</span>';
|
|
1472
|
+
}
|
|
1473
|
+
html += '</div>';
|
|
1474
|
+
if (card.snippets.length) {
|
|
1475
|
+
html += '<div class="card-body" style="padding:10px 14px;font-size:var(--text-sm);white-space:pre-wrap;color:var(--ink-secondary)">';
|
|
1476
|
+
html += esc(card.snippets.join('\\n'));
|
|
1477
|
+
html += '</div>';
|
|
1478
|
+
}
|
|
1479
|
+
html += '</div>';
|
|
1480
|
+
}
|
|
1481
|
+
return html;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1321
1484
|
function doSearch() {
|
|
1322
1485
|
var q = document.getElementById('search-query').value.trim();
|
|
1323
1486
|
if (!q) return;
|
|
1324
|
-
var
|
|
1487
|
+
var projects = getSelectedProjects();
|
|
1325
1488
|
var type = document.getElementById('search-type-filter').value;
|
|
1326
1489
|
var statusEl = document.getElementById('search-status');
|
|
1327
1490
|
var resultsEl = document.getElementById('search-results');
|
|
1328
1491
|
statusEl.textContent = 'Searching...';
|
|
1329
1492
|
resultsEl.innerHTML = '';
|
|
1330
1493
|
|
|
1331
|
-
var
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
resultsEl.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">No results for \\u201c' + esc(q) + '\\u201d</div>';
|
|
1345
|
-
return;
|
|
1494
|
+
var fetches = [];
|
|
1495
|
+
if (projects.length <= 1) {
|
|
1496
|
+
var url = '/api/search?q=' + encodeURIComponent(q) + '&limit=20';
|
|
1497
|
+
if (projects.length === 1) url += '&project=' + encodeURIComponent(projects[0]);
|
|
1498
|
+
if (type) url += '&type=' + encodeURIComponent(type);
|
|
1499
|
+
fetches.push(fetch(searchAuthUrl(url)).then(function(r) { return r.json(); }));
|
|
1500
|
+
} else {
|
|
1501
|
+
for (var pi = 0; pi < projects.length; pi++) {
|
|
1502
|
+
(function(proj) {
|
|
1503
|
+
var purl = '/api/search?q=' + encodeURIComponent(q) + '&limit=20&project=' + encodeURIComponent(proj);
|
|
1504
|
+
if (type) purl += '&type=' + encodeURIComponent(type);
|
|
1505
|
+
fetches.push(fetch(searchAuthUrl(purl)).then(function(r) { return r.json(); }));
|
|
1506
|
+
})(projects[pi]);
|
|
1346
1507
|
}
|
|
1508
|
+
}
|
|
1347
1509
|
|
|
1348
|
-
|
|
1349
|
-
var
|
|
1350
|
-
var
|
|
1351
|
-
for (var
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
if (current) cards.push(current);
|
|
1356
|
-
var bracket = line.indexOf(']');
|
|
1357
|
-
var source = line.slice(1, bracket);
|
|
1358
|
-
var meta = line.slice(bracket + 1).trim();
|
|
1359
|
-
current = { source: source, meta: meta, snippets: [] };
|
|
1360
|
-
} else if (line === '(keyword fallback)') {
|
|
1361
|
-
// skip
|
|
1362
|
-
} else if (current) {
|
|
1363
|
-
current.snippets.push(line);
|
|
1364
|
-
} else {
|
|
1365
|
-
// orphan line, show as standalone
|
|
1366
|
-
cards.push({ source: '', meta: '', snippets: [line] });
|
|
1367
|
-
}
|
|
1510
|
+
Promise.all(fetches).then(function(results) {
|
|
1511
|
+
var allCards = [];
|
|
1512
|
+
var hasError = false;
|
|
1513
|
+
for (var ri = 0; ri < results.length; ri++) {
|
|
1514
|
+
if (!results[ri].ok) { hasError = true; continue; }
|
|
1515
|
+
var parsed = parseResults(results[ri].results || []);
|
|
1516
|
+
allCards = allCards.concat(parsed);
|
|
1368
1517
|
}
|
|
1369
|
-
if (
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
for (var c = 0; c < cards.length; c++) {
|
|
1374
|
-
var card = cards[c];
|
|
1375
|
-
html += '<div class="card" style="margin-bottom:8px">';
|
|
1376
|
-
html += '<div class="card-header" style="padding:10px 14px">';
|
|
1377
|
-
if (card.source) {
|
|
1378
|
-
html += '<span style="font-weight:500;font-size:var(--text-sm)">' + esc(card.source) + '</span>';
|
|
1379
|
-
}
|
|
1380
|
-
if (card.meta) {
|
|
1381
|
-
html += '<span class="text-muted" style="font-size:var(--text-xs);margin-left:8px">' + esc(card.meta) + '</span>';
|
|
1382
|
-
}
|
|
1383
|
-
html += '</div>';
|
|
1384
|
-
if (card.snippets.length) {
|
|
1385
|
-
html += '<div class="card-body" style="padding:10px 14px;font-size:var(--text-sm);white-space:pre-wrap;color:var(--ink-secondary)">';
|
|
1386
|
-
html += esc(card.snippets.join('\\n'));
|
|
1387
|
-
html += '</div>';
|
|
1388
|
-
}
|
|
1389
|
-
html += '</div>';
|
|
1518
|
+
if (!allCards.length) {
|
|
1519
|
+
statusEl.textContent = hasError ? 'Search error.' : 'No results.';
|
|
1520
|
+
resultsEl.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">' + (hasError ? 'Search failed' : 'No results for \\u201c' + esc(q) + '\\u201d') + '</div>';
|
|
1521
|
+
return;
|
|
1390
1522
|
}
|
|
1391
|
-
|
|
1523
|
+
statusEl.textContent = allCards.length + ' result(s)';
|
|
1524
|
+
resultsEl.innerHTML = renderCards(allCards);
|
|
1392
1525
|
}).catch(function(err) {
|
|
1393
1526
|
statusEl.textContent = '';
|
|
1394
1527
|
resultsEl.innerHTML = '<div style="padding:24px;color:var(--muted);text-align:center">Search error: ' + esc(String(err)) + '</div>';
|
|
1395
1528
|
});
|
|
1396
1529
|
}
|
|
1397
1530
|
|
|
1398
|
-
// Populate project filter
|
|
1531
|
+
// Populate project filter dropdown
|
|
1399
1532
|
function loadSearchProjects() {
|
|
1400
1533
|
if (_searchProjectsLoaded) return;
|
|
1401
1534
|
_searchProjectsLoaded = true;
|
|
1402
1535
|
fetch(searchAuthUrl('/api/projects')).then(function(r) { return r.json(); }).then(function(data) {
|
|
1403
1536
|
if (!data.ok) return;
|
|
1404
|
-
var
|
|
1537
|
+
var dd = document.getElementById('search-project-dropdown');
|
|
1538
|
+
if (!dd) return;
|
|
1539
|
+
var html = '';
|
|
1405
1540
|
(data.projects || []).forEach(function(p) {
|
|
1406
|
-
var
|
|
1407
|
-
|
|
1408
|
-
|
|
1541
|
+
html += '<label class="search-ms-item" style="display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;font-size:var(--text-sm);white-space:nowrap"><input type="checkbox" value="' + esc(p.name) + '" style="accent-color:var(--accent);cursor:pointer" /><span>' + esc(p.name) + '</span></label>';
|
|
1542
|
+
});
|
|
1543
|
+
dd.innerHTML = html;
|
|
1544
|
+
dd.querySelectorAll('input[type=checkbox]').forEach(function(cb) {
|
|
1545
|
+
cb.addEventListener('change', function() { toggleProjectFilter(cb.value); });
|
|
1409
1546
|
});
|
|
1410
1547
|
}).catch(function() {});
|
|
1411
1548
|
}
|
|
@@ -1454,13 +1591,9 @@ export function renderEventWiringScript() {
|
|
|
1454
1591
|
// --- Review filters ---
|
|
1455
1592
|
var reviewFilterProject = document.getElementById('review-filter-project');
|
|
1456
1593
|
if (reviewFilterProject) reviewFilterProject.addEventListener('change', function() { filterReviewCards(); });
|
|
1457
|
-
var reviewFilterMachine = document.getElementById('review-filter-machine');
|
|
1458
|
-
if (reviewFilterMachine) reviewFilterMachine.addEventListener('change', function() { filterReviewCards(); });
|
|
1459
|
-
var reviewFilterModel = document.getElementById('review-filter-model');
|
|
1460
|
-
if (reviewFilterModel) reviewFilterModel.addEventListener('change', function() { filterReviewCards(); });
|
|
1461
1594
|
|
|
1462
1595
|
var highlightBtn = document.getElementById('highlight-only-btn');
|
|
1463
|
-
if (highlightBtn) highlightBtn.addEventListener('
|
|
1596
|
+
if (highlightBtn) highlightBtn.addEventListener('change', function() { filterReviewCards(); });
|
|
1464
1597
|
|
|
1465
1598
|
// --- Graph controls ---
|
|
1466
1599
|
var graphZoomIn = document.getElementById('graph-zoom-in');
|
|
@@ -1480,6 +1613,20 @@ export function renderEventWiringScript() {
|
|
|
1480
1613
|
var sessionsFilterProject = document.getElementById('sessions-filter-project');
|
|
1481
1614
|
if (sessionsFilterProject) sessionsFilterProject.addEventListener('change', function() { loadSessions(); });
|
|
1482
1615
|
|
|
1616
|
+
// --- Mascot click animation ---
|
|
1617
|
+
var mascotSvg = document.querySelector('.header-brand svg');
|
|
1618
|
+
if (mascotSvg) {
|
|
1619
|
+
mascotSvg.addEventListener('click', function() {
|
|
1620
|
+
mascotSvg.classList.remove('popped');
|
|
1621
|
+
void mascotSvg.offsetWidth;
|
|
1622
|
+
mascotSvg.classList.add('popped');
|
|
1623
|
+
mascotSvg.addEventListener('animationend', function handler() {
|
|
1624
|
+
mascotSvg.classList.remove('popped');
|
|
1625
|
+
mascotSvg.removeEventListener('animationend', handler);
|
|
1626
|
+
});
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1483
1630
|
// --- Command palette ---
|
|
1484
1631
|
var cmdpal = document.getElementById('cmdpal');
|
|
1485
1632
|
if (cmdpal) cmdpal.addEventListener('click', function(e) { closeCmdPal(e); });
|
|
@@ -13,7 +13,7 @@ import { buildGraph, collectProjectsForUI, collectSkillsForUI, getHooksData, isA
|
|
|
13
13
|
import { CONSOLIDATION_ENTRY_THRESHOLD } from "./content-validate.js";
|
|
14
14
|
import { ensureTopicReferenceDoc, getProjectTopicsResponse, listProjectReferenceDocs, pinProjectTopicSuggestion, readReferenceContent, reclassifyLegacyTopicDocs, unpinProjectTopicSuggestion, writeProjectTopics, } from "./project-topics.js";
|
|
15
15
|
import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides, VALID_TASK_MODES } from "./governance-policy.js";
|
|
16
|
-
import { updateProjectConfigOverrides } from "./project-config.js";
|
|
16
|
+
import { readProjectConfig, updateProjectConfigOverrides } from "./project-config.js";
|
|
17
17
|
import { findSkill } from "./skill-registry.js";
|
|
18
18
|
import { setSkillEnabledAndSync } from "./skill-files.js";
|
|
19
19
|
import { listAllSessions, getSessionArtifacts } from "./mcp-session.js";
|
|
@@ -803,8 +803,31 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
803
803
|
try {
|
|
804
804
|
const { runSearch } = await import("./cli-search.js");
|
|
805
805
|
const result = await runSearch({ query, limit: Math.min(searchLimit, 50), project: searchProject, type: searchType }, phrenPath, profile || "");
|
|
806
|
+
// Build file date map from source headers like [project/filename]
|
|
807
|
+
const fileDates = {};
|
|
808
|
+
for (const line of result.lines) {
|
|
809
|
+
const srcMatch = line.match(/^\[([^\]]+)\]\s/);
|
|
810
|
+
if (srcMatch) {
|
|
811
|
+
const sourceKey = srcMatch[1];
|
|
812
|
+
if (fileDates[sourceKey])
|
|
813
|
+
continue;
|
|
814
|
+
const slashIdx = sourceKey.indexOf("/");
|
|
815
|
+
if (slashIdx > 0) {
|
|
816
|
+
const proj = sourceKey.slice(0, slashIdx);
|
|
817
|
+
const file = sourceKey.slice(slashIdx + 1);
|
|
818
|
+
try {
|
|
819
|
+
const filePath = path.join(phrenPath, proj, file);
|
|
820
|
+
if (fs.existsSync(filePath)) {
|
|
821
|
+
const stat = fs.statSync(filePath);
|
|
822
|
+
fileDates[sourceKey] = stat.mtime.toISOString();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch { /* skip */ }
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
806
829
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
807
|
-
res.end(JSON.stringify({ ok: true, query, results: result.lines }));
|
|
830
|
+
res.end(JSON.stringify({ ok: true, query, results: result.lines, fileDates }));
|
|
808
831
|
}
|
|
809
832
|
catch (err) {
|
|
810
833
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
@@ -854,6 +877,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
854
877
|
githubUrl: item.githubUrl,
|
|
855
878
|
context: item.context,
|
|
856
879
|
checked: item.checked,
|
|
880
|
+
sessionId: item.sessionId,
|
|
857
881
|
});
|
|
858
882
|
}
|
|
859
883
|
}
|
|
@@ -976,6 +1000,37 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
976
1000
|
const settingsProject = String(qs.project || "");
|
|
977
1001
|
const merged = settingsProject && isValidProjectName(settingsProject) ? mergeConfig(phrenPath, settingsProject) : null;
|
|
978
1002
|
const overrides = settingsProject && isValidProjectName(settingsProject) ? getProjectConfigOverrides(phrenPath, settingsProject) : null;
|
|
1003
|
+
// Build project info when a specific project is selected
|
|
1004
|
+
let projectInfo = null;
|
|
1005
|
+
if (settingsProject && isValidProjectName(settingsProject)) {
|
|
1006
|
+
const projectDir = path.join(phrenPath, settingsProject);
|
|
1007
|
+
const configFile = path.join(projectDir, "phren.project.yaml");
|
|
1008
|
+
const projConfig = readProjectConfig(phrenPath, settingsProject);
|
|
1009
|
+
const findingsPath = path.join(projectDir, "FINDINGS.md");
|
|
1010
|
+
const taskPath = path.join(projectDir, "tasks.md");
|
|
1011
|
+
let findingCount = 0;
|
|
1012
|
+
if (fs.existsSync(findingsPath)) {
|
|
1013
|
+
findingCount = (fs.readFileSync(findingsPath, "utf8").match(/^- /gm) || []).length;
|
|
1014
|
+
}
|
|
1015
|
+
let taskCount = 0;
|
|
1016
|
+
if (fs.existsSync(taskPath)) {
|
|
1017
|
+
const queueMatch = fs.readFileSync(taskPath, "utf8").match(/## Queue[\s\S]*?(?=## |$)/);
|
|
1018
|
+
if (queueMatch)
|
|
1019
|
+
taskCount = (queueMatch[0].match(/^- /gm) || []).length;
|
|
1020
|
+
}
|
|
1021
|
+
projectInfo = {
|
|
1022
|
+
diskPath: projConfig.sourcePath || projectDir,
|
|
1023
|
+
ownership: projConfig.ownership || "default",
|
|
1024
|
+
configFile,
|
|
1025
|
+
configExists: fs.existsSync(configFile),
|
|
1026
|
+
hasFindings: fs.existsSync(findingsPath),
|
|
1027
|
+
hasTasks: fs.existsSync(taskPath),
|
|
1028
|
+
hasSummary: fs.existsSync(path.join(projectDir, "summary.md")),
|
|
1029
|
+
hasClaudeMd: fs.existsSync(path.join(projectDir, "CLAUDE.md")),
|
|
1030
|
+
findingCount,
|
|
1031
|
+
taskCount,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
979
1034
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
980
1035
|
res.end(JSON.stringify({
|
|
981
1036
|
ok: true,
|
|
@@ -993,6 +1048,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
993
1048
|
workflowPolicy,
|
|
994
1049
|
merged,
|
|
995
1050
|
overrides,
|
|
1051
|
+
projectInfo,
|
|
996
1052
|
}));
|
|
997
1053
|
}
|
|
998
1054
|
catch (err) {
|