feat: customizable delete session command template
Symmetric with create template. Default: 'tmux kill-session -t {name}'.
Configurable in Settings → Commands tab (renamed from 'New Session').
DELETE /api/sessions/{name} reads template from settings, substitutes
{name}, runs synchronously via subprocess.run with 30s timeout.
Enables custom teardown workflows like:
amplifier-dev ~/dev/{name} --destroy
Changes:
- settings.py: add delete_session_template to DEFAULT_SETTINGS
- main.py: DELETE endpoint uses subprocess.run + template instead of run_tmux
- index.html: add delete template textarea + reset btn; rename tab to Commands
- app.js: DELETE_SESSION_DEFAULT_TEMPLATE constant, openSettings loads it,
bindStaticEventListeners wires input/reset; constant exported
Tests added:
- test_settings.py: 3 tests for default, load, and patch of delete_session_template
- test_api.py: 2 tests for custom/default template substitution via subprocess mock;
update existing test_delete_session_calls_kill_session to match new implementation
- test_frontend_html.py: 5 tests for textarea, placeholder, rows, reset btn, tab label
- test_app.mjs: 5 tests for constant definition, value, and wiring in openSettings/bindStaticEventListeners
This commit is contained in:
+30
-3
@@ -142,6 +142,7 @@ const DISPLAY_DEFAULTS = {
|
||||
notificationPermission: 'default',
|
||||
};
|
||||
const NEW_SESSION_DEFAULT_TEMPLATE = 'tmux new-session -d -s {name}';
|
||||
const DELETE_SESSION_DEFAULT_TEMPLATE = 'tmux kill-session -t {name}';
|
||||
|
||||
// ─── DOM helpers ──────────────────────────────────────────────────────────────
|
||||
function $(id) {
|
||||
@@ -1221,11 +1222,17 @@ function openSettings() {
|
||||
autoOpenEl.checked = ss && ss.auto_open !== undefined ? !!ss.auto_open : true;
|
||||
}
|
||||
|
||||
// New Session tab - populate template textarea
|
||||
// Commands tab - populate create template textarea
|
||||
const templateEl = $('setting-template');
|
||||
if (templateEl) {
|
||||
templateEl.value = (ss && ss.new_session_template) || NEW_SESSION_DEFAULT_TEMPLATE;
|
||||
}
|
||||
|
||||
// Commands tab - populate delete template textarea
|
||||
const deleteTemplateEl = $('setting-delete-template');
|
||||
if (deleteTemplateEl) {
|
||||
deleteTemplateEl.value = (ss && ss.delete_session_template) || DELETE_SESSION_DEFAULT_TEMPLATE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1687,7 +1694,7 @@ function bindStaticEventListeners() {
|
||||
});
|
||||
});
|
||||
|
||||
// New Session tab — template textarea with 500ms debounce
|
||||
// Commands tab — create template textarea with 500ms debounce
|
||||
var _templateDebounceTimer;
|
||||
on($('setting-template'), 'input', function() {
|
||||
clearTimeout(_templateDebounceTimer);
|
||||
@@ -1697,12 +1704,29 @@ function bindStaticEventListeners() {
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// New Session tab — reset button restores default template
|
||||
// Commands tab — create template reset button restores default
|
||||
on($('setting-template-reset'), 'click', function() {
|
||||
var el = $('setting-template');
|
||||
if (el) el.value = NEW_SESSION_DEFAULT_TEMPLATE;
|
||||
patchServerSetting('new_session_template', NEW_SESSION_DEFAULT_TEMPLATE);
|
||||
});
|
||||
|
||||
// Commands tab — delete template textarea with 500ms debounce
|
||||
var _deleteTemplateDebounceTimer;
|
||||
on($('setting-delete-template'), 'input', function() {
|
||||
clearTimeout(_deleteTemplateDebounceTimer);
|
||||
var val = this.value;
|
||||
_deleteTemplateDebounceTimer = setTimeout(function() {
|
||||
patchServerSetting('delete_session_template', val);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Commands tab — delete template reset button restores default
|
||||
on($('setting-delete-template-reset'), 'click', function() {
|
||||
var el = $('setting-delete-template');
|
||||
if (el) el.value = DELETE_SESSION_DEFAULT_TEMPLATE;
|
||||
patchServerSetting('delete_session_template', DELETE_SESSION_DEFAULT_TEMPLATE);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Test-only helpers ────────────────────────────────────────────────────────
|
||||
@@ -1798,6 +1822,9 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
createNewSession,
|
||||
// Kill session
|
||||
killSession,
|
||||
// Constants
|
||||
NEW_SESSION_DEFAULT_TEMPLATE,
|
||||
DELETE_SESSION_DEFAULT_TEMPLATE,
|
||||
// Test-only helpers
|
||||
_setCurrentSessions,
|
||||
_setViewMode,
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<button class="settings-tab settings-tab--active" data-tab="display">Display</button>
|
||||
<button class="settings-tab" data-tab="sessions">Sessions</button>
|
||||
<button class="settings-tab" data-tab="notifications">Notifications</button>
|
||||
<button class="settings-tab" data-tab="new-session">New Session</button>
|
||||
<button class="settings-tab" data-tab="new-session">Commands</button>
|
||||
</nav>
|
||||
<div class="settings-content">
|
||||
<div class="settings-panel" data-tab="display">
|
||||
@@ -169,6 +169,12 @@
|
||||
<span class="settings-helper">{name} is replaced with the session name</span>
|
||||
<button id="setting-template-reset" class="settings-action-btn">Reset to default</button>
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label" for="setting-delete-template">Delete session command</label>
|
||||
<textarea id="setting-delete-template" class="settings-textarea" rows="3" placeholder="tmux kill-session -t {name}"></textarea>
|
||||
<span class="settings-helper">{name} is replaced with the session name</span>
|
||||
<button id="setting-delete-template-reset" class="settings-action-btn">Reset to default</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2063,3 +2063,58 @@ test('pollSessions calls updateFaviconBadge', () => {
|
||||
'pollSessions must call updateFaviconBadge — update favicon on every poll cycle'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// --- Delete session template (task: customizable delete command) ---
|
||||
|
||||
test('app.js defines DELETE_SESSION_DEFAULT_TEMPLATE constant', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
assert.ok(
|
||||
source.includes('DELETE_SESSION_DEFAULT_TEMPLATE'),
|
||||
'app.js must define DELETE_SESSION_DEFAULT_TEMPLATE constant'
|
||||
);
|
||||
});
|
||||
|
||||
test('DELETE_SESSION_DEFAULT_TEMPLATE value is tmux kill-session -t {name}', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
assert.ok(
|
||||
source.includes("'tmux kill-session -t {name}'") || source.includes('"tmux kill-session -t {name}"'),
|
||||
"DELETE_SESSION_DEFAULT_TEMPLATE must be set to 'tmux kill-session -t {name}'"
|
||||
);
|
||||
});
|
||||
|
||||
test('openSettings loads delete_session_template from server settings', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
const fnStart = source.indexOf('function openSettings(');
|
||||
assert.ok(fnStart !== -1, 'openSettings must exist');
|
||||
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
|
||||
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 3000);
|
||||
assert.ok(
|
||||
fnBody.includes('setting-delete-template'),
|
||||
'openSettings must populate #setting-delete-template from server settings'
|
||||
);
|
||||
});
|
||||
|
||||
test('bindStaticEventListeners wires delete template input to save', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
const fnStart = source.indexOf('function bindStaticEventListeners(');
|
||||
assert.ok(fnStart !== -1, 'bindStaticEventListeners must exist');
|
||||
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
|
||||
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 6000);
|
||||
assert.ok(
|
||||
fnBody.includes('setting-delete-template'),
|
||||
'bindStaticEventListeners must wire #setting-delete-template input event to save'
|
||||
);
|
||||
});
|
||||
|
||||
test('bindStaticEventListeners wires delete template reset button', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
const fnStart = source.indexOf('function bindStaticEventListeners(');
|
||||
assert.ok(fnStart !== -1, 'bindStaticEventListeners must exist');
|
||||
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
|
||||
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 6000);
|
||||
assert.ok(
|
||||
fnBody.includes('setting-delete-template-reset'),
|
||||
'bindStaticEventListeners must wire #setting-delete-template-reset click handler'
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user