mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(cases): refresh the case picker and add search to Manage (#483)
The Run bar's case picker only loaded /api/cases at page load, so folders deleted or created on disk stayed listed until a reload. It now refetches on open and every 5 seconds while open, repainting only when the list changed and falling back to another case if the selected one was removed. The Manage tab of Add Case gains a search box filtering by name or path. Reorder arrows are disabled while a filter is active so a swap cannot involve a hidden case. Claude-Session: https://claude.ai/code/session_01GuHtuPiHXdykq9T6rKQJ9n Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
77ba41f8da
commit
8841bcc93f
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"aicodeman": patch
|
||||
---
|
||||
|
||||
Add a search box to the Manage tab of the Add Case dialog. It filters the case list by name or path, and the reorder arrows are disabled while a filter is active so a swap cannot involve a hidden case.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"aicodeman": patch
|
||||
---
|
||||
|
||||
The case picker now refreshes its list from `/api/cases` when it opens and every 5 seconds while it stays open, so folders deleted or created on disk appear without a page reload. If the selected case has been removed, the picker falls back to another case without saving it as the last-used one.
|
||||
@@ -3175,6 +3175,7 @@
|
||||
<h2>Manage</h2>
|
||||
</div>
|
||||
<p class="set-section-blurb">Reorder or remove cases, and pick up anything exported from a docker case.</p>
|
||||
<input type="search" id="caseManageSearch" class="set-input" placeholder="Search cases by name or path" autocomplete="off" spellcheck="false" aria-label="Search cases" oninput="app.setCaseManageFilter(this.value)" style="margin-bottom: 8px;">
|
||||
<div class="case-manage-list" id="caseManageList">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
|
||||
@@ -122,6 +122,9 @@ const RUN_MODE_LAUNCH = {
|
||||
* (`openSessionOptions`), guaranteed to drift from each other the moment a
|
||||
* ninth CLI landed in one and not the other.
|
||||
*/
|
||||
/** How often the OPEN case picker re-reads /api/cases (it also refreshes once on open). */
|
||||
const CASE_PICKER_REFRESH_MS = 5000;
|
||||
|
||||
const EXTERNAL_CLI_MODES = new Set(Object.keys(RUN_MODE_LAUNCH));
|
||||
const BUILT_IN_RUN_MODES = new Set(['claude', 'shell', ...Object.keys(RUN_MODE_LAUNCH)]);
|
||||
|
||||
@@ -246,16 +249,74 @@ Object.assign(CodemanApp.prototype, {
|
||||
const input = document.getElementById('quickStartCaseSearch');
|
||||
const list = document.getElementById('quickStartCaseList');
|
||||
if (!input || !list) return;
|
||||
const wasOpen = this._casePickerOpen === true;
|
||||
this._casePickerOpen = true;
|
||||
this._casePickerFilter = filter;
|
||||
this._casePickerActiveIndex = 0;
|
||||
input.setAttribute('aria-expanded', 'true');
|
||||
this.renderCasePickerList();
|
||||
// Every keystroke re-enters here, so only the closed -> open transition
|
||||
// refreshes and arms the timer; typing must not fire a fetch per key.
|
||||
if (!wasOpen) this._startCasePickerRefresh();
|
||||
},
|
||||
|
||||
/** Re-read the case list while the picker is open, so folders deleted or created on disk show up without a page reload. */
|
||||
_startCasePickerRefresh() {
|
||||
void this.refreshCasePickerCases();
|
||||
if (this._casePickerRefreshTimer) return;
|
||||
this._casePickerRefreshTimer = setInterval(() => void this.refreshCasePickerCases(), CASE_PICKER_REFRESH_MS);
|
||||
},
|
||||
|
||||
_stopCasePickerRefresh() {
|
||||
if (this._casePickerRefreshTimer) clearInterval(this._casePickerRefreshTimer);
|
||||
this._casePickerRefreshTimer = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lighter than loadQuickStartCases(): that one closes the picker and re-picks a
|
||||
* selection, which would yank the list away from someone mid-browse. This only
|
||||
* swaps the data and repaints, and does nothing when the list is unchanged.
|
||||
*/
|
||||
async refreshCasePickerCases() {
|
||||
if (this._casePickerRefreshInFlight) return;
|
||||
this._casePickerRefreshInFlight = true;
|
||||
try {
|
||||
const res = await fetch('/api/cases');
|
||||
if (!res.ok) return;
|
||||
const cases = (await res.json()).data;
|
||||
if (!Array.isArray(cases) || !this._casePickerOpen) return;
|
||||
const signature = list => JSON.stringify((list || []).map(c => [c.name, c.path, c.location]));
|
||||
if (signature(cases) === signature(this.cases)) return;
|
||||
this.cases = cases;
|
||||
|
||||
const select = document.getElementById('quickStartCase');
|
||||
if (select) {
|
||||
const previous = select.value;
|
||||
this.renderQuickStartCaseSelectOptions(select, this.getCasePickerOptions());
|
||||
if (cases.some(c => c.name === previous)) {
|
||||
select.value = previous;
|
||||
} else if (cases.length > 0) {
|
||||
// The selected case was removed on disk: fall back the way the initial
|
||||
// load does, without saving it as the user's last-used case.
|
||||
const fallback = cases.find(c => c.name === 'testcase') || cases[0];
|
||||
select.value = fallback.name;
|
||||
this.updateDirDisplayForCase(fallback.name);
|
||||
this.updateMobileCaseLabel(fallback.name);
|
||||
this.updateCasePickerInput(fallback.name);
|
||||
}
|
||||
}
|
||||
this.renderCasePickerList();
|
||||
} catch {
|
||||
// A failed poll leaves the list as it was; the next tick retries.
|
||||
} finally {
|
||||
this._casePickerRefreshInFlight = false;
|
||||
}
|
||||
},
|
||||
|
||||
closeCasePicker() {
|
||||
const input = document.getElementById('quickStartCaseSearch');
|
||||
const list = document.getElementById('quickStartCaseList');
|
||||
this._stopCasePickerRefresh();
|
||||
this._casePickerOpen = false;
|
||||
this._casePickerFilter = '';
|
||||
input?.setAttribute('aria-expanded', 'false');
|
||||
@@ -4286,6 +4347,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Case Management (reorder + delete)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
setCaseManageFilter(value) {
|
||||
this._caseManageFilter = String(value || '');
|
||||
this.renderCaseManageList();
|
||||
},
|
||||
|
||||
renderCaseManageList() {
|
||||
const container = document.getElementById('caseManageList');
|
||||
const cases = this.cases || [];
|
||||
@@ -4294,6 +4360,18 @@ Object.assign(CodemanApp.prototype, {
|
||||
return;
|
||||
}
|
||||
|
||||
// Every term must appear in the name or path (same rule as the Run picker's
|
||||
// filter). Reordering stays on the FULL list, so the arrows are disabled while
|
||||
// a filter is active: a swap with a neighbour the user cannot see is a surprise.
|
||||
const terms = (this._caseManageFilter || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const filtering = terms.length > 0;
|
||||
const visible = filtering
|
||||
? cases.filter(c => {
|
||||
const haystack = `${c.name} ${c.path || ''}`.toLowerCase();
|
||||
return terms.every(term => haystack.includes(term));
|
||||
})
|
||||
: cases;
|
||||
|
||||
// Cases an agent worker created (server-side marker file, see agent-case-marker.ts).
|
||||
// A long orchestration leaves one scratch directory per worker behind, so they get
|
||||
// a badge and a bulk cleanup entry point rather than having to be recognised by name.
|
||||
@@ -4305,9 +4383,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
title="Review and delete the scratch cases agent workers left behind">Clean up</button>
|
||||
</div>`
|
||||
: '';
|
||||
cases.forEach((c, idx) => {
|
||||
const isFirst = idx === 0;
|
||||
const isLast = idx === cases.length - 1;
|
||||
if (filtering && visible.length === 0) {
|
||||
html += '<div class="form-hint" style="text-align: center; padding: 2rem 0;">No cases match</div>';
|
||||
}
|
||||
visible.forEach(c => {
|
||||
const idx = cases.indexOf(c);
|
||||
const isFirst = filtering || idx === 0;
|
||||
const isLast = filtering || idx === cases.length - 1;
|
||||
const reorderTitle = filtering ? 'Clear the search to reorder' : null;
|
||||
// Was `/Users/<user>` only, the mirror image of the Run menu's bug: every
|
||||
// case path on a Linux host rendered in full, unabbreviated.
|
||||
const pathDisplay = c.path ? this._shortenHomePath(c.path) : '';
|
||||
@@ -4331,9 +4414,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
: ''
|
||||
}
|
||||
<button class="case-manage-btn" onclick="app.moveCaseUp(${escapeHtml(JSON.stringify(c.name))})"
|
||||
title="Move up" ${isFirst ? 'disabled' : ''}>▲</button>
|
||||
title="${reorderTitle || 'Move up'}" ${isFirst ? 'disabled' : ''}>▲</button>
|
||||
<button class="case-manage-btn" onclick="app.moveCaseDown(${escapeHtml(JSON.stringify(c.name))})"
|
||||
title="Move down" ${isLast ? 'disabled' : ''}>▼</button>
|
||||
title="${reorderTitle || 'Move down'}" ${isLast ? 'disabled' : ''}>▼</button>
|
||||
<button class="case-manage-btn case-manage-btn-delete" onclick="app.deleteCase(${escapeHtml(JSON.stringify(c.name))})"
|
||||
title="Delete case">✕</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user