diff --git a/src/web/public/constants.js b/src/web/public/constants.js
index ef31a045..dd954e1d 100644
--- a/src/web/public/constants.js
+++ b/src/web/public/constants.js
@@ -291,6 +291,12 @@ const SSE_EVENTS = {
ORCHESTRATOR_TASK_FAILED: 'orchestrator:taskFailed',
ORCHESTRATOR_COMPLETED: 'orchestrator:completed',
ORCHESTRATOR_ERROR: 'orchestrator:error',
+
+ // Cases
+ CASE_CREATED: 'case:created',
+ CASE_LINKED: 'case:linked',
+ CASE_DELETED: 'case:deleted',
+ CASE_ORDER_CHANGED: 'case:order-changed',
};
// ═══════════════════════════════════════════════════════════════
diff --git a/src/web/public/index.html b/src/web/public/index.html
index 250be5aa..b8599ba1 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -1360,6 +1360,7 @@
+
@@ -1387,6 +1388,13 @@
Absolute path to an existing project folder, e.g. /home/you/my-project
+
+
+
+
+
+
Drag or use arrows to reorder. Changes are saved automatically.
+
diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js
index 8e893093..b63fa48b 100644
--- a/src/web/public/session-ui.js
+++ b/src/web/public/session-ui.js
@@ -1038,13 +1038,19 @@ Object.assign(CodemanApp.prototype, {
modal.querySelectorAll('.modal-tab-content').forEach(content => {
content.classList.toggle('hidden', content.id !== tabName);
});
- // Update submit button text
+ // Update submit button (hide for manage tab)
const submitBtn = document.getElementById('caseModalSubmit');
- submitBtn.textContent = tabName === 'case-create' ? 'Create' : 'Link';
+ if (tabName === 'case-manage') {
+ submitBtn.style.display = 'none';
+ this.renderCaseManageList();
+ } else {
+ submitBtn.style.display = '';
+ submitBtn.textContent = tabName === 'case-create' ? 'Create' : 'Link';
+ }
// Focus appropriate input
if (tabName === 'case-create') {
document.getElementById('newCaseName').focus();
- } else {
+ } else if (tabName === 'case-link') {
document.getElementById('linkCaseName').focus();
}
},
@@ -1152,6 +1158,107 @@ Object.assign(CodemanApp.prototype, {
},
+ // ═══════════════════════════════════════════════════════════════
+ // Case Management (reorder + delete)
+ // ═══════════════════════════════════════════════════════════════
+
+ renderCaseManageList() {
+ const container = document.getElementById('caseManageList');
+ const cases = this.cases || [];
+ if (cases.length === 0) {
+ container.innerHTML = '
No cases yet
';
+ return;
+ }
+
+ let html = '';
+ cases.forEach((c, idx) => {
+ const isFirst = idx === 0;
+ const isLast = idx === cases.length - 1;
+ const pathDisplay = c.path ? c.path.replace(/^\/Users\/[^/]+/, '~') : '';
+ html += `
+
+
+ ${escapeHtml(c.name)}
+ ${escapeHtml(pathDisplay)}
+
+
+
+
+
+
+
+ `;
+ });
+ container.innerHTML = html;
+ },
+
+ async moveCaseUp(name) {
+ const cases = this.cases || [];
+ const idx = cases.findIndex(c => c.name === name);
+ if (idx <= 0) return;
+ // Swap positions (immutable)
+ const reordered = [...cases];
+ [reordered[idx - 1], reordered[idx]] = [reordered[idx], reordered[idx - 1]];
+ this.cases = reordered;
+ this.renderCaseManageList();
+ await this.saveCaseOrder(reordered.map(c => c.name));
+ },
+
+ async moveCaseDown(name) {
+ const cases = this.cases || [];
+ const idx = cases.findIndex(c => c.name === name);
+ if (idx < 0 || idx >= cases.length - 1) return;
+ const reordered = [...cases];
+ [reordered[idx], reordered[idx + 1]] = [reordered[idx + 1], reordered[idx]];
+ this.cases = reordered;
+ this.renderCaseManageList();
+ await this.saveCaseOrder(reordered.map(c => c.name));
+ },
+
+ async deleteCase(name) {
+ if (!confirm(`Delete case "${name}"? Linked cases will only be unlinked (folder preserved). Created cases will be permanently deleted.`)) {
+ return;
+ }
+
+ try {
+ const res = await fetch(`/api/cases/${encodeURIComponent(name)}`, { method: 'DELETE' });
+ const data = await res.json();
+ if (data.success) {
+ this.showToast(`Case "${name}" ${data.data?.type === 'unlinked' ? 'unlinked' : 'deleted'}`, 'success');
+ // Remove from current list and refresh
+ this.cases = (this.cases || []).filter(c => c.name !== name);
+ this.renderCaseManageList();
+ // Refresh the dropdown
+ const select = document.getElementById('quickStartCase');
+ const currentCase = select.value;
+ await this.loadQuickStartCases(currentCase === name ? null : currentCase);
+ } else {
+ this.showToast(data.error || 'Failed to delete case', 'error');
+ }
+ } catch (err) {
+ this.showToast('Failed to delete case: ' + err.message, 'error');
+ }
+ },
+
+ async saveCaseOrder(order) {
+ try {
+ await fetch('/api/cases/order', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ order })
+ });
+ // Refresh dropdown to reflect new order
+ const select = document.getElementById('quickStartCase');
+ const currentCase = select.value;
+ await this.loadQuickStartCases(currentCase);
+ } catch (err) {
+ this.showToast('Failed to save case order: ' + err.message, 'error');
+ }
+ },
+
// ═══════════════════════════════════════════════════════════════
// Mobile Case Picker
// ═══════════════════════════════════════════════════════════════
@@ -1181,6 +1288,11 @@ Object.assign(CodemanApp.prototype, {
${escapeHtml(c.name)}
+
+
+