diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css
index ed681b37..2d5ed698 100644
--- a/src/web/public/mobile.css
+++ b/src/web/public/mobile.css
@@ -162,7 +162,11 @@ html.mobile-init .file-browser-panel {
}
.case-select-group {
- max-width: 150px;
+ max-width: none;
+ }
+
+ .case-combobox {
+ width: 160px;
}
.toolbar-select {
@@ -194,6 +198,27 @@ html.mobile-init .file-browser-panel {
z-index: 1300;
}
+ .command-palette-modal {
+ padding: 10vh 0.75rem 0;
+ }
+
+ .command-palette-shell {
+ width: 100%;
+ max-height: 74vh;
+ }
+
+ .command-palette-input-row {
+ grid-template-columns: 20px minmax(0, 1fr);
+ }
+
+ .command-palette-input-row kbd {
+ display: none;
+ }
+
+ .command-palette-item {
+ min-height: 56px;
+ }
+
.modal-tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
diff --git a/src/web/public/panels-ui.js b/src/web/public/panels-ui.js
index 26918a95..2a22c2af 100644
--- a/src/web/public/panels-ui.js
+++ b/src/web/public/panels-ui.js
@@ -250,6 +250,369 @@ Object.assign(CodemanApp.prototype, {
this.openImagePopup(data);
},
+ // ═══════════════════════════════════════════════════════════════
+ // Command Palette (COD-153)
+ // Fast Cmd/Ctrl+K switcher for currently open sessions, plus launch-new.
+ // ═══════════════════════════════════════════════════════════════
+
+ shouldOpenCommandPaletteFromShortcut(e) {
+ if (!e) return false;
+ const key = (e.key || '').toLowerCase();
+ if (key !== 'k' && e.code !== 'KeyK') return false;
+ if (!(e.metaKey || e.ctrlKey || e.altKey)) return false;
+
+ const target = e.target;
+ if (!target) return true;
+ const tagName = (target.tagName || '').toUpperCase();
+ const className = typeof target.className === 'string' ? target.className : '';
+ const isXtermHelper =
+ target.classList?.contains?.('xterm-helper-textarea') || className.includes('xterm-helper-textarea');
+ if (isXtermHelper) return true;
+ if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return false;
+ if (target.isContentEditable) return false;
+ if (typeof target.closest === 'function' && target.closest('[contenteditable="true"]')) return false;
+ return true;
+ },
+
+ openCommandPalette() {
+ const modal = document.getElementById('commandPaletteModal');
+ const search = document.getElementById('commandPaletteSearch');
+ if (!modal || !search) return;
+
+ this.closeMobileHeaderUtilities?.();
+ this.commandPaletteActiveIndex = 0;
+ search.value = '';
+ modal.classList.add('active');
+
+ this._wireCommandPalette();
+ this.renderCommandPalette();
+
+ search.focus();
+ search.select?.();
+ },
+
+ closeCommandPalette() {
+ const modal = document.getElementById('commandPaletteModal');
+ if (modal) modal.classList.remove('active');
+ },
+
+ _wireCommandPalette() {
+ if (this._commandPaletteWired) return;
+ this._commandPaletteWired = true;
+
+ const modal = document.getElementById('commandPaletteModal');
+ const search = document.getElementById('commandPaletteSearch');
+ const list = document.getElementById('commandPaletteList');
+
+ search?.addEventListener('input', () => {
+ this.commandPaletteActiveIndex = 0;
+ this.renderCommandPalette();
+ });
+
+ search?.addEventListener('keydown', async (e) => {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ this.moveCommandPaletteSelection(1);
+ return;
+ }
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ this.moveCommandPaletteSelection(-1);
+ return;
+ }
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ e.stopPropagation();
+ await this.activateCommandPaletteItem();
+ return;
+ }
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ this.closeCommandPalette();
+ }
+ });
+
+ modal?.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ this.closeCommandPalette();
+ }
+ });
+
+ list?.addEventListener?.('click', (e) => {
+ const row = e.target?.closest?.('[data-command-index]');
+ if (!row) return;
+ this.commandPaletteActiveIndex = Number(row.dataset.commandIndex) || 0;
+ void this.activateCommandPaletteItem();
+ });
+ },
+
+ buildCommandPaletteItems(query = '') {
+ const needle = query.trim().toLowerCase();
+ const orderedIds = [
+ ...(Array.isArray(this.sessionOrder) ? this.sessionOrder : []),
+ ...Array.from(this.sessions?.keys?.() || []).filter((id) => !this.sessionOrder?.includes?.(id)),
+ ];
+ const seen = new Set();
+ const sessionItems = [];
+
+ for (const sessionId of orderedIds) {
+ if (seen.has(sessionId)) continue;
+ seen.add(sessionId);
+ const session = this.sessions?.get?.(sessionId);
+ if (!session) continue;
+ const title = this.getSessionName?.(session) || session.name || session.title || sessionId.slice(0, 8);
+ const subtitleParts = [session.workingDir, session.mode, session.status].filter(Boolean);
+ const haystack = [title, session.workingDir, session.mode, session.status, sessionId].filter(Boolean).join(' ').toLowerCase();
+ if (needle && !haystack.includes(needle)) continue;
+ sessionItems.push({
+ id: `session:${sessionId}`,
+ type: 'session',
+ sessionId,
+ title,
+ subtitle: subtitleParts.join(' · '),
+ });
+ }
+
+ sessionItems.push(this._buildCommandPaletteNewSessionItem(query));
+ sessionItems.push({ id: 'browse-sessions', type: 'browse-sessions', title: 'Browse all sessions…', subtitle: 'Open Session Manager' });
+ return sessionItems;
+ },
+
+ _buildCommandPaletteNewSessionItem(query = '') {
+ const mode = this.runMode || this._runMode || 'claude';
+ const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini' };
+ const caseName = this._findCommandPaletteCaseMatch(query) || document.getElementById('quickStartCase')?.value || 'testcase';
+ return {
+ id: 'new-session',
+ type: 'new-session',
+ caseName,
+ title: 'New session',
+ subtitle: `Run ${labels[mode] || mode} in ${caseName}`,
+ };
+ },
+
+ _findCommandPaletteCaseMatch(query = '') {
+ const needle = query.trim().toLowerCase();
+ if (!needle || !Array.isArray(this.cases)) return null;
+
+ const scoreCase = (caseItem) => {
+ const name = String(caseItem?.name || '').trim();
+ if (!name) return 0;
+ const haystack = [
+ name,
+ caseItem?.path,
+ caseItem?.casePath,
+ caseItem?.workingDir,
+ caseItem?.remote?.path,
+ caseItem?.remote?.hostId,
+ ]
+ .filter(Boolean)
+ .join(' ')
+ .toLowerCase();
+ const lowerName = name.toLowerCase();
+ if (lowerName === needle) return 100;
+ if (lowerName.startsWith(needle)) return 90;
+ if (lowerName.includes(needle)) return 80;
+ if (haystack.includes(needle)) return 60;
+ return 0;
+ };
+
+ let best = null;
+ let bestScore = 0;
+ for (const caseItem of this.cases) {
+ const score = scoreCase(caseItem);
+ if (score > bestScore) {
+ best = caseItem;
+ bestScore = score;
+ }
+ }
+ return best?.name || null;
+ },
+
+ renderCommandPalette() {
+ const search = document.getElementById('commandPaletteSearch');
+ const list = document.getElementById('commandPaletteList');
+ if (!list) return;
+
+ const query = search?.value || '';
+ const items = this.buildCommandPaletteItems(query);
+ this.commandPaletteItems = items;
+ this.commandPaletteActiveIndex = Math.max(0, Math.min(this.commandPaletteActiveIndex || 0, items.length - 1));
+
+ list.innerHTML = items
+ .map((item, index) => {
+ const active = index === this.commandPaletteActiveIndex ? ' active' : '';
+ const icon = item.type === 'new-session' ? '+' : item.type === 'browse-sessions' ? '≡' : '›';
+ const browse = item.type === 'browse-sessions' ? ' command-palette-item--browse' : '';
+ return `
+
+ `;
+ })
+ .join('');
+ },
+
+ moveCommandPaletteSelection(delta) {
+ const items = this.commandPaletteItems || this.buildCommandPaletteItems(document.getElementById('commandPaletteSearch')?.value || '');
+ if (!items.length) return;
+ this.commandPaletteActiveIndex = (this.commandPaletteActiveIndex + delta + items.length) % items.length;
+ this.renderCommandPalette();
+ },
+
+ async activateCommandPaletteItem(index = this.commandPaletteActiveIndex || 0) {
+ const item = (this.commandPaletteItems || [])[index];
+ if (!item) return;
+
+ this.closeCommandPalette();
+ if (item.type === 'session' && item.sessionId) {
+ await this.selectSession(item.sessionId);
+ return;
+ }
+ if (item.type === 'browse-sessions') {
+ this.openSessionManager();
+ return;
+ }
+ if (item.type === 'new-session') {
+ const caseSelect = document.getElementById('quickStartCase');
+ if (caseSelect && item.caseName) {
+ if (
+ caseSelect.tagName === 'SELECT' &&
+ typeof caseSelect.appendChild === 'function' &&
+ !Array.from(caseSelect.options || []).some((option) => option.value === item.caseName)
+ ) {
+ const option = document.createElement('option');
+ option.value = item.caseName;
+ option.textContent = item.caseName;
+ caseSelect.appendChild(option);
+ }
+ caseSelect.value = item.caseName;
+ }
+ await this.run();
+ }
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // Session Manager Modal (COD-121)
+ // Persistent, header-reachable session list (GET /api/sessions/unified)
+ // reachable mid-session, with a server-side search box. Reuses the
+ // unit-2 history item renderer so clicking resumes/switches sessions.
+ // ═══════════════════════════════════════════════════════════════
+
+ async openSessionManager() {
+ const modal = document.getElementById('sessionManagerModal');
+ if (modal) {
+ modal.classList.add('active');
+ // Escape closes the modal even while focus is in the search input. A
+ // modal-scoped listener is robust regardless of the global Escape chain
+ // (which runs other close handlers first and can short-circuit). Wire once.
+ if (!this._sessionManagerEscWired) {
+ this._sessionManagerEscWired = true;
+ modal.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ e.stopPropagation();
+ this.closeSessionManager();
+ }
+ });
+ }
+ }
+
+ // Ensure cases are loaded so item subtitles can show "#caseName" labels.
+ // Mirror loadHistorySessions(): prefer already-loaded this.cases.
+ if (!Array.isArray(this.cases) || this.cases.length === 0) {
+ try {
+ const r = await fetch('/api/cases');
+ const d = r.ok ? await r.json() : null;
+ this.cases = d?.data || [];
+ } catch {
+ this.cases = this.cases || [];
+ }
+ }
+
+ const search = document.getElementById('sessionManagerSearch');
+ if (search) {
+ // Wire the debounced search input once (lazy — the element exists by
+ // the time the modal is first opened, and mixin methods are bound).
+ if (!this._sessionManagerSearchWired) {
+ this._sessionManagerSearchWired = true;
+ search.addEventListener('input', () => {
+ const value = search.value.trim();
+ this._debouncedCall('sessionManagerSearch', () => this._loadSessionManagerList(value), 200);
+ });
+ }
+ search.value = '';
+ search.focus();
+ }
+ await this._loadSessionManagerList('');
+ },
+
+ closeSessionManager() {
+ const modal = document.getElementById('sessionManagerModal');
+ if (modal) modal.classList.remove('active');
+ },
+
+ /**
+ * COD-121: live-refresh the unified session list when sessions change
+ * (created/updated/deleted via SSE). Only touches surfaces that are currently
+ * showing — the open Session Manager modal and/or the visible welcome list —
+ * and is debounced so an event burst collapses into one re-fetch. The current
+ * search query is preserved.
+ */
+ _onSessionListMaybeChanged() {
+ const modal = document.getElementById('sessionManagerModal');
+ if (modal && modal.classList.contains('active')) {
+ this._debouncedCall(
+ 'sessionManagerRefresh',
+ () => this._loadSessionManagerList(this._sessionManagerQuery || ''),
+ 400
+ );
+ }
+ const welcome = document.getElementById('welcomeOverlay');
+ if (welcome && welcome.classList.contains('visible')) {
+ this._debouncedCall('welcomeHistoryRefresh', () => this.loadHistorySessions(), 600);
+ }
+ },
+
+ async _loadSessionManagerList(q = '') {
+ this._sessionManagerQuery = q;
+ const list = document.getElementById('sessionManagerList');
+ if (!list) return;
+ try {
+ const url = '/api/sessions/unified?limit=200' + (q ? '&q=' + encodeURIComponent(q) : '');
+ const res = await fetch(url);
+ const data = await res.json();
+ const sessions = data.data?.sessions || [];
+ list.replaceChildren();
+ if (sessions.length === 0) {
+ const empty = document.createElement('p');
+ empty.className = 'empty-message';
+ empty.textContent = q ? 'No sessions match your search' : 'No sessions found';
+ list.appendChild(empty);
+ return;
+ }
+ for (const s of sessions) {
+ const item = this._buildHistoryItem(s, this.cases, { showViewAll: false });
+ // COD-130: scope the modal-close to the main (resume) row only, in the
+ // bubble phase. The ⋯ kebab button calls stopPropagation(), so clicking
+ // it (or its menu) no longer closes the Session Manager modal.
+ item.querySelector('.history-item-main')?.addEventListener('click', () => this.closeSessionManager());
+ list.appendChild(item);
+ }
+ } catch (err) {
+ console.error('[_loadSessionManagerList]', err);
+ list.replaceChildren();
+ const errLine = document.createElement('p');
+ errLine.className = 'empty-message';
+ errLine.textContent = 'Failed to load sessions';
+ list.appendChild(errLine);
+ }
+ },
// ═══════════════════════════════════════════════════════════════
// Away Digest Modal
diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js
index 733276b3..30383f78 100644
--- a/src/web/public/session-ui.js
+++ b/src/web/public/session-ui.js
@@ -44,6 +44,203 @@ Object.assign(CodemanApp.prototype, {
// Quick Start
// ═══════════════════════════════════════════════════════════════
+ formatCasePickerLabel(c) {
+ return c?.location === 'remote' && c.remote?.hostId ? `${c.name} @ ${c.remote.hostId}` : c?.name || '';
+ },
+
+ buildCasePickerOptions(cases = []) {
+ const normalized = [];
+ const seen = new Set();
+ for (const c of cases) {
+ if (!c?.name || seen.has(c.name)) continue;
+ seen.add(c.name);
+ normalized.push(c);
+ }
+ if (!seen.has('testcase')) {
+ normalized.push({ name: 'testcase' });
+ }
+
+ return normalized
+ .map(c => {
+ const label = this.formatCasePickerLabel(c);
+ const searchText = [
+ c.name,
+ label,
+ c.path,
+ c.location,
+ c.remote?.hostId,
+ c.remote?.label,
+ c.remote?.path
+ ].filter(Boolean).join(' ').toLowerCase();
+ return { name: c.name, label, case: c, searchText };
+ })
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base', numeric: true }));
+ },
+
+ filterCasePickerOptions(options, query) {
+ const terms = String(query || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
+ if (terms.length === 0) return options;
+ return options.filter(option => terms.every(term => option.searchText.includes(term)));
+ },
+
+ getCasePickerOptions() {
+ return this.buildCasePickerOptions(this.cases || []);
+ },
+
+ updateCasePickerInput(caseName) {
+ const input = document.getElementById('quickStartCaseSearch');
+ if (!input) return;
+ const option = this.getCasePickerOptions().find(item => item.name === caseName);
+ input.value = option?.label || caseName || 'testcase';
+ input.title = option?.label || input.value;
+ },
+
+ renderQuickStartCaseSelectOptions(select, options) {
+ if (!select) return;
+ select.innerHTML = options
+ .map(option => `
`)
+ .join('');
+ },
+
+ openCasePicker(filter = '') {
+ const input = document.getElementById('quickStartCaseSearch');
+ const list = document.getElementById('quickStartCaseList');
+ if (!input || !list) return;
+ this._casePickerOpen = true;
+ this._casePickerFilter = filter;
+ this._casePickerActiveIndex = 0;
+ input.setAttribute('aria-expanded', 'true');
+ this.renderCasePickerList();
+ },
+
+ closeCasePicker() {
+ const input = document.getElementById('quickStartCaseSearch');
+ const list = document.getElementById('quickStartCaseList');
+ this._casePickerOpen = false;
+ this._casePickerFilter = '';
+ input?.setAttribute('aria-expanded', 'false');
+ input?.removeAttribute('aria-activedescendant');
+ list?.classList.add('hidden');
+ },
+
+ renderCasePickerList() {
+ const input = document.getElementById('quickStartCaseSearch');
+ const list = document.getElementById('quickStartCaseList');
+ const select = document.getElementById('quickStartCase');
+ if (!input || !list || !select) return;
+
+ const options = this.filterCasePickerOptions(this.getCasePickerOptions(), this._casePickerFilter || '');
+ const selectedName = select.value || 'testcase';
+ const maxIndex = Math.max(0, options.length - 1);
+ this._casePickerActiveIndex = Math.min(Math.max(this._casePickerActiveIndex || 0, 0), maxIndex);
+
+ if (options.length === 0) {
+ list.innerHTML = '
No cases match
';
+ list.classList.remove('hidden');
+ input.removeAttribute('aria-activedescendant');
+ return;
+ }
+
+ list.innerHTML = options
+ .map((option, index) => {
+ const active = index === this._casePickerActiveIndex;
+ const selected = option.name === selectedName;
+ const id = `quickStartCaseOption-${index}`;
+ return `
+
+ `;
+ })
+ .join('');
+ list.classList.remove('hidden');
+ input.setAttribute('aria-activedescendant', `quickStartCaseOption-${this._casePickerActiveIndex}`);
+ },
+
+ selectQuickStartCase(caseName, { save = true } = {}) {
+ const select = document.getElementById('quickStartCase');
+ if (!select) return;
+ select.value = caseName || 'testcase';
+ this.updateCasePickerInput(select.value);
+ this.closeCasePicker();
+ this.updateDirDisplayForCase(select.value);
+ this.updateMobileCaseLabel(select.value);
+ if (save) {
+ this.saveLastUsedCase(select.value);
+ }
+ },
+
+ setupQuickStartCasePicker() {
+ const select = document.getElementById('quickStartCase');
+ const input = document.getElementById('quickStartCaseSearch');
+ const list = document.getElementById('quickStartCaseList');
+ const picker = document.getElementById('quickStartCasePicker');
+ if (!select || !input || !list || !picker || input.dataset.listenerAdded) return;
+
+ input.addEventListener('focus', () => {
+ input.select?.();
+ this.openCasePicker('');
+ });
+ input.addEventListener('click', () => {
+ input.select?.();
+ this.openCasePicker('');
+ });
+ input.addEventListener('input', () => {
+ this.openCasePicker(input.value);
+ });
+ input.addEventListener('keydown', event => {
+ const options = this.filterCasePickerOptions(this.getCasePickerOptions(), this._casePickerFilter || input.value);
+ if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ this._casePickerActiveIndex = Math.min((this._casePickerActiveIndex || 0) + 1, Math.max(0, options.length - 1));
+ this._casePickerOpen ? this.renderCasePickerList() : this.openCasePicker(input.value);
+ } else if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ this._casePickerActiveIndex = Math.max((this._casePickerActiveIndex || 0) - 1, 0);
+ this._casePickerOpen ? this.renderCasePickerList() : this.openCasePicker(input.value);
+ } else if (event.key === 'Enter') {
+ const option = options[this._casePickerActiveIndex || 0];
+ if (option) {
+ event.preventDefault();
+ this.selectQuickStartCase(option.name);
+ this.run?.();
+ }
+ } else if (event.key === 'Escape') {
+ event.preventDefault();
+ this.updateCasePickerInput(select.value);
+ this.closeCasePicker();
+ } else if (event.key === 'Tab') {
+ this.updateCasePickerInput(select.value);
+ this.closeCasePicker();
+ }
+ });
+ list.addEventListener('mousedown', event => event.preventDefault());
+ list.addEventListener('click', event => {
+ const option = event.target.closest?.('.case-combobox-option');
+ if (option?.dataset?.case) {
+ this.selectQuickStartCase(option.dataset.case);
+ }
+ });
+ if (document.addEventListener && !this._casePickerDocumentListenerAdded) {
+ document.addEventListener('pointerdown', event => {
+ if (!picker.contains(event.target)) {
+ this.updateCasePickerInput(select.value);
+ this.closeCasePicker();
+ }
+ });
+ this._casePickerDocumentListenerAdded = true;
+ }
+ input.dataset.listenerAdded = 'true';
+ },
+
async loadQuickStartCases(selectCaseName = null, settingsPromise = null) {
try {
// Load settings to get lastUsedCase (reuse shared promise if provided)
@@ -64,25 +261,8 @@ Object.assign(CodemanApp.prototype, {
const select = document.getElementById('quickStartCase');
- // Build options - existing cases first, then testcase as fallback if not present
- let options = '';
- const hasTestcase = cases.some(c => c.name === 'testcase');
- const isMobile = MobileDetection.getDeviceType() === 'mobile';
- const maxNameLength = isMobile ? 8 : 20; // Truncate to 8 chars on mobile
-
- cases.forEach(c => {
- const displayName = c.name.length > maxNameLength
- ? c.name.substring(0, maxNameLength) + '…'
- : c.name;
- options += `
`;
- });
-
- // Add testcase option if it doesn't exist (will be created on first run)
- if (!hasTestcase) {
- options = `
` + options;
- }
-
- select.innerHTML = options;
+ const options = this.getCasePickerOptions();
+ this.renderQuickStartCaseSelectOptions(select, options);
console.log('[loadQuickStartCases] Set options:', select.innerHTML.substring(0, 200));
// If a specific case was requested, select it
@@ -107,6 +287,9 @@ Object.assign(CodemanApp.prototype, {
document.getElementById('dirDisplay').textContent = '~/codeman-cases/testcase';
this.updateMobileCaseLabel('testcase');
}
+ this.updateCasePickerInput(select.value);
+ this.renderCasePickerList();
+ this.closeCasePicker();
// Only add event listener once (on first load)
if (!select.dataset.listenerAdded) {
@@ -114,9 +297,11 @@ Object.assign(CodemanApp.prototype, {
this.updateDirDisplayForCase(select.value);
this.saveLastUsedCase(select.value);
this.updateMobileCaseLabel(select.value);
+ this.updateCasePickerInput(select.value);
});
select.dataset.listenerAdded = 'true';
}
+ this.setupQuickStartCasePicker();
} catch (err) {
console.error('Failed to load cases:', err);
}
@@ -483,6 +668,8 @@ Object.assign(CodemanApp.prototype, {
caseData = createCaseData.data.case;
}
+ const selectedCase = (this.cases || []).find(c => c.name === caseName);
+ const isRemoteCase = caseData.location === 'remote' || selectedCase?.location === 'remote';
const workingDir = caseData.path;
if (!workingDir) throw new Error('Case path not found');
@@ -509,7 +696,7 @@ Object.assign(CodemanApp.prototype, {
fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ workingDir, mode: 'shell', name })
+ body: JSON.stringify({ ...(isRemoteCase ? { caseName } : { workingDir }), mode: 'shell', name })
}).then(r => r.json())
);
const createResults = await Promise.all(createPromises);
@@ -1484,7 +1671,14 @@ Object.assign(CodemanApp.prototype, {
// Refresh the dropdown
const select = document.getElementById('quickStartCase');
const currentCase = select.value;
+ if (currentCase === name) {
+ // Blur the native picker before reload so it doesn't show the stale value
+ select.blur?.();
+ }
await this.loadQuickStartCases(currentCase === name ? null : currentCase);
+ if (currentCase === name) {
+ await this.saveLastUsedCase(document.getElementById('quickStartCase')?.value || 'testcase');
+ }
} else {
this.showToast(data.error || 'Failed to delete case', 'error');
}
@@ -1521,11 +1715,7 @@ Object.assign(CodemanApp.prototype, {
// Build case list HTML
let html = '';
- const cases = this.cases || [];
-
- // Add testcase if not in list
- const hasTestcase = cases.some(c => c.name === 'testcase');
- const allCases = hasTestcase ? cases : [{ name: 'testcase' }, ...cases];
+ const allCases = this.getCasePickerOptions();
for (const c of allCases) {
const isSelected = c.name === currentCase;
@@ -1537,7 +1727,7 @@ Object.assign(CodemanApp.prototype, {
-
${escapeHtml(c.name)}
+
${escapeHtml(c.label)}