Changeset 0.25.2 (#162)
This commit is contained in:
@@ -101,6 +101,7 @@
|
||||
<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/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>
|
||||
|
||||
{{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
{{/* Top Bar */}}
|
||||
<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>
|
||||
Back to Chat
|
||||
Back
|
||||
</a>
|
||||
<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>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
{{/* Top Bar */}}
|
||||
<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>
|
||||
Back to Chat
|
||||
Back
|
||||
</a>
|
||||
<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>
|
||||
|
||||
@@ -171,16 +171,13 @@
|
||||
|
||||
.msg-inner { max-width: 768px; margin: 0 auto; display: flex; gap: 10px; }
|
||||
|
||||
/* User messages: right-aligned bubble */
|
||||
.message.user { display: flex; justify-content: flex-end; }
|
||||
/* User messages: same fixed-width container, mirrored */
|
||||
.message.user .msg-inner { flex-direction: row-reverse; }
|
||||
.message.user .msg-body {
|
||||
background: var(--user-bubble);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
padding: 10px 14px;
|
||||
flex: initial;
|
||||
max-width: 85%;
|
||||
}
|
||||
.message.user .msg-head { flex-direction: row-reverse; }
|
||||
.message.user .msg-role { color: var(--accent); }
|
||||
|
||||
@@ -112,14 +112,6 @@
|
||||
.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 .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; }
|
||||
|
||||
/* Split button (New Chat + dropdown) */
|
||||
|
||||
@@ -157,6 +157,11 @@
|
||||
}
|
||||
|
||||
.sidebar.collapsed .user-menu-wrap .user-flyout {
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
position: fixed;
|
||||
left: var(--sidebar-rail);
|
||||
bottom: 8px;
|
||||
top: auto;
|
||||
right: auto;
|
||||
width: 200px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -135,10 +135,12 @@ function wikilinkAutocomplete(linkCompleter) {
|
||||
options: results.map(r => ({
|
||||
label: r.label || r.title,
|
||||
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 + ']]';
|
||||
view.dispatch({
|
||||
changes: { from, to, insert },
|
||||
changes: { from, to: endPos, insert },
|
||||
selection: { anchor: from + insert.length },
|
||||
});
|
||||
},
|
||||
|
||||
94
src/js/drag-resize.js
Normal file
94
src/js/drag-resize.js
Normal 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;
|
||||
}
|
||||
@@ -19,8 +19,6 @@ async function openNotes() {
|
||||
}
|
||||
PanelRegistry.open('notes');
|
||||
_exitSelectMode();
|
||||
await loadNotesList();
|
||||
await loadNoteFolders();
|
||||
}
|
||||
|
||||
async function loadNotesList(folder, searchQuery) {
|
||||
@@ -603,6 +601,10 @@ async function saveMessageToNote(msgIndex) {
|
||||
// Check for text selection within the message element
|
||||
const sel = window.getSelection();
|
||||
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 msgEl = msgId ? document.querySelector(`.message[data-msg-id="${msgId}"] .msg-text`) : null;
|
||||
if (sel && sel.rangeCount > 0 && msgEl?.contains(sel.anchorNode)) {
|
||||
@@ -820,7 +822,18 @@ function _registerNotesPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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);
|
||||
|
||||
// ── Wire listeners ──
|
||||
@@ -833,6 +846,9 @@ function _registerNotesPanel() {
|
||||
if (typeof openNoteGraph === 'function') openNoteGraph();
|
||||
});
|
||||
el.querySelector('#notesTodayBtn').addEventListener('click', openDailyNote);
|
||||
el.querySelector('#notesGraphBackBtn').addEventListener('click', () => {
|
||||
if (typeof closeNoteGraph === 'function') closeNoteGraph();
|
||||
});
|
||||
el.querySelector('#notesSelectModeBtn').addEventListener('click', () => {
|
||||
if (_notesSelectMode) _exitSelectMode();
|
||||
else _enterSelectMode();
|
||||
@@ -890,7 +906,9 @@ function _registerNotesPanel() {
|
||||
element: el,
|
||||
label: 'Notes',
|
||||
onOpen() {
|
||||
// Loading is handled by openNotes() which calls loadNotesList
|
||||
// Ensure notes are loaded whenever the panel opens
|
||||
loadNotesList();
|
||||
loadNoteFolders();
|
||||
},
|
||||
onClose() {
|
||||
_destroyNoteEditor();
|
||||
|
||||
@@ -345,26 +345,28 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
|
||||
splitEl.appendChild(handle);
|
||||
}
|
||||
|
||||
// Drag logic
|
||||
let startX, startY, leftEl, rightEl, startLeftBasis, startRightBasis;
|
||||
// Track adjacent panes for dblclick reset
|
||||
let lastLeftEl = null, lastRightEl = null;
|
||||
|
||||
const onMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
const allPanes = Array.from(splitEl.querySelectorAll(':scope > .pane, :scope > .pane-split'));
|
||||
initDragResize(handle, {
|
||||
direction,
|
||||
onStart(_clientPos) {
|
||||
// Find the two panes adjacent to this handle
|
||||
const handleIdx = Array.from(splitEl.children).indexOf(handle);
|
||||
leftEl = splitEl.children[handleIdx - 1];
|
||||
rightEl = splitEl.children[handleIdx + 1];
|
||||
if (!leftEl || !rightEl) return;
|
||||
const leftEl = splitEl.children[handleIdx - 1];
|
||||
const rightEl = splitEl.children[handleIdx + 1];
|
||||
if (!leftEl || !rightEl) return false;
|
||||
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
// Use current rendered size (matches what flex layout computed)
|
||||
startLeftBasis = leftEl.getBoundingClientRect().width;
|
||||
startRightBasis = rightEl.getBoundingClientRect().width;
|
||||
lastLeftEl = leftEl;
|
||||
lastRightEl = rightEl;
|
||||
|
||||
// Immediately lock the non-flex panes to their current sizes
|
||||
// so the first move delta doesn't cause a visual snap
|
||||
const isHoriz = direction === 'horizontal';
|
||||
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') {
|
||||
leftEl.style.flexBasis = startLeftBasis + 'px';
|
||||
leftEl.style.flexGrow = '0';
|
||||
@@ -377,37 +379,28 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
|
||||
}
|
||||
|
||||
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 rightIsFlex = rightEl.dataset.paneFlex === '1';
|
||||
// Cap: no single fixed pane should exceed 60% of container
|
||||
const maxSize = splitEl.getBoundingClientRect().width * 0.6;
|
||||
const isHoriz = direction === 'horizontal';
|
||||
const dim = isHoriz ? 'width' : 'height';
|
||||
const maxSize = splitEl.getBoundingClientRect()[dim] * 0.6;
|
||||
|
||||
if (leftIsFlex) {
|
||||
// Only adjust the right (fixed) pane; left flex absorbs remainder
|
||||
const newRight = Math.min(maxSize, Math.max(50, startRightBasis - delta));
|
||||
rightEl.style.flexBasis = newRight + 'px';
|
||||
rightEl.style.flexGrow = '0';
|
||||
rightEl.style.flexShrink = '0';
|
||||
} else if (rightIsFlex) {
|
||||
// Only adjust the left (fixed) pane; right flex absorbs remainder
|
||||
const newLeft = Math.min(maxSize, Math.max(50, startLeftBasis + delta));
|
||||
leftEl.style.flexBasis = newLeft + 'px';
|
||||
leftEl.style.flexGrow = '0';
|
||||
leftEl.style.flexShrink = '0';
|
||||
} else {
|
||||
// Both fixed — adjust both (original behavior)
|
||||
const newLeft = Math.max(50, startLeftBasis + delta);
|
||||
const newRight = Math.max(50, startRightBasis - delta);
|
||||
leftEl.style.flexBasis = newLeft + 'px';
|
||||
@@ -417,43 +410,34 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
|
||||
rightEl.style.flexGrow = '0';
|
||||
rightEl.style.flexShrink = '0';
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
},
|
||||
onEnd(_ctx) {
|
||||
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);
|
||||
};
|
||||
|
||||
handle.addEventListener('mousedown', onMouseDown);
|
||||
},
|
||||
});
|
||||
|
||||
// Double-click resets to default
|
||||
handle.addEventListener('dblclick', () => {
|
||||
if (leftEl) {
|
||||
if (leftEl.dataset.paneFlex === '1') {
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
_resetPaneSize(lastLeftEl);
|
||||
_resetPaneSize(lastRightEl);
|
||||
_persistSizes(surfaceId, splitEl);
|
||||
});
|
||||
|
||||
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 ─────────────────────────────
|
||||
|
||||
function _persistSizes(surfaceId, splitEl) {
|
||||
|
||||
@@ -302,46 +302,37 @@ function toggleSidePanelFullscreen() {
|
||||
// ── Workspace Resize Handle ────────────────
|
||||
|
||||
function _initWorkspaceResize() {
|
||||
let startX, startW;
|
||||
const secondary = document.getElementById('workspaceSecondary');
|
||||
const handle = document.getElementById('workspaceHandle');
|
||||
if (!handle || !secondary) return;
|
||||
|
||||
const onDown = (e) => {
|
||||
if (!handle.classList.contains('active')) return;
|
||||
if (secondary.classList.contains('fullscreen')) return;
|
||||
e.preventDefault();
|
||||
const clientX = e.clientX ?? e.touches?.[0]?.clientX;
|
||||
startX = clientX;
|
||||
startW = secondary.getBoundingClientRect().width;
|
||||
initDragResize(handle, {
|
||||
direction: 'horizontal',
|
||||
canDrag() {
|
||||
return handle.classList.contains('active') &&
|
||||
!secondary.classList.contains('fullscreen');
|
||||
},
|
||||
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';
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
|
||||
const onMove = (e) => {
|
||||
const cx = e.clientX ?? e.touches?.[0]?.clientX ?? startX;
|
||||
const delta = startX - cx;
|
||||
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, startW + delta));
|
||||
void secondary.offsetWidth;
|
||||
const startW = secondary.getBoundingClientRect().width;
|
||||
return { startW };
|
||||
},
|
||||
onMove(delta, ctx) {
|
||||
// Handle is left of secondary — dragging left (negative delta) grows it
|
||||
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, ctx.startW - delta));
|
||||
secondary.style.width = newW + 'px';
|
||||
secondary.style.minWidth = newW + 'px';
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
},
|
||||
onEnd(_ctx) {
|
||||
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 ─────────────────
|
||||
|
||||
@@ -153,6 +153,11 @@ async function deleteProject(projectId) {
|
||||
App.activeProjectId = null;
|
||||
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.toast('Project deleted', 'success');
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user