diff --git a/pkg/miniapp/frontend/src/app.js b/pkg/miniapp/frontend/src/app.js
index 400f57676..009d500bb 100644
--- a/pkg/miniapp/frontend/src/app.js
+++ b/pkg/miniapp/frontend/src/app.js
@@ -15,7 +15,7 @@ if (!window.ORCH_ENABLED) {
var orchPanel = document.getElementById('orch');
if (orchTabBtn) orchTabBtn.style.display = 'none';
if (orchPanel) orchPanel.style.display = 'none';
- document.documentElement.style.setProperty('--tab-count', '6');
+ document.documentElement.style.setProperty('--tab-count', '7');
}
// Tab switching — re-fetch data unless SSE delivered recently
@@ -42,6 +42,7 @@ tabs.forEach((tab, index) => {
if (p === 'plan' && !fresh) loadPlan();
if (p === 'skills' && !fresh) loadSkills();
if (p === 'session' && !fresh) loadSession();
+ if (p === 'research') loadResearch();
if (p === 'git') loadGit();
if (p === 'dev' && !fresh) loadDev();
if (p === 'config') connectLogsWs();
@@ -1337,5 +1338,196 @@ window.toggleSystemPrompt = toggleSystemPrompt;
window.loadGit = loadGit;
window.saveLogSnapshot = saveLogSnapshot;
+// ── Research ──
+
+var researchCurrentTaskId = null;
+
+const STATUS_COLORS = {
+ pending: { bg: 'rgba(234,179,8,0.15)', text: '#ca8a04' },
+ active: { bg: 'rgba(59,130,246,0.15)', text: '#2563eb' },
+ completed: { bg: 'rgba(34,197,94,0.15)', text: '#16a34a' },
+ failed: { bg: 'rgba(239,68,68,0.15)', text: '#dc2626' },
+ canceled: { bg: 'rgba(107,114,128,0.15)', text: '#6b7280' },
+};
+
+async function loadResearch() {
+ var el = document.getElementById('research-tasks');
+ var loading = document.getElementById('research-loading');
+ loading.classList.remove('hidden');
+ el.innerHTML = '';
+ try {
+ var resp = await fetch(
+ API_BASE + '/miniapp/api/research?initData=' + encodeURIComponent(initData)
+ );
+ var tasks = await resp.json();
+ loading.classList.add('hidden');
+ if (!tasks || tasks.length === 0) {
+ el.innerHTML = '
No research tasks yet.
';
+ return;
+ }
+ el.innerHTML = tasks.map(function(t) {
+ var sc = STATUS_COLORS[t.status] || STATUS_COLORS.pending;
+ return '' +
+ '
' +
+ '' + esc(t.title) + '' +
+ '' + t.status + '' +
+ '
' +
+ (t.description ? '
' + esc(t.description).substring(0, 120) + '
' : '') +
+ '
' +
+ t.document_count + ' docs · ' + new Date(t.created_at).toLocaleDateString() +
+ '
' +
+ '
';
+ }).join('');
+ } catch (e) {
+ loading.classList.add('hidden');
+ el.innerHTML = 'Failed to load tasks.
';
+ }
+}
+
+function esc(s) {
+ var d = document.createElement('div');
+ d.textContent = s;
+ return d.innerHTML;
+}
+
+async function openResearchTask(id) {
+ researchCurrentTaskId = id;
+ document.getElementById('research-list-view').classList.add('hidden');
+ document.getElementById('research-detail-view').classList.remove('hidden');
+ var el = document.getElementById('research-detail-content');
+ el.innerHTML = 'Loading...
';
+ try {
+ var resp = await fetch(
+ API_BASE + '/miniapp/api/research/' + id + '?initData=' + encodeURIComponent(initData)
+ );
+ var task = await resp.json();
+ var sc = STATUS_COLORS[task.status] || STATUS_COLORS.pending;
+ var canCancel = task.status === 'pending' || task.status === 'active';
+ var canReopen = task.status === 'completed' || task.status === 'failed';
+
+ var html = '' +
+ '
' +
+ '' + esc(task.title) + '' +
+ '' + task.status + '' +
+ '
';
+ if (task.description) {
+ html += '
' + esc(task.description) + '
';
+ }
+ html += '
' +
+ 'Created: ' + new Date(task.created_at).toLocaleString() +
+ (task.completed_at ? ' · Completed: ' + new Date(task.completed_at).toLocaleString() : '') +
+ '
';
+ if (canCancel || canReopen) {
+ html += '
';
+ if (canCancel) html += '';
+ if (canReopen) html += '';
+ html += '
';
+ }
+ html += '
';
+
+ // Documents
+ html += 'Documents (' + task.documents.length + ')
';
+ if (task.documents.length === 0) {
+ html += 'No documents yet.
';
+ } else {
+ html += task.documents.map(function(d) {
+ return '' +
+ '
' +
+ '#' + d.seq + '' +
+ '' + esc(d.title) + '' +
+ '' + d.doc_type + '' +
+ '
' +
+ (d.summary ? '
' + esc(d.summary) + '
' : '') +
+ '
' +
+ '
';
+ }).join('');
+ }
+ el.innerHTML = html;
+ } catch (e) {
+ el.innerHTML = 'Failed to load task.
';
+ }
+}
+
+async function toggleResearchDoc(card, taskId, docId) {
+ var body = card.querySelector('.research-doc-body');
+ if (!body) return;
+ if (!body.classList.contains('hidden')) {
+ body.classList.add('hidden');
+ return;
+ }
+ body.classList.remove('hidden');
+ if (body.dataset.loaded) return;
+ try {
+ var resp = await fetch(
+ API_BASE + '/miniapp/api/research/' + taskId + '/doc/' + docId +
+ '?initData=' + encodeURIComponent(initData)
+ );
+ var data = await resp.json();
+ body.dataset.loaded = '1';
+ body.innerHTML = '' +
+ esc(data.content) + '
';
+ } catch (e) {
+ body.innerHTML = 'Failed to load document.
';
+ }
+}
+
+async function researchAction(taskId, action) {
+ try {
+ await fetch(
+ API_BASE + '/miniapp/api/research/' + taskId + '?initData=' + encodeURIComponent(initData),
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: action }) }
+ );
+ openResearchTask(taskId);
+ } catch (e) {
+ // ignore
+ }
+}
+
+function showResearchList() {
+ document.getElementById('research-detail-view').classList.add('hidden');
+ document.getElementById('research-list-view').classList.remove('hidden');
+ researchCurrentTaskId = null;
+ loadResearch();
+}
+
+function showNewTaskForm() {
+ document.getElementById('research-new-form').classList.remove('hidden');
+ document.getElementById('research-title').focus();
+}
+
+function hideNewTaskForm() {
+ document.getElementById('research-new-form').classList.add('hidden');
+ document.getElementById('research-title').value = '';
+ document.getElementById('research-desc').value = '';
+}
+
+async function createResearchTask() {
+ var title = document.getElementById('research-title').value.trim();
+ var desc = document.getElementById('research-desc').value.trim();
+ if (!title) return;
+ try {
+ await fetch(
+ API_BASE + '/miniapp/api/research?initData=' + encodeURIComponent(initData),
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title, description: desc }) }
+ );
+ hideNewTaskForm();
+ loadResearch();
+ } catch (e) {
+ // ignore
+ }
+}
+
+window.showNewTaskForm = showNewTaskForm;
+window.hideNewTaskForm = hideNewTaskForm;
+window.createResearchTask = createResearchTask;
+window.openResearchTask = openResearchTask;
+window.toggleResearchDoc = toggleResearchDoc;
+window.researchAction = researchAction;
+window.showResearchList = showResearchList;
+
diff --git a/pkg/miniapp/frontend/src/styles.css b/pkg/miniapp/frontend/src/styles.css
index 538f1715d..7847478d4 100644
--- a/pkg/miniapp/frontend/src/styles.css
+++ b/pkg/miniapp/frontend/src/styles.css
@@ -76,7 +76,7 @@
top: 2px;
bottom: 2px;
left: 2px;
- width: calc(100% / var(--tab-count, 7) - 2px);
+ width: calc(100% / var(--tab-count, 8) - 2px);
border-radius: 8px;
background: var(--tab-pill-bg);
box-shadow: 0 0.5px 2px rgba(0,0,0,0.12), 0 0.5px 1px rgba(0,0,0,0.08);
diff --git a/pkg/miniapp/static/dist/app.css b/pkg/miniapp/static/dist/app.css
index b871d83f5..27b0f8337 100644
--- a/pkg/miniapp/static/dist/app.css
+++ b/pkg/miniapp/static/dist/app.css
@@ -76,7 +76,7 @@ body {
.tab-indicator {
position: absolute;
- width: calc(100% / var(--tab-count, 7) - 2px);
+ width: calc(100% / var(--tab-count, 8) - 2px);
background: var(--tab-pill-bg);
z-index: 0;
border-radius: 8px;
diff --git a/pkg/miniapp/static/dist/app.js b/pkg/miniapp/static/dist/app.js
index ba4140aac..de2e00f8e 100644
--- a/pkg/miniapp/static/dist/app.js
+++ b/pkg/miniapp/static/dist/app.js
@@ -91,7 +91,7 @@
orchTabBtn.style.display = "none";
if (orchPanel)
orchPanel.style.display = "none";
- document.documentElement.style.setProperty("--tab-count", "6");
+ document.documentElement.style.setProperty("--tab-count", "7");
}
var orchTabBtn;
var orchPanel;
@@ -116,6 +116,8 @@
loadSkills();
if (p === "session" && !fresh)
loadSession();
+ if (p === "research")
+ loadResearch();
if (p === "git")
loadGit();
if (p === "dev" && !fresh)
@@ -1456,4 +1458,137 @@
window.toggleSystemPrompt = toggleSystemPrompt;
window.loadGit = loadGit;
window.saveLogSnapshot = saveLogSnapshot;
+ var researchCurrentTaskId = null;
+ var STATUS_COLORS = {
+ pending: { bg: "rgba(234,179,8,0.15)", text: "#ca8a04" },
+ active: { bg: "rgba(59,130,246,0.15)", text: "#2563eb" },
+ completed: { bg: "rgba(34,197,94,0.15)", text: "#16a34a" },
+ failed: { bg: "rgba(239,68,68,0.15)", text: "#dc2626" },
+ canceled: { bg: "rgba(107,114,128,0.15)", text: "#6b7280" }
+ };
+ async function loadResearch() {
+ var el = document.getElementById("research-tasks");
+ var loading = document.getElementById("research-loading");
+ loading.classList.remove("hidden");
+ el.innerHTML = "";
+ try {
+ var resp = await fetch(API_BASE + "/miniapp/api/research?initData=" + encodeURIComponent(initData));
+ var tasks = await resp.json();
+ loading.classList.add("hidden");
+ if (!tasks || tasks.length === 0) {
+ el.innerHTML = 'No research tasks yet.
';
+ return;
+ }
+ el.innerHTML = tasks.map(function(t) {
+ var sc = STATUS_COLORS[t.status] || STATUS_COLORS.pending;
+ return `` + '
' + '' + esc(t.title) + "" + '' + t.status + "" + "
" + (t.description ? '
' + esc(t.description).substring(0, 120) + "
" : "") + '
' + t.document_count + " docs · " + new Date(t.created_at).toLocaleDateString() + "
" + "
";
+ }).join("");
+ } catch (e) {
+ loading.classList.add("hidden");
+ el.innerHTML = 'Failed to load tasks.
';
+ }
+ }
+ function esc(s) {
+ var d = document.createElement("div");
+ d.textContent = s;
+ return d.innerHTML;
+ }
+ async function openResearchTask(id) {
+ researchCurrentTaskId = id;
+ document.getElementById("research-list-view").classList.add("hidden");
+ document.getElementById("research-detail-view").classList.remove("hidden");
+ var el = document.getElementById("research-detail-content");
+ el.innerHTML = 'Loading...
';
+ try {
+ var resp = await fetch(API_BASE + "/miniapp/api/research/" + id + "?initData=" + encodeURIComponent(initData));
+ var task = await resp.json();
+ var sc = STATUS_COLORS[task.status] || STATUS_COLORS.pending;
+ var canCancel = task.status === "pending" || task.status === "active";
+ var canReopen = task.status === "completed" || task.status === "failed";
+ var html = '' + '
' + '' + esc(task.title) + "" + '' + task.status + "" + "
";
+ if (task.description) {
+ html += '
' + esc(task.description) + "
";
+ }
+ html += '
' + "Created: " + new Date(task.created_at).toLocaleString() + (task.completed_at ? " · Completed: " + new Date(task.completed_at).toLocaleString() : "") + "
";
+ if (canCancel || canReopen) {
+ html += '
';
+ if (canCancel)
+ html += ``;
+ if (canReopen)
+ html += ``;
+ html += "
";
+ }
+ html += "
";
+ html += 'Documents (' + task.documents.length + ")
";
+ if (task.documents.length === 0) {
+ html += 'No documents yet.
';
+ } else {
+ html += task.documents.map(function(d) {
+ return `` + '
' + '#' + d.seq + "" + '' + esc(d.title) + "" + '' + d.doc_type + "" + "
" + (d.summary ? '
' + esc(d.summary) + "
" : "") + '
" + "
";
+ }).join("");
+ }
+ el.innerHTML = html;
+ } catch (e) {
+ el.innerHTML = 'Failed to load task.
';
+ }
+ }
+ async function toggleResearchDoc(card, taskId, docId) {
+ var body = card.querySelector(".research-doc-body");
+ if (!body)
+ return;
+ if (!body.classList.contains("hidden")) {
+ body.classList.add("hidden");
+ return;
+ }
+ body.classList.remove("hidden");
+ if (body.dataset.loaded)
+ return;
+ try {
+ var resp = await fetch(API_BASE + "/miniapp/api/research/" + taskId + "/doc/" + docId + "?initData=" + encodeURIComponent(initData));
+ var data = await resp.json();
+ body.dataset.loaded = "1";
+ body.innerHTML = '' + esc(data.content) + "
";
+ } catch (e) {
+ body.innerHTML = 'Failed to load document.
';
+ }
+ }
+ async function researchAction(taskId, action) {
+ try {
+ await fetch(API_BASE + "/miniapp/api/research/" + taskId + "?initData=" + encodeURIComponent(initData), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action }) });
+ openResearchTask(taskId);
+ } catch (e) {}
+ }
+ function showResearchList() {
+ document.getElementById("research-detail-view").classList.add("hidden");
+ document.getElementById("research-list-view").classList.remove("hidden");
+ researchCurrentTaskId = null;
+ loadResearch();
+ }
+ function showNewTaskForm() {
+ document.getElementById("research-new-form").classList.remove("hidden");
+ document.getElementById("research-title").focus();
+ }
+ function hideNewTaskForm() {
+ document.getElementById("research-new-form").classList.add("hidden");
+ document.getElementById("research-title").value = "";
+ document.getElementById("research-desc").value = "";
+ }
+ async function createResearchTask() {
+ var title = document.getElementById("research-title").value.trim();
+ var desc = document.getElementById("research-desc").value.trim();
+ if (!title)
+ return;
+ try {
+ await fetch(API_BASE + "/miniapp/api/research?initData=" + encodeURIComponent(initData), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, description: desc }) });
+ hideNewTaskForm();
+ loadResearch();
+ } catch (e) {}
+ }
+ window.showNewTaskForm = showNewTaskForm;
+ window.hideNewTaskForm = hideNewTaskForm;
+ window.createResearchTask = createResearchTask;
+ window.openResearchTask = openResearchTask;
+ window.toggleResearchDoc = toggleResearchDoc;
+ window.researchAction = researchAction;
+ window.showResearchList = showResearchList;
})();
diff --git a/pkg/miniapp/static/index.html b/pkg/miniapp/static/index.html
index 8a2d6b7e1..f1d950877 100644
--- a/pkg/miniapp/static/index.html
+++ b/pkg/miniapp/static/index.html
@@ -19,6 +19,7 @@
+
@@ -79,6 +80,31 @@
+
+
+
+ Research Tasks
+
+
+
+
Loading tasks...
+
+
+
+
+