mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
Merge master into the 1.41.0 release branch (faster session close, README GIFs)
This commit is contained in:
commit
209a539489
17 files changed
+492
-66
No files matched your search
+72
-20
@@ -2294,6 +2294,8 @@ class CodemanApp {
|
||||
}
|
||||
|
||||
_onSessionCreated(data) {
|
||||
// A session this tab is closing stays closed until the server answers.
|
||||
if (this._closingSessions?.has(data.id)) return;
|
||||
this.sessions.set(data.id, data);
|
||||
// Add new session to end of tab order
|
||||
if (!this.sessionOrder.includes(data.id)) {
|
||||
@@ -2318,6 +2320,9 @@ class CodemanApp {
|
||||
|
||||
_onSessionUpdated(data) {
|
||||
const session = data.session || data;
|
||||
// A session this tab is closing stays closed until the server answers
|
||||
// (closeSession() puts it back if the delete is refused).
|
||||
if (this._closingSessions?.has(session.id)) return;
|
||||
const oldSession = this.sessions.get(session.id);
|
||||
const claudeSessionIdJustSet = session.claudeSessionId && (!oldSession || !oldSession.claudeSessionId);
|
||||
this.sessions.set(session.id, session);
|
||||
@@ -6115,9 +6120,19 @@ class CodemanApp {
|
||||
// Session Tabs
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
renderSessionTabs() {
|
||||
renderSessionTabs({ immediate = false } = {}) {
|
||||
// Don't re-render while user is typing in the inline rename input
|
||||
if (this._inlineRenameActive) return;
|
||||
if (immediate) {
|
||||
// For a change the user just made and is watching for (closing a tab). The
|
||||
// debounce restarts on every session update, so with busy sessions around a
|
||||
// debounced pass can lag well past its 100ms. A pass still pending would only
|
||||
// repeat this one, so it is dropped.
|
||||
clearTimeout(this._debounceTimers.sessionTabs);
|
||||
this._debounceTimers.sessionTabs = null;
|
||||
this._renderSessionTabsImmediate();
|
||||
return;
|
||||
}
|
||||
this._debouncedCall('sessionTabs', this._renderSessionTabsImmediate);
|
||||
}
|
||||
|
||||
@@ -9824,27 +9839,32 @@ class CodemanApp {
|
||||
}
|
||||
|
||||
async closeSession(sessionId, killMux = true) {
|
||||
// ⚠️ Captured BEFORE the await, and the delete is announced to
|
||||
// _onSessionDeleted through _closingSessions. The `session_deleted` SSE
|
||||
// broadcast for THIS delete routinely lands while the request is still in
|
||||
// flight, and that handler nulls activeSessionId and shows the welcome
|
||||
// screen. Re-reading the field after the await therefore made the fallback
|
||||
// below a coin flip: closing the tab you were on either moved you to the
|
||||
// next session or dumped you on the home screen, depending on which path
|
||||
// won the race (both outcomes measured on one build, 2026-08-17).
|
||||
// Already on its way out (a repeated click, the mux panel racing the tab).
|
||||
if (this._closingSessions.has(sessionId)) return;
|
||||
// The tab goes FIRST and the server is asked after. The kill takes the server
|
||||
// a few hundred ms (SIGTERM grace, the process tree, tmux), and a tab that sat
|
||||
// there that long after "Kill" read as a click that did nothing. A refused
|
||||
// delete puts the row back below.
|
||||
//
|
||||
// ⚠️ Everything is read BEFORE the first await, and the delete is announced
|
||||
// to _onSessionDeleted through _closingSessions: the `session_deleted` SSE
|
||||
// broadcast for THIS delete arrives while the request is still in flight, and
|
||||
// that handler must leave the follow-up selection to this method (both
|
||||
// outcomes of that race were measured on one build, 2026-08-17).
|
||||
const session = this.sessions.get(sessionId);
|
||||
const orderIndex = this.sessionOrder.indexOf(sessionId);
|
||||
const wasActive = this.activeSessionId === sessionId;
|
||||
// Tile grid open: the fallback is the NEIGHBOURING TILE, never the first
|
||||
// sessionOrder entry (often not tiled, which would collapse the grid).
|
||||
// Captured here for the same reason as wasActive: the SSE delete can remove
|
||||
// the tile while the request is still in flight.
|
||||
const grid = this._tileGrid;
|
||||
const tileNeighborId = grid?.has(sessionId) ? window.CodemanTileGrid.tileNeighbor(grid.ids, sessionId) : null;
|
||||
this._closingSessions.add(sessionId);
|
||||
let res = null;
|
||||
try {
|
||||
await this._apiDelete(`/api/sessions/${sessionId}?killMux=${killMux}`);
|
||||
this._cleanupSessionData(sessionId);
|
||||
// The last tile leaving closes the grid (no reselect): the pick below runs.
|
||||
if (grid?.has(sessionId)) this.removeTile(sessionId, { refocus: false });
|
||||
// The same teardown the SSE event runs (split pane, tile, detached window,
|
||||
// WebSocket, per-session state), only now instead of when the server is
|
||||
// done. It is idempotent, so the real event finds nothing left to do.
|
||||
this._onSessionDeleted({ id: sessionId });
|
||||
|
||||
if (wasActive && grid?.open) {
|
||||
// `auto`: the app chose this tile because the previous one went away.
|
||||
@@ -9869,18 +9889,50 @@ class CodemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
this.renderSessionTabs();
|
||||
this.renderSessionTabs({ immediate: true });
|
||||
|
||||
res = await this._apiDelete(`/api/sessions/${sessionId}?killMux=${killMux}`);
|
||||
} catch (err) {
|
||||
// `res` stays null: handled below like a delete that got no answer.
|
||||
console.warn('[closeSession] close failed:', err);
|
||||
} finally {
|
||||
this._closingSessions.delete(sessionId);
|
||||
}
|
||||
|
||||
// 404: already gone (closed from another tab or device), which is what was asked.
|
||||
let gone = !!res && (res.ok || res.status === 404);
|
||||
if (!gone) {
|
||||
// Refused, or no answer. Ask rather than guess: a delete can land on the
|
||||
// server and still lose its reply, and its session_deleted event has then
|
||||
// already been spent while the request was in flight.
|
||||
const check = await this._api(`/api/sessions/${sessionId}`);
|
||||
gone = check?.status === 404;
|
||||
}
|
||||
|
||||
if (gone) {
|
||||
// An SSE resync (handleInit) that landed mid-request rebuilds the list from
|
||||
// the server, which still had the session then.
|
||||
if (this.sessions.has(sessionId)) this._onSessionDeleted({ id: sessionId });
|
||||
if (killMux) {
|
||||
this.showToast('Session closed and tmux killed', 'success');
|
||||
} else {
|
||||
this.showToast('Tab hidden, tmux still running', 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
this.showToast('Failed to close session', 'error');
|
||||
} finally {
|
||||
this._closingSessions.delete(sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Still on the server (or the server is unreachable, in which case the resync
|
||||
// on reconnect has the last word): its row comes back where it was.
|
||||
if (session && !this.sessions.has(sessionId)) {
|
||||
this.sessions.set(sessionId, session);
|
||||
if (!this.sessionOrder.includes(sessionId)) {
|
||||
const at = orderIndex === -1 ? this.sessionOrder.length : Math.min(orderIndex, this.sessionOrder.length);
|
||||
this.sessionOrder.splice(at, 0, sessionId);
|
||||
this.saveSessionOrder();
|
||||
}
|
||||
this.renderSessionTabs();
|
||||
}
|
||||
this.showToast('Failed to close session', 'error');
|
||||
}
|
||||
|
||||
// Request confirmation before closing a session
|
||||
|
||||
Reference in new issue
Block a user