mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
Clarify worktree session archive/delete semantics
(cherry picked from commit f5c8fb58d1)
This commit is contained in:
committed by
nesquena-hermes
parent
de982d40a0
commit
2da4f108c5
+31
-2
@@ -146,6 +146,34 @@ def _active_skill_search_dirs(skills_dir: Path) -> list[Path]:
|
||||
return [p for p in dirs if p.exists()]
|
||||
|
||||
|
||||
def _worktree_retained_payload(session) -> dict:
|
||||
"""Return explicit no-cleanup metadata for worktree-backed session actions."""
|
||||
worktree_path = getattr(session, "worktree_path", None) if session else None
|
||||
if not worktree_path:
|
||||
return {}
|
||||
payload = {
|
||||
"worktree_retained": True,
|
||||
"worktree_path": worktree_path,
|
||||
}
|
||||
worktree_branch = getattr(session, "worktree_branch", None)
|
||||
worktree_repo_root = getattr(session, "worktree_repo_root", None)
|
||||
if worktree_branch:
|
||||
payload["worktree_branch"] = worktree_branch
|
||||
if worktree_repo_root:
|
||||
payload["worktree_repo_root"] = worktree_repo_root
|
||||
return payload
|
||||
|
||||
|
||||
def _worktree_retained_payload_for_session_id(sid: str) -> dict:
|
||||
try:
|
||||
return _worktree_retained_payload(get_session(sid, metadata_only=True))
|
||||
except KeyError:
|
||||
return {}
|
||||
except Exception:
|
||||
logger.debug("Failed to read worktree metadata for deleted session %s", sid)
|
||||
return {}
|
||||
|
||||
|
||||
def _skills_list_from_dir(skills_dir: Path, category: str | None = None) -> dict:
|
||||
"""List skills using an explicit local skills directory.
|
||||
|
||||
@@ -4219,6 +4247,7 @@ def handle_post(handler, parsed) -> bool:
|
||||
if cli_meta_for_delete.get("read_only"):
|
||||
return bad(handler, "Read-only imported sessions cannot be deleted from WebUI", 400)
|
||||
is_messaging_session = _is_messaging_session_id(sid)
|
||||
worktree_retained = _worktree_retained_payload_for_session_id(sid)
|
||||
# Delete from WebUI session store
|
||||
with LOCK:
|
||||
SESSIONS.pop(sid, None)
|
||||
@@ -4257,7 +4286,7 @@ def handle_post(handler, parsed) -> bool:
|
||||
delete_cli_session(sid)
|
||||
except Exception:
|
||||
logger.debug("Failed to delete CLI session %s", sid)
|
||||
return j(handler, {"ok": True})
|
||||
return j(handler, {"ok": True, **worktree_retained})
|
||||
|
||||
if parsed.path == "/api/session/clear":
|
||||
try:
|
||||
@@ -4894,7 +4923,7 @@ def handle_post(handler, parsed) -> bool:
|
||||
with _get_session_agent_lock(sid):
|
||||
s.archived = bool(body.get("archived", True))
|
||||
s.save(touch_updated_at=False)
|
||||
return j(handler, {"ok": True, "session": s.compact()})
|
||||
return j(handler, {"ok": True, "session": s.compact(), **_worktree_retained_payload(s)})
|
||||
|
||||
# ── Session move to project (POST) ──
|
||||
if parsed.path == "/api/session/move":
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
@@ -411,8 +411,10 @@ const LOCALES = {
|
||||
session_archive: 'Archive conversation',
|
||||
session_restore: 'Restore conversation',
|
||||
session_archive_desc: 'Hide this conversation until archived is shown',
|
||||
session_archive_worktree_desc: 'Hide this conversation; keep its worktree on disk',
|
||||
session_restore_desc: 'Bring this conversation back into the main list',
|
||||
session_archived: 'Session archived',
|
||||
session_archived_worktree: 'Session archived. Worktree remains on disk.',
|
||||
session_restored: 'Session restored',
|
||||
session_archive_failed: 'Archive failed: ',
|
||||
session_duplicate: 'Duplicate conversation',
|
||||
@@ -423,6 +425,11 @@ const LOCALES = {
|
||||
session_stop_response_desc: 'Cancel the running response for this conversation',
|
||||
session_delete: 'Delete conversation',
|
||||
session_delete_desc: 'Permanently remove this conversation',
|
||||
session_delete_confirm: 'Delete this conversation?',
|
||||
session_delete_worktree_desc: 'Delete only the WebUI conversation; keep the worktree on disk',
|
||||
session_delete_worktree_confirm: (path) => `Delete this conversation? The worktree at ${path} will remain on disk.`,
|
||||
session_deleted: 'Conversation deleted',
|
||||
session_deleted_worktree: 'Conversation deleted. Worktree remains on disk.',
|
||||
session_select_mode: 'Select',
|
||||
session_select_mode_desc: 'Select conversations to batch manage',
|
||||
session_select_all: 'Select all',
|
||||
@@ -433,6 +440,8 @@ const LOCALES = {
|
||||
session_batch_move: 'Move to project',
|
||||
session_batch_delete_confirm: 'Delete {0} conversations?',
|
||||
session_batch_archive_confirm: 'Archive {0} conversations?',
|
||||
session_batch_delete_worktree_confirm: 'Delete {0} conversations? {1} worktree-backed conversation(s) will leave their worktree directories on disk.',
|
||||
session_batch_archive_worktree_confirm: 'Archive {0} conversations? {1} worktree-backed conversation(s) will keep their worktree directories on disk.',
|
||||
session_no_selection: 'No conversations selected',
|
||||
// settings panel
|
||||
settings_heading_title: 'Control Center',
|
||||
@@ -2604,8 +2613,10 @@ const LOCALES = {
|
||||
session_archive: '会話をアーカイブ',
|
||||
session_restore: '会話を復元',
|
||||
session_archive_desc: 'アーカイブを表示するまでこの会話を非表示にする',
|
||||
session_archive_worktree_desc: 'この会話を非表示にし、worktree はディスク上に残します',
|
||||
session_restore_desc: 'この会話をメイン一覧に戻す',
|
||||
session_archived: 'セッションをアーカイブしました',
|
||||
session_archived_worktree: 'セッションをアーカイブしました。Worktree はディスク上に残ります。',
|
||||
session_restored: 'セッションを復元しました',
|
||||
session_archive_failed: 'アーカイブ失敗: ',
|
||||
session_duplicate: '会話を複製',
|
||||
@@ -2616,6 +2627,11 @@ const LOCALES = {
|
||||
session_stop_response_desc: 'この会話の実行中の応答をキャンセルします',
|
||||
session_delete: '会話を削除',
|
||||
session_delete_desc: 'この会話を完全に削除',
|
||||
session_delete_confirm: 'この会話を削除しますか?',
|
||||
session_delete_worktree_desc: 'WebUI の会話だけを削除し、worktree はディスク上に残します',
|
||||
session_delete_worktree_confirm: (path) => `この会話を削除しますか? ${path} の worktree はディスク上に残ります。`,
|
||||
session_deleted: '会話を削除しました',
|
||||
session_deleted_worktree: '会話を削除しました。Worktree はディスク上に残ります。',
|
||||
session_select_mode: '選択',
|
||||
session_select_mode_desc: '会話を選択して一括管理',
|
||||
session_select_all: 'すべて選択',
|
||||
@@ -2626,6 +2642,8 @@ const LOCALES = {
|
||||
session_batch_move: 'プロジェクトへ移動',
|
||||
session_batch_delete_confirm: '{0} 件の会話を削除しますか?',
|
||||
session_batch_archive_confirm: '{0} 件の会話をアーカイブしますか?',
|
||||
session_batch_delete_worktree_confirm: '{0} 件の会話を削除しますか? {1} 件の worktree 付き会話は、worktree ディレクトリをディスク上に残します。',
|
||||
session_batch_archive_worktree_confirm: '{0} 件の会話をアーカイブしますか? {1} 件の worktree 付き会話は、worktree ディレクトリをディスク上に保持します。',
|
||||
session_no_selection: '会話が選択されていません',
|
||||
// settings panel
|
||||
settings_heading_title: 'コントロールセンター',
|
||||
@@ -4149,10 +4167,17 @@ const LOCALES = {
|
||||
// Session management and settings keys (en fallback — pending translation)
|
||||
session_archive: 'Archive conversation',
|
||||
session_archive_desc: 'Hide this conversation until archived is shown',
|
||||
session_archive_worktree_desc: 'Hide this conversation; keep its worktree on disk',
|
||||
session_archive_failed: 'Archive failed: ',
|
||||
session_archived: 'Session archived',
|
||||
session_archived_worktree: 'Session archived. Worktree remains on disk.',
|
||||
session_delete: 'Delete conversation',
|
||||
session_delete_desc: 'Permanently remove this conversation',
|
||||
session_delete_confirm: 'Delete this conversation?',
|
||||
session_delete_worktree_desc: 'Delete only the WebUI conversation; keep the worktree on disk',
|
||||
session_delete_worktree_confirm: (path) => `Delete this conversation? The worktree at ${path} will remain on disk.`,
|
||||
session_deleted: 'Conversation deleted',
|
||||
session_deleted_worktree: 'Conversation deleted. Worktree remains on disk.',
|
||||
session_duplicate: 'Duplicate conversation',
|
||||
session_duplicate_desc: 'Create a copy with the same workspace and model',
|
||||
session_duplicate_failed: 'Duplicate failed: ',
|
||||
@@ -4180,6 +4205,8 @@ const LOCALES = {
|
||||
session_batch_move: 'Переместить в проект',
|
||||
session_batch_delete_confirm: 'Удалить {0} бесед(ы)?',
|
||||
session_batch_archive_confirm: 'Архивировать {0} бесед(ы)?',
|
||||
session_batch_delete_worktree_confirm: 'Удалить {0} бесед(ы)? У {1} бесед с worktree каталоги worktree останутся на диске.',
|
||||
session_batch_archive_worktree_confirm: 'Архивировать {0} бесед(ы)? У {1} бесед с worktree каталоги worktree останутся на диске.',
|
||||
session_no_selection: 'Ничего не выбрано',
|
||||
settings_dropdown_appearance: 'Appearance',
|
||||
settings_dropdown_conversation: 'Conversation',
|
||||
@@ -5170,10 +5197,17 @@ const LOCALES = {
|
||||
// Session management and settings keys (en fallback — pending translation)
|
||||
session_archive: 'Archive conversation',
|
||||
session_archive_desc: 'Hide this conversation until archived is shown',
|
||||
session_archive_worktree_desc: 'Ocultar esta conversación; conservar su worktree en disco',
|
||||
session_archive_failed: 'Archive failed: ',
|
||||
session_archived: 'Session archived',
|
||||
session_archived_worktree: 'Sesión archivada. El worktree permanece en disco.',
|
||||
session_delete: 'Delete conversation',
|
||||
session_delete_desc: 'Permanently remove this conversation',
|
||||
session_delete_confirm: '¿Eliminar esta conversación?',
|
||||
session_delete_worktree_desc: 'Eliminar solo la conversación de WebUI; conservar el worktree en disco',
|
||||
session_delete_worktree_confirm: (path) => `¿Eliminar esta conversación? El worktree en ${path} permanecerá en disco.`,
|
||||
session_deleted: 'Conversación eliminada',
|
||||
session_deleted_worktree: 'Conversación eliminada. El worktree permanece en disco.',
|
||||
session_duplicate: 'Duplicate conversation',
|
||||
session_duplicate_desc: 'Create a copy with the same workspace and model',
|
||||
session_duplicate_failed: 'Duplicate failed: ',
|
||||
@@ -5201,6 +5235,8 @@ const LOCALES = {
|
||||
session_batch_move: 'Mover al proyecto',
|
||||
session_batch_delete_confirm: '¿Eliminar {0} conversaciones?',
|
||||
session_batch_archive_confirm: '¿Archivar {0} conversaciones?',
|
||||
session_batch_delete_worktree_confirm: '¿Eliminar {0} conversaciones? {1} conversaciones con worktree dejarán sus directorios de worktree en disco.',
|
||||
session_batch_archive_worktree_confirm: '¿Archivar {0} conversaciones? {1} conversaciones con worktree conservarán sus directorios de worktree en disco.',
|
||||
session_no_selection: 'Ninguna conversación seleccionada',
|
||||
settings_dropdown_appearance: 'Appearance',
|
||||
settings_dropdown_conversation: 'Conversation',
|
||||
@@ -5931,10 +5967,17 @@ const LOCALES = {
|
||||
// Session management and settings keys (en fallback — pending translation)
|
||||
session_archive: 'Archive conversation',
|
||||
session_archive_desc: 'Hide this conversation until archived is shown',
|
||||
session_archive_worktree_desc: 'Diese Konversation ausblenden; den Worktree auf der Festplatte behalten',
|
||||
session_archive_failed: 'Archive failed: ',
|
||||
session_archived: 'Session archived',
|
||||
session_archived_worktree: 'Sitzung archiviert. Der Worktree bleibt auf der Festplatte.',
|
||||
session_delete: 'Delete conversation',
|
||||
session_delete_desc: 'Permanently remove this conversation',
|
||||
session_delete_confirm: 'Diese Konversation löschen?',
|
||||
session_delete_worktree_desc: 'Nur die WebUI-Konversation löschen; den Worktree auf der Festplatte behalten',
|
||||
session_delete_worktree_confirm: (path) => `Diese Konversation löschen? Der Worktree unter ${path} bleibt auf der Festplatte.`,
|
||||
session_deleted: 'Konversation gelöscht',
|
||||
session_deleted_worktree: 'Konversation gelöscht. Der Worktree bleibt auf der Festplatte.',
|
||||
session_duplicate: 'Duplicate conversation',
|
||||
session_duplicate_desc: 'Create a copy with the same workspace and model',
|
||||
session_duplicate_failed: 'Duplicate failed: ',
|
||||
@@ -5962,6 +6005,8 @@ const LOCALES = {
|
||||
session_batch_move: 'Zum Projekt verschieben',
|
||||
session_batch_delete_confirm: '{0} Konversationen löschen?',
|
||||
session_batch_archive_confirm: '{0} Konversationen archivieren?',
|
||||
session_batch_delete_worktree_confirm: '{0} Konversationen löschen? {1} worktree-gestützte Konversation(en) behalten ihre Worktree-Verzeichnisse auf der Festplatte.',
|
||||
session_batch_archive_worktree_confirm: '{0} Konversationen archivieren? {1} worktree-gestützte Konversation(en) behalten ihre Worktree-Verzeichnisse auf der Festplatte.',
|
||||
session_no_selection: 'Keine Konversationen ausgewählt',
|
||||
settings_dropdown_appearance: 'Appearance',
|
||||
settings_dropdown_conversation: 'Conversation',
|
||||
@@ -7233,10 +7278,17 @@ const LOCALES = {
|
||||
// Session management and settings keys (en fallback — pending translation)
|
||||
session_archive: '归档会话',
|
||||
session_archive_desc: '隐藏此会话,直到显示归档',
|
||||
session_archive_worktree_desc: '隐藏此会话;保留磁盘上的 worktree',
|
||||
session_archive_failed: '归档失败:',
|
||||
session_archived: '会话已归档',
|
||||
session_archived_worktree: '会话已归档。Worktree 仍保留在磁盘上。',
|
||||
session_delete: '删除会话',
|
||||
session_delete_desc: '永久删除此会话',
|
||||
session_delete_confirm: '删除此会话?',
|
||||
session_delete_worktree_desc: '仅删除 WebUI 会话;保留磁盘上的 worktree',
|
||||
session_delete_worktree_confirm: (path) => `删除此会话?位于 ${path} 的 worktree 将保留在磁盘上。`,
|
||||
session_deleted: '会话已删除',
|
||||
session_deleted_worktree: '会话已删除。Worktree 仍保留在磁盘上。',
|
||||
session_duplicate: '复制会话',
|
||||
session_duplicate_desc: '用相同工作区和模型创建副本',
|
||||
session_duplicate_failed: '复制失败:',
|
||||
@@ -7264,6 +7316,8 @@ const LOCALES = {
|
||||
session_batch_move: '移动到项目',
|
||||
session_batch_delete_confirm: '删除 {0} 个会话?',
|
||||
session_batch_archive_confirm: '归档 {0} 个会话?',
|
||||
session_batch_delete_worktree_confirm: '删除 {0} 个会话?其中 {1} 个 worktree 会话会把 worktree 目录保留在磁盘上。',
|
||||
session_batch_archive_worktree_confirm: '归档 {0} 个会话?其中 {1} 个 worktree 会话会把 worktree 目录保留在磁盘上。',
|
||||
session_no_selection: '未选择任何会话',
|
||||
settings_dropdown_appearance: '外观',
|
||||
settings_dropdown_conversation: '对话',
|
||||
@@ -7661,8 +7715,10 @@ const LOCALES = {
|
||||
session_archive: '封存對話',
|
||||
session_restore: '還原對話',
|
||||
session_archive_desc: '隱藏此對話,直到開啟顯示封存',
|
||||
session_archive_worktree_desc: '隱藏此對話;保留磁碟上的 worktree',
|
||||
session_restore_desc: '將此對話移回主清單',
|
||||
session_archived: '對話已封存',
|
||||
session_archived_worktree: '對話已封存。Worktree 仍保留在磁碟上。',
|
||||
session_restored: '對話已還原',
|
||||
session_archive_failed: '封存失敗:',
|
||||
session_duplicate: '複製對話',
|
||||
@@ -7673,6 +7729,11 @@ const LOCALES = {
|
||||
session_stop_response_desc: 'Cancel the running response for this conversation',
|
||||
session_delete: '刪除對話',
|
||||
session_delete_desc: '永久移除這個對話',
|
||||
session_delete_confirm: '刪除此對話?',
|
||||
session_delete_worktree_desc: '只刪除 WebUI 對話;保留磁碟上的 worktree',
|
||||
session_delete_worktree_confirm: (path) => `刪除此對話?位於 ${path} 的 worktree 將保留在磁碟上。`,
|
||||
session_deleted: '對話已刪除',
|
||||
session_deleted_worktree: '對話已刪除。Worktree 仍保留在磁碟上。',
|
||||
session_select_mode: '選取',
|
||||
session_select_mode_desc: '選取會話以批次管理',
|
||||
session_select_all: '全選',
|
||||
@@ -7683,6 +7744,8 @@ const LOCALES = {
|
||||
session_batch_move: '移至專案',
|
||||
session_batch_delete_confirm: '刪除 {0} 個會話?',
|
||||
session_batch_archive_confirm: '封存 {0} 個會話?',
|
||||
session_batch_delete_worktree_confirm: '刪除 {0} 個會話?其中 {1} 個 worktree 會話會把 worktree 目錄保留在磁碟上。',
|
||||
session_batch_archive_worktree_confirm: '封存 {0} 個會話?其中 {1} 個 worktree 會話會把 worktree 目錄保留在磁碟上。',
|
||||
session_no_selection: '未選取任何會話',
|
||||
// settings panel
|
||||
settings_heading_title: '控制中心',
|
||||
@@ -8854,8 +8917,10 @@ const LOCALES = {
|
||||
session_archive: 'Arquivar conversa',
|
||||
session_restore: 'Restaurar conversa',
|
||||
session_archive_desc: 'Esconder conversa até mostrar arquivados',
|
||||
session_archive_worktree_desc: 'Esconder esta conversa; manter o worktree no disco',
|
||||
session_restore_desc: 'Trazer conversa de volta à lista principal',
|
||||
session_archived: 'Sessão arquivada',
|
||||
session_archived_worktree: 'Sessão arquivada. O worktree permanece no disco.',
|
||||
session_restored: 'Sessão restaurada',
|
||||
session_archive_failed: 'Falha ao arquivar: ',
|
||||
session_duplicate: 'Duplicar conversa',
|
||||
@@ -8866,6 +8931,13 @@ const LOCALES = {
|
||||
session_stop_response_desc: 'Cancel the running response for this conversation',
|
||||
session_delete: 'Excluir conversa',
|
||||
session_delete_desc: 'Remover permanentemente esta conversa',
|
||||
session_delete_confirm: 'Excluir esta conversa?',
|
||||
session_delete_worktree_desc: 'Excluir apenas a conversa no WebUI; manter o worktree no disco',
|
||||
session_delete_worktree_confirm: (path) => `Excluir esta conversa? O worktree em ${path} permanecerá no disco.`,
|
||||
session_deleted: 'Conversa excluída',
|
||||
session_deleted_worktree: 'Conversa excluída. O worktree permanece no disco.',
|
||||
session_batch_delete_worktree_confirm: 'Excluir {0} conversas? {1} conversa(s) com worktree manterão seus diretórios de worktree no disco.',
|
||||
session_batch_archive_worktree_confirm: 'Arquivar {0} conversas? {1} conversa(s) com worktree manterão seus diretórios de worktree no disco.',
|
||||
// settings panel
|
||||
settings_heading_title: 'Control Center',
|
||||
settings_heading_subtitle: 'Preferências, ferramentas de conversa e controles do sistema.',
|
||||
@@ -9840,8 +9912,10 @@ const LOCALES = {
|
||||
session_archive: 'Archive conversation',
|
||||
session_restore: 'Restore conversation',
|
||||
session_archive_desc: 'Hide this conversation until archived is shown',
|
||||
session_archive_worktree_desc: '이 대화를 숨기고 worktree는 디스크에 유지합니다',
|
||||
session_restore_desc: 'Bring this conversation back into the main list',
|
||||
session_archived: 'Session archived',
|
||||
session_archived_worktree: '세션이 보관되었습니다. Worktree는 디스크에 남아 있습니다.',
|
||||
session_restored: 'Session restored',
|
||||
session_archive_failed: 'Archive failed: ',
|
||||
session_duplicate: 'Duplicate conversation',
|
||||
@@ -9852,6 +9926,11 @@ const LOCALES = {
|
||||
session_stop_response_desc: 'Cancel the running response for this conversation',
|
||||
session_delete: 'Delete conversation',
|
||||
session_delete_desc: 'Permanently remove this conversation',
|
||||
session_delete_confirm: '이 대화를 삭제하시겠습니까?',
|
||||
session_delete_worktree_desc: 'WebUI 대화만 삭제하고 worktree는 디스크에 유지합니다',
|
||||
session_delete_worktree_confirm: (path) => `이 대화를 삭제하시겠습니까? ${path}의 worktree는 디스크에 남아 있습니다.`,
|
||||
session_deleted: '대화가 삭제되었습니다',
|
||||
session_deleted_worktree: '대화가 삭제되었습니다. Worktree는 디스크에 남아 있습니다.',
|
||||
session_select_mode: '선택',
|
||||
session_select_mode_desc: '일괄 관리할 대화를 선택하세요',
|
||||
session_select_all: '전체 선택',
|
||||
@@ -9862,6 +9941,8 @@ const LOCALES = {
|
||||
session_batch_move: '프로젝트로 이동',
|
||||
session_batch_delete_confirm: '{0}개의 대화를 삭제하시겠습니까?',
|
||||
session_batch_archive_confirm: '{0}개의 대화를 보관하시겠습니까?',
|
||||
session_batch_delete_worktree_confirm: '{0}개의 대화를 삭제하시겠습니까? worktree가 있는 대화 {1}개는 worktree 디렉터리를 디스크에 남깁니다.',
|
||||
session_batch_archive_worktree_confirm: '{0}개의 대화를 보관하시겠습니까? worktree가 있는 대화 {1}개는 worktree 디렉터리를 디스크에 유지합니다.',
|
||||
session_no_selection: '선택된 대화가 없습니다',
|
||||
// settings panel
|
||||
settings_heading_title: '제어 센터',
|
||||
|
||||
+41
-9
@@ -1279,6 +1279,27 @@ const SESSION_VIRTUAL_THRESHOLD_ROWS = 80;
|
||||
let _sessionVirtualScrollList = null;
|
||||
let _sessionVirtualScrollRaf = 0;
|
||||
|
||||
function _sessionSnapshotById(sid){
|
||||
if(!sid)return null;
|
||||
if(S.session&&S.session.session_id===sid) return S.session;
|
||||
return (_allSessions||[]).find(s=>s&&s.session_id===sid)||null;
|
||||
}
|
||||
function _worktreeSessionCount(ids){
|
||||
return (ids||[]).reduce((count,sid)=>{
|
||||
const session=_sessionSnapshotById(sid);
|
||||
return count+(session&&session.worktree_path?1:0);
|
||||
},0);
|
||||
}
|
||||
function _sessionArchiveDescription(session){
|
||||
return session&&session.worktree_path?t('session_archive_worktree_desc'):t('session_archive_desc');
|
||||
}
|
||||
function _sessionArchiveToast(session){
|
||||
return session&&session.worktree_path?t('session_archived_worktree'):t('session_archived');
|
||||
}
|
||||
function _sessionDeleteDescription(session){
|
||||
return session&&session.worktree_path?t('session_delete_worktree_desc'):t('session_delete_desc');
|
||||
}
|
||||
|
||||
function _sessionIdFromLocation(){
|
||||
if(typeof window==='undefined'||!window.location) return null;
|
||||
const marker='/session/';
|
||||
@@ -1376,10 +1397,15 @@ function _renderBatchActionBar(){
|
||||
archiveBtn.textContent=t('session_batch_archive');
|
||||
archiveBtn.onclick=async()=>{
|
||||
const ids=[..._selectedSessions];
|
||||
const ok=await showConfirmDialog({message:t('session_batch_archive_confirm',ids.length),confirmLabel:t('session_batch_archive'),danger:true});
|
||||
const wtCount=_worktreeSessionCount(ids);
|
||||
const ok=await showConfirmDialog({
|
||||
message:wtCount?t('session_batch_archive_worktree_confirm',ids.length,wtCount):t('session_batch_archive_confirm',ids.length),
|
||||
confirmLabel:t('session_batch_archive'),
|
||||
danger:true
|
||||
});
|
||||
if(!ok)return;
|
||||
try{await Promise.all(ids.map(sid=>api('/api/session/archive',{method:'POST',body:JSON.stringify({session_id:sid,archived:true})})));
|
||||
showToast(t('session_archived'));exitSessionSelectMode();await renderSessionList();
|
||||
showToast(wtCount?t('session_archived_worktree'):t('session_archived'));exitSessionSelectMode();await renderSessionList();
|
||||
}catch(e){showToast('Archive failed: '+(e.message||e));}
|
||||
};bar.appendChild(archiveBtn);
|
||||
// Move
|
||||
@@ -1391,7 +1417,12 @@ function _renderBatchActionBar(){
|
||||
deleteBtn.textContent=t('session_batch_delete');
|
||||
deleteBtn.onclick=async()=>{
|
||||
const ids=[..._selectedSessions];
|
||||
const ok=await showConfirmDialog({message:t('session_batch_delete_confirm',ids.length),confirmLabel:t('delete_title'),danger:true});
|
||||
const wtCount=_worktreeSessionCount(ids);
|
||||
const ok=await showConfirmDialog({
|
||||
message:wtCount?t('session_batch_delete_worktree_confirm',ids.length,wtCount):t('session_batch_delete_confirm',ids.length),
|
||||
confirmLabel:t('delete_title'),
|
||||
danger:true
|
||||
});
|
||||
if(!ok)return;
|
||||
try{
|
||||
await Promise.all(ids.map(sid=>api('/api/session/delete',{method:'POST',body:JSON.stringify({session_id:sid})})));
|
||||
@@ -1402,7 +1433,7 @@ function _renderBatchActionBar(){
|
||||
if(remaining.sessions&&remaining.sessions.length){await loadSession(remaining.sessions[0].session_id);}
|
||||
else{$('msgInner').innerHTML='';$('emptyState').style.display='';}
|
||||
}
|
||||
showToast(t('session_delete')+' ('+ids.length+')');exitSessionSelectMode();await renderSessionList();
|
||||
showToast((wtCount?t('session_deleted_worktree'):t('session_delete'))+' ('+ids.length+')');exitSessionSelectMode();await renderSessionList();
|
||||
}catch(e){showToast('Delete failed: '+(e.message||e));}
|
||||
};bar.appendChild(deleteBtn);
|
||||
}
|
||||
@@ -1570,7 +1601,7 @@ function _openSessionActionMenu(session, anchorEl){
|
||||
));
|
||||
menu.appendChild(_buildSessionAction(
|
||||
session.archived?t('session_restore'):t('session_archive'),
|
||||
session.archived?t('session_restore_desc'):t('session_archive_desc'),
|
||||
session.archived?t('session_restore_desc'):_sessionArchiveDescription(session),
|
||||
session.archived?ICONS.unarchive:ICONS.archive,
|
||||
async()=>{
|
||||
closeSessionActionMenu();
|
||||
@@ -1579,7 +1610,7 @@ function _openSessionActionMenu(session, anchorEl){
|
||||
session.archived=!session.archived;
|
||||
if(S.session&&S.session.session_id===session.session_id) S.session.archived=session.archived;
|
||||
await renderSessionList();
|
||||
showToast(session.archived?t('session_archived'):t('session_restored'));
|
||||
showToast(session.archived?_sessionArchiveToast(session):t('session_restored'));
|
||||
}catch(err){showToast(t('session_archive_failed')+err.message);}
|
||||
}
|
||||
));
|
||||
@@ -1601,7 +1632,7 @@ function _openSessionActionMenu(session, anchorEl){
|
||||
if(!isExternalSession){
|
||||
menu.appendChild(_buildSessionAction(
|
||||
t('session_delete'),
|
||||
t('session_delete_desc'),
|
||||
_sessionDeleteDescription(session),
|
||||
ICONS.trash,
|
||||
async()=>{
|
||||
closeSessionActionMenu();
|
||||
@@ -3013,8 +3044,9 @@ if(typeof window!=='undefined'){
|
||||
}
|
||||
|
||||
async function deleteSession(sid){
|
||||
const session=_sessionSnapshotById(sid);
|
||||
const ok=await showConfirmDialog({
|
||||
message:'Delete this conversation?',
|
||||
message:session&&session.worktree_path?t('session_delete_worktree_confirm',session.worktree_path):t('session_delete_confirm'),
|
||||
confirmLabel:t('delete_title'),
|
||||
danger:true
|
||||
});
|
||||
@@ -3040,7 +3072,7 @@ async function deleteSession(sid){
|
||||
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
|
||||
}
|
||||
}
|
||||
showToast('Conversation deleted');
|
||||
showToast(session&&session.worktree_path?t('session_deleted_worktree'):t('session_deleted'));
|
||||
await renderSessionList();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import api.models as models
|
||||
import api.routes as routes
|
||||
from api.models import SESSIONS, Session
|
||||
|
||||
|
||||
def _capture_post(monkeypatch, body):
|
||||
captured = {}
|
||||
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
|
||||
monkeypatch.setattr(routes, "read_body", lambda handler: body)
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"j",
|
||||
lambda handler, payload, status=200, extra_headers=None: captured.update(
|
||||
payload=payload,
|
||||
status=status,
|
||||
)
|
||||
or True,
|
||||
)
|
||||
return captured
|
||||
|
||||
|
||||
def _isolate_session_store(tmp_path, monkeypatch):
|
||||
session_dir = tmp_path / "sessions"
|
||||
session_dir.mkdir()
|
||||
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
|
||||
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
|
||||
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
|
||||
monkeypatch.setattr(routes, "SESSION_INDEX_FILE", session_dir / "_index.json")
|
||||
SESSIONS.clear()
|
||||
return session_dir
|
||||
|
||||
|
||||
def _worktree_session(tmp_path, session_id):
|
||||
repo = tmp_path / "repo"
|
||||
worktree = repo / ".worktrees" / f"hermes-{session_id}"
|
||||
worktree.mkdir(parents=True)
|
||||
s = Session(
|
||||
session_id=session_id,
|
||||
title="Worktree session",
|
||||
workspace=str(worktree),
|
||||
worktree_path=str(worktree),
|
||||
worktree_branch=f"hermes/{session_id}",
|
||||
worktree_repo_root=str(repo),
|
||||
)
|
||||
s.save()
|
||||
return s, worktree
|
||||
|
||||
|
||||
def test_delete_worktree_session_reports_retained_worktree_without_cleanup(tmp_path, monkeypatch):
|
||||
session_dir = _isolate_session_store(tmp_path, monkeypatch)
|
||||
session, worktree = _worktree_session(tmp_path, "wtdelete1")
|
||||
captured = _capture_post(monkeypatch, {"session_id": session.session_id})
|
||||
monkeypatch.setattr(routes, "_lookup_cli_session_metadata", lambda sid: {})
|
||||
monkeypatch.setattr(routes, "_is_messaging_session_id", lambda sid: False)
|
||||
monkeypatch.setattr(models, "delete_cli_session", lambda sid: None)
|
||||
|
||||
assert routes.handle_post(object(), SimpleNamespace(path="/api/session/delete")) is True
|
||||
|
||||
assert captured["status"] == 200
|
||||
assert captured["payload"]["ok"] is True
|
||||
assert captured["payload"]["worktree_retained"] is True
|
||||
assert captured["payload"]["worktree_path"] == str(worktree.resolve())
|
||||
assert captured["payload"]["worktree_branch"] == "hermes/wtdelete1"
|
||||
assert not (session_dir / "wtdelete1.json").exists()
|
||||
assert worktree.exists(), "session delete must not remove the git worktree directory"
|
||||
|
||||
|
||||
def test_archive_worktree_session_reports_retained_worktree_without_cleanup(tmp_path, monkeypatch):
|
||||
_isolate_session_store(tmp_path, monkeypatch)
|
||||
session, worktree = _worktree_session(tmp_path, "wtarchive1")
|
||||
captured = _capture_post(
|
||||
monkeypatch,
|
||||
{"session_id": session.session_id, "archived": True},
|
||||
)
|
||||
|
||||
assert routes.handle_post(object(), SimpleNamespace(path="/api/session/archive")) is True
|
||||
|
||||
assert captured["status"] == 200
|
||||
assert captured["payload"]["ok"] is True
|
||||
assert captured["payload"]["session"]["archived"] is True
|
||||
assert captured["payload"]["worktree_retained"] is True
|
||||
assert captured["payload"]["worktree_path"] == str(worktree.resolve())
|
||||
assert worktree.exists(), "session archive must not remove the git worktree directory"
|
||||
assert Session.load("wtarchive1").archived is True
|
||||
@@ -0,0 +1,52 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def read(path):
|
||||
return (ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_delete_confirmation_mentions_retained_worktree():
|
||||
src = read("static/sessions.js")
|
||||
i18n = read("static/i18n.js")
|
||||
assert "function _sessionSnapshotById(sid)" in src
|
||||
assert "session.worktree_path?t('session_delete_worktree_confirm',session.worktree_path)" in src
|
||||
assert "session_delete_worktree_confirm" in i18n
|
||||
assert "will remain on disk" in i18n
|
||||
assert "session_delete_worktree_confirm: (path) => `Delete this conversation? The worktree at ${path} will remain on disk.`" in i18n
|
||||
assert "session_delete_worktree_desc: 'Delete only the WebUI conversation; keep the worktree on disk'" in i18n
|
||||
assert "session_deleted_worktree: 'Conversation deleted. Worktree remains on disk.'" in i18n
|
||||
|
||||
|
||||
def test_batch_archive_delete_confirmations_count_worktree_sessions():
|
||||
src = read("static/sessions.js")
|
||||
i18n = read("static/i18n.js")
|
||||
assert "function _worktreeSessionCount(ids)" in src
|
||||
assert "session_batch_delete_worktree_confirm" in src
|
||||
assert "session_batch_archive_worktree_confirm" in src
|
||||
assert "session_batch_delete_worktree_confirm" in i18n
|
||||
assert "session_batch_archive_worktree_confirm" in i18n
|
||||
|
||||
|
||||
def test_archive_and_delete_action_descriptions_are_worktree_specific():
|
||||
src = read("static/sessions.js")
|
||||
i18n = read("static/i18n.js")
|
||||
assert "function _sessionArchiveDescription(session)" in src
|
||||
assert "function _sessionDeleteDescription(session)" in src
|
||||
assert "session&&session.worktree_path?t('session_archive_worktree_desc')" in src
|
||||
assert "session&&session.worktree_path?t('session_delete_worktree_desc')" in src
|
||||
assert "session_archive_worktree_desc" in i18n
|
||||
assert "session_delete_worktree_desc" in i18n
|
||||
assert "session_archive_worktree_desc: 'Hide this conversation; keep its worktree on disk'" in i18n
|
||||
assert "session_archived_worktree: 'Session archived. Worktree remains on disk.'" in i18n
|
||||
|
||||
|
||||
def test_worktree_archive_delete_api_responses_are_explicit():
|
||||
src = read("api/routes.py")
|
||||
assert "def _worktree_retained_payload(session)" in src
|
||||
assert "def _worktree_retained_payload_for_session_id(sid: str)" in src
|
||||
assert '"worktree_retained": True' in src
|
||||
assert '{"ok": True, **worktree_retained}' in src
|
||||
assert '{"ok": True, "session": s.compact(), **_worktree_retained_payload(s)}' in src
|
||||
Reference in New Issue
Block a user