Changeset 0.25.2 (#162)

This commit is contained in:
2026-03-08 22:36:56 +00:00
parent b3f8b747dd
commit 6c484fa7f8
13 changed files with 240 additions and 151 deletions

View File

@@ -1 +1 @@
0.25.1 0.25.2

View File

@@ -101,6 +101,7 @@
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/file-tree.js?v={{.Version}}"></script> <script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/file-tree.js?v={{.Version}}"></script>
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/code-editor.js?v={{.Version}}"></script> <script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/code-editor.js?v={{.Version}}"></script>
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/note-editor.js?v={{.Version}}"></script> <script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/note-editor.js?v={{.Version}}"></script>
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/drag-resize.js?v={{.Version}}"></script>
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/pane-container.js?v={{.Version}}"></script> <script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/pane-container.js?v={{.Version}}"></script>
{{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}} {{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}}

View File

@@ -9,9 +9,9 @@
{{/* Top Bar */}} {{/* Top Bar */}}
<div class="admin-topbar"> <div class="admin-topbar">
<a href="{{.BasePath}}/" class="admin-topbar-back"> <a href="{{.BasePath}}/" class="admin-topbar-back" onclick="if(history.length>1){history.back();return false;}">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
Back to Chat Back
</a> </a>
<div class="admin-topbar-sep"></div> <div class="admin-topbar-sep"></div>
<span class="admin-topbar-title"><img src="{{.BasePath}}/favicon.svg?v={{.Version}}" alt="" width="18" height="18" style="vertical-align: -3px;"> Administration</span> <span class="admin-topbar-title"><img src="{{.BasePath}}/favicon.svg?v={{.Version}}" alt="" width="18" height="18" style="vertical-align: -3px;"> Administration</span>

View File

@@ -9,9 +9,9 @@
{{/* Top Bar */}} {{/* Top Bar */}}
<div class="settings-topbar"> <div class="settings-topbar">
<a href="{{.BasePath}}/" class="settings-topbar-back"> <a href="{{.BasePath}}/" class="settings-topbar-back" onclick="if(history.length>1){history.back();return false;}">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
Back to Chat Back
</a> </a>
<div class="settings-topbar-sep"></div> <div class="settings-topbar-sep"></div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-2)"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-2)"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>

View File

@@ -171,16 +171,13 @@
.msg-inner { max-width: 768px; margin: 0 auto; display: flex; gap: 10px; } .msg-inner { max-width: 768px; margin: 0 auto; display: flex; gap: 10px; }
/* User messages: right-aligned bubble */ /* User messages: same fixed-width container, mirrored */
.message.user { display: flex; justify-content: flex-end; }
.message.user .msg-inner { flex-direction: row-reverse; } .message.user .msg-inner { flex-direction: row-reverse; }
.message.user .msg-body { .message.user .msg-body {
background: var(--user-bubble); background: var(--user-bubble);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 16px 16px 4px 16px; border-radius: 16px 16px 4px 16px;
padding: 10px 14px; padding: 10px 14px;
flex: initial;
max-width: 85%;
} }
.message.user .msg-head { flex-direction: row-reverse; } .message.user .msg-head { flex-direction: row-reverse; }
.message.user .msg-role { color: var(--accent); } .message.user .msg-role { color: var(--accent); }

View File

@@ -112,14 +112,6 @@
.sidebar.collapsed .sb-btn { justify-content: center; padding: 8px 0; gap: 0; } .sidebar.collapsed .sb-btn { justify-content: center; padding: 8px 0; gap: 0; }
.sidebar.collapsed .split-btn-main { justify-content: center; padding: 8px 0; gap: 0; } .sidebar.collapsed .split-btn-main { justify-content: center; padding: 8px 0; gap: 0; }
.sidebar.collapsed .user-btn { justify-content: center; padding: 6px 0; gap: 0; } .sidebar.collapsed .user-btn { justify-content: center; padding: 6px 0; gap: 0; }
.sidebar.collapsed .user-flyout {
position: fixed;
left: var(--sidebar-rail);
bottom: 8px;
right: auto;
width: 200px;
margin-bottom: 0;
}
.sidebar.collapsed .avatar-bug { display: none; } .sidebar.collapsed .avatar-bug { display: none; }
/* Split button (New Chat + dropdown) */ /* Split button (New Chat + dropdown) */

View File

@@ -157,6 +157,11 @@
} }
.sidebar.collapsed .user-menu-wrap .user-flyout { .sidebar.collapsed .user-menu-wrap .user-flyout {
left: 8px; position: fixed;
right: 8px; left: var(--sidebar-rail);
bottom: 8px;
top: auto;
right: auto;
width: 200px;
margin-bottom: 0;
} }

View File

@@ -135,10 +135,12 @@ function wikilinkAutocomplete(linkCompleter) {
options: results.map(r => ({ options: results.map(r => ({
label: r.label || r.title, label: r.label || r.title,
apply: (view, completion, from, to) => { apply: (view, completion, from, to) => {
// Replace query text with completed title + closing ]] // Check if ]] already exists after cursor (from closeBrackets auto-close)
const afterCursor = view.state.doc.sliceString(to, to + 2);
const endPos = afterCursor === ']]' ? to + 2 : to;
const insert = completion.label + ']]'; const insert = completion.label + ']]';
view.dispatch({ view.dispatch({
changes: { from, to, insert }, changes: { from, to: endPos, insert },
selection: { anchor: from + insert.length }, selection: { anchor: from + insert.length },
}); });
}, },

94
src/js/drag-resize.js Normal file
View File

@@ -0,0 +1,94 @@
// ==========================================
// Chat Switchboard — Drag Resize Utility
// ==========================================
// v0.25.2: Single drag-resize primitive used by both the workspace
// handle (panels.js) and PaneContainer split handles (pane-container.js).
//
// Fixes vs the two prior implementations:
// - Full-viewport overlay during drag prevents iframes / CM6 from
// swallowing mousemove events (the "jump to middle" bug).
// - Touch support (touchstart / touchmove / touchend) everywhere.
// - Transitions on dragged elements are killed on grab and restored
// only after release — no mid-animation capture of wrong startW.
//
// Usage:
// initDragResize(handleEl, {
// direction: 'horizontal', // or 'vertical'
// onStart(clientPos) → ctx | false, // return drag context or false to cancel
// onMove(delta, ctx), // delta = px from start (signed)
// onEnd(ctx), // cleanup / persist
// });
/**
* Attach drag-resize behavior to a handle element.
*
* @param {HTMLElement} handleEl — the drag grip element
* @param {object} opts
* @param {'horizontal'|'vertical'} opts.direction
* @param {function} opts.onStart — receives initial clientX/Y, returns context or false
* @param {function} opts.onMove — receives (delta, ctx)
* @param {function} opts.onEnd — receives (ctx)
* @param {function} [opts.canDrag] — optional guard; return false to ignore mousedown
*/
function initDragResize(handleEl, opts) {
if (!handleEl) return;
const isHoriz = (opts.direction || 'horizontal') === 'horizontal';
const onPointerDown = (e) => {
// Optional guard (e.g. "only when .active class is present")
if (opts.canDrag && !opts.canDrag()) return;
e.preventDefault();
const clientPos = _clientPos(e, isHoriz);
if (clientPos == null) return;
const ctx = opts.onStart(clientPos);
if (ctx === false) return;
// ── Overlay: blocks iframes / CodeMirror from eating events ──
const overlay = document.createElement('div');
overlay.style.cssText =
'position:fixed;inset:0;z-index:99999;' +
'cursor:' + (isHoriz ? 'col-resize' : 'row-resize') + ';';
document.body.appendChild(overlay);
document.body.style.cursor = isHoriz ? 'col-resize' : 'row-resize';
document.body.style.userSelect = 'none';
const onMove = (e) => {
const pos = _clientPos(e, isHoriz);
if (pos == null) return;
opts.onMove(pos - clientPos, ctx);
};
const onUp = () => {
overlay.remove();
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onUp);
if (opts.onEnd) opts.onEnd(ctx);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
document.addEventListener('touchmove', onMove, { passive: false });
document.addEventListener('touchend', onUp);
};
handleEl.addEventListener('mousedown', onPointerDown);
handleEl.addEventListener('touchstart', onPointerDown, { passive: false });
}
/** Extract the relevant coordinate from a mouse or touch event. */
function _clientPos(e, isHoriz) {
if (e.touches?.length > 0) {
return isHoriz ? e.touches[0].clientX : e.touches[0].clientY;
}
if (e.changedTouches?.length > 0) {
return isHoriz ? e.changedTouches[0].clientX : e.changedTouches[0].clientY;
}
return isHoriz ? e.clientX : e.clientY;
}

View File

@@ -19,8 +19,6 @@ async function openNotes() {
} }
PanelRegistry.open('notes'); PanelRegistry.open('notes');
_exitSelectMode(); _exitSelectMode();
await loadNotesList();
await loadNoteFolders();
} }
async function loadNotesList(folder, searchQuery) { async function loadNotesList(folder, searchQuery) {
@@ -603,6 +601,10 @@ async function saveMessageToNote(msgIndex) {
// Check for text selection within the message element // Check for text selection within the message element
const sel = window.getSelection(); const sel = window.getSelection();
let content = msg.content; let content = msg.content;
// Strip thinking/reasoning blocks — only the response matters in notes
content = content.replace(/<(?:thinking|think)>[\s\S]*?<\/(?:thinking|think)>/gi, '').trim();
const msgId = msg.id || ''; const msgId = msg.id || '';
const msgEl = msgId ? document.querySelector(`.message[data-msg-id="${msgId}"] .msg-text`) : null; const msgEl = msgId ? document.querySelector(`.message[data-msg-id="${msgId}"] .msg-text`) : null;
if (sel && sel.rangeCount > 0 && msgEl?.contains(sel.anchorNode)) { if (sel && sel.rangeCount > 0 && msgEl?.contains(sel.anchorNode)) {
@@ -820,7 +822,18 @@ function _registerNotesPanel() {
</div> </div>
</div> </div>
<!-- Graph view --> <!-- Graph view -->
<div id="notesGraphView" style="display:none;"></div>`; <div id="notesGraphView" class="notes-graph-view" style="display:none;">
<div class="notes-graph-toolbar">
<button class="btn-small" id="notesGraphBackBtn" title="Back to list">← Back</button>
<span class="notes-graph-stats">
<span id="graphNodeCount">0</span> notes · <span id="graphEdgeCount">0</span> links
</span>
<div style="flex:1"></div>
<button class="btn-small" onclick="_graphToggleOrphans()" title="Toggle orphan nodes">Orphans</button>
<button class="btn-small" onclick="_graphResetZoom()" title="Reset zoom">Reset</button>
</div>
<canvas id="noteGraphCanvas" style="width:100%;display:block;"></canvas>
</div>`;
body.appendChild(el); body.appendChild(el);
// ── Wire listeners ── // ── Wire listeners ──
@@ -833,6 +846,9 @@ function _registerNotesPanel() {
if (typeof openNoteGraph === 'function') openNoteGraph(); if (typeof openNoteGraph === 'function') openNoteGraph();
}); });
el.querySelector('#notesTodayBtn').addEventListener('click', openDailyNote); el.querySelector('#notesTodayBtn').addEventListener('click', openDailyNote);
el.querySelector('#notesGraphBackBtn').addEventListener('click', () => {
if (typeof closeNoteGraph === 'function') closeNoteGraph();
});
el.querySelector('#notesSelectModeBtn').addEventListener('click', () => { el.querySelector('#notesSelectModeBtn').addEventListener('click', () => {
if (_notesSelectMode) _exitSelectMode(); if (_notesSelectMode) _exitSelectMode();
else _enterSelectMode(); else _enterSelectMode();
@@ -890,7 +906,9 @@ function _registerNotesPanel() {
element: el, element: el,
label: 'Notes', label: 'Notes',
onOpen() { onOpen() {
// Loading is handled by openNotes() which calls loadNotesList // Ensure notes are loaded whenever the panel opens
loadNotesList();
loadNoteFolders();
}, },
onClose() { onClose() {
_destroyNoteEditor(); _destroyNoteEditor();

View File

@@ -345,26 +345,28 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
splitEl.appendChild(handle); splitEl.appendChild(handle);
} }
// Drag logic // Track adjacent panes for dblclick reset
let startX, startY, leftEl, rightEl, startLeftBasis, startRightBasis; let lastLeftEl = null, lastRightEl = null;
const onMouseDown = (e) => { initDragResize(handle, {
e.preventDefault(); direction,
const allPanes = Array.from(splitEl.querySelectorAll(':scope > .pane, :scope > .pane-split')); onStart(_clientPos) {
// Find the two panes adjacent to this handle // Find the two panes adjacent to this handle
const handleIdx = Array.from(splitEl.children).indexOf(handle); const handleIdx = Array.from(splitEl.children).indexOf(handle);
leftEl = splitEl.children[handleIdx - 1]; const leftEl = splitEl.children[handleIdx - 1];
rightEl = splitEl.children[handleIdx + 1]; const rightEl = splitEl.children[handleIdx + 1];
if (!leftEl || !rightEl) return; if (!leftEl || !rightEl) return false;
startX = e.clientX; lastLeftEl = leftEl;
startY = e.clientY; lastRightEl = rightEl;
// Use current rendered size (matches what flex layout computed)
startLeftBasis = leftEl.getBoundingClientRect().width;
startRightBasis = rightEl.getBoundingClientRect().width;
// Immediately lock the non-flex panes to their current sizes const isHoriz = direction === 'horizontal';
// so the first move delta doesn't cause a visual snap const dim = isHoriz ? 'width' : 'height';
const startLeftBasis = leftEl.getBoundingClientRect()[dim];
const startRightBasis = rightEl.getBoundingClientRect()[dim];
// Lock non-flex panes to current rendered size so the first
// move delta doesn't cause a visual snap
if (leftEl.dataset.paneFlex !== '1') { if (leftEl.dataset.paneFlex !== '1') {
leftEl.style.flexBasis = startLeftBasis + 'px'; leftEl.style.flexBasis = startLeftBasis + 'px';
leftEl.style.flexGrow = '0'; leftEl.style.flexGrow = '0';
@@ -377,37 +379,28 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
} }
handle.classList.add('pane-handle--active'); handle.classList.add('pane-handle--active');
document.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const onMouseMove = (e) => {
const delta = direction === 'horizontal'
? e.clientX - startX
: e.clientY - startY;
return { leftEl, rightEl, startLeftBasis, startRightBasis };
},
onMove(delta, ctx) {
const { leftEl, rightEl, startLeftBasis, startRightBasis } = ctx;
const leftIsFlex = leftEl.dataset.paneFlex === '1'; const leftIsFlex = leftEl.dataset.paneFlex === '1';
const rightIsFlex = rightEl.dataset.paneFlex === '1'; const rightIsFlex = rightEl.dataset.paneFlex === '1';
// Cap: no single fixed pane should exceed 60% of container const isHoriz = direction === 'horizontal';
const maxSize = splitEl.getBoundingClientRect().width * 0.6; const dim = isHoriz ? 'width' : 'height';
const maxSize = splitEl.getBoundingClientRect()[dim] * 0.6;
if (leftIsFlex) { if (leftIsFlex) {
// Only adjust the right (fixed) pane; left flex absorbs remainder
const newRight = Math.min(maxSize, Math.max(50, startRightBasis - delta)); const newRight = Math.min(maxSize, Math.max(50, startRightBasis - delta));
rightEl.style.flexBasis = newRight + 'px'; rightEl.style.flexBasis = newRight + 'px';
rightEl.style.flexGrow = '0'; rightEl.style.flexGrow = '0';
rightEl.style.flexShrink = '0'; rightEl.style.flexShrink = '0';
} else if (rightIsFlex) { } else if (rightIsFlex) {
// Only adjust the left (fixed) pane; right flex absorbs remainder
const newLeft = Math.min(maxSize, Math.max(50, startLeftBasis + delta)); const newLeft = Math.min(maxSize, Math.max(50, startLeftBasis + delta));
leftEl.style.flexBasis = newLeft + 'px'; leftEl.style.flexBasis = newLeft + 'px';
leftEl.style.flexGrow = '0'; leftEl.style.flexGrow = '0';
leftEl.style.flexShrink = '0'; leftEl.style.flexShrink = '0';
} else { } else {
// Both fixed — adjust both (original behavior)
const newLeft = Math.max(50, startLeftBasis + delta); const newLeft = Math.max(50, startLeftBasis + delta);
const newRight = Math.max(50, startRightBasis - delta); const newRight = Math.max(50, startRightBasis - delta);
leftEl.style.flexBasis = newLeft + 'px'; leftEl.style.flexBasis = newLeft + 'px';
@@ -417,43 +410,34 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
rightEl.style.flexGrow = '0'; rightEl.style.flexGrow = '0';
rightEl.style.flexShrink = '0'; rightEl.style.flexShrink = '0';
} }
}; },
onEnd(_ctx) {
const onMouseUp = () => {
handle.classList.remove('pane-handle--active'); handle.classList.remove('pane-handle--active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Persist sizes
_persistSizes(surfaceId, splitEl); _persistSizes(surfaceId, splitEl);
}; },
});
handle.addEventListener('mousedown', onMouseDown);
// Double-click resets to default // Double-click resets to default
handle.addEventListener('dblclick', () => { handle.addEventListener('dblclick', () => {
if (leftEl) { _resetPaneSize(lastLeftEl);
if (leftEl.dataset.paneFlex === '1') { _resetPaneSize(lastRightEl);
leftEl.style.flex = '1'; leftEl.style.flexBasis = ''; leftEl.style.flexGrow = ''; leftEl.style.flexShrink = '';
} else {
leftEl.style.flexBasis = ''; leftEl.style.flexGrow = ''; leftEl.style.flexShrink = '';
}
}
if (rightEl) {
if (rightEl.dataset.paneFlex === '1') {
rightEl.style.flex = '1'; rightEl.style.flexBasis = ''; rightEl.style.flexGrow = ''; rightEl.style.flexShrink = '';
} else {
rightEl.style.flexBasis = ''; rightEl.style.flexGrow = ''; rightEl.style.flexShrink = '';
}
}
_persistSizes(surfaceId, splitEl); _persistSizes(surfaceId, splitEl);
}); });
return { handle, splitEl, index, direction }; return { handle, splitEl, index, direction };
} }
/** Reset a pane element to its default flex sizing. */
function _resetPaneSize(el) {
if (!el) return;
if (el.dataset.paneFlex === '1') {
el.style.flex = '1';
}
el.style.flexBasis = '';
el.style.flexGrow = '';
el.style.flexShrink = '';
}
// ── Persistence ───────────────────────────── // ── Persistence ─────────────────────────────
function _persistSizes(surfaceId, splitEl) { function _persistSizes(surfaceId, splitEl) {

View File

@@ -302,46 +302,37 @@ function toggleSidePanelFullscreen() {
// ── Workspace Resize Handle ──────────────── // ── Workspace Resize Handle ────────────────
function _initWorkspaceResize() { function _initWorkspaceResize() {
let startX, startW;
const secondary = document.getElementById('workspaceSecondary'); const secondary = document.getElementById('workspaceSecondary');
const handle = document.getElementById('workspaceHandle'); const handle = document.getElementById('workspaceHandle');
if (!handle || !secondary) return; if (!handle || !secondary) return;
const onDown = (e) => { initDragResize(handle, {
if (!handle.classList.contains('active')) return; direction: 'horizontal',
if (secondary.classList.contains('fullscreen')) return; canDrag() {
e.preventDefault(); return handle.classList.contains('active') &&
const clientX = e.clientX ?? e.touches?.[0]?.clientX; !secondary.classList.contains('fullscreen');
startX = clientX; },
startW = secondary.getBoundingClientRect().width; onStart(_clientPos) {
// Kill transition first, force reflow, then measure.
// This ensures startW reflects the final CSS value (not mid-animation),
// and the first onMove won't animate since transition is already dead.
// Do NOT set inline width here — getBoundingClientRect includes border
// but style.width is content-box, so it causes a 1px jump on click.
secondary.style.transition = 'none'; secondary.style.transition = 'none';
document.body.style.cursor = 'col-resize'; void secondary.offsetWidth;
document.body.style.userSelect = 'none'; const startW = secondary.getBoundingClientRect().width;
return { startW };
const onMove = (e) => { },
const cx = e.clientX ?? e.touches?.[0]?.clientX ?? startX; onMove(delta, ctx) {
const delta = startX - cx; // Handle is left of secondary — dragging left (negative delta) grows it
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, startW + delta)); const newW = Math.max(280, Math.min(window.innerWidth * 0.7, ctx.startW - delta));
secondary.style.width = newW + 'px'; secondary.style.width = newW + 'px';
secondary.style.minWidth = newW + 'px'; secondary.style.minWidth = newW + 'px';
}; },
const onUp = () => { onEnd(_ctx) {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
secondary.style.transition = ''; secondary.style.transition = '';
}; },
document.addEventListener('mousemove', onMove); });
document.addEventListener('mouseup', onUp);
document.addEventListener('touchmove', onMove, { passive: false });
document.addEventListener('touchend', onUp);
};
handle.addEventListener('mousedown', onDown);
handle.addEventListener('touchstart', onDown, { passive: false });
} }
// ── Mobile Swipe Navigation ───────────────── // ── Mobile Swipe Navigation ─────────────────

View File

@@ -153,6 +153,11 @@ async function deleteProject(projectId) {
App.activeProjectId = null; App.activeProjectId = null;
localStorage.removeItem('cs-active-project'); localStorage.removeItem('cs-active-project');
} }
// Close project panel if the deleted project was open in it
if (_projectPanelId === projectId && typeof PanelRegistry !== 'undefined') {
_projectPanelId = null;
PanelRegistry.close('project');
}
UI.renderChatList(); UI.renderChatList();
UI.toast('Project deleted', 'success'); UI.toast('Project deleted', 'success');
} catch (e) { } catch (e) {