diff --git a/.changeset/casemanage2.md b/.changeset/casemanage2.md
new file mode 100644
index 00000000..0b971a67
--- /dev/null
+++ b/.changeset/casemanage2.md
@@ -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.
diff --git a/.changeset/casepicker1.md b/.changeset/casepicker1.md
new file mode 100644
index 00000000..e03aa06c
--- /dev/null
+++ b/.changeset/casepicker1.md
@@ -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.
diff --git a/src/web/public/index.html b/src/web/public/index.html
index 40eb112f..ba7a254d 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -3175,6 +3175,7 @@
Manage
Reorder or remove cases, and pick up anything exported from a docker case.
+
diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js
index 7ee0e02f..6ad55178 100644
--- a/src/web/public/session-ui.js
+++ b/src/web/public/session-ui.js
@@ -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
`
: '';
- cases.forEach((c, idx) => {
- const isFirst = idx === 0;
- const isLast = idx === cases.length - 1;
+ if (filtering && visible.length === 0) {
+ html += '
No cases match
';
+ }
+ 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/` 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, {
: ''
}
+ title="${reorderTitle || 'Move up'}" ${isFirst ? 'disabled' : ''}>▲
+ title="${reorderTitle || 'Move down'}" ${isLast ? 'disabled' : ''}>▼