mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
fix(tabs): inline rename review fixes
Reopening the editor over a rename still in flight filled it from the name the server had not replaced yet, so dismissing it (blur commits) queued the old name behind the new one and undid the rename. The queue now records the newest queued name per session (_inlineRenamePending, cleared with the queue entry), and a reopened editor takes its prefix, input and "unchanged" comparison from it. An untouched confirm sends nothing more. A failed write only toasted while its editor was still current. The queue reports the failure itself now, and the editor only puts its label back. One rejected task blocked every later rename of that session until reload. Each task now chains from a settled predecessor, the local apply after a successful PUT is guarded, and the queue entry is cleaned up on either outcome. The rail and sidebar editor's 4rem floor moves from a stylesheet `!important` into the inline min-width startInlineRename already writes per layout (0 in the header strip, 4rem in the rail and sidebar). Tests: the reopened-editor case now expects only "First" to be sent; new cases cover a 500 answered after the editor is gone and a throw in updateSubagentParentNames; the long-prefix check runs in the sidebar and detailed sidebar too and asserts the inline floor; the header strip editor keeps min-width 0.
This commit is contained in:
@@ -2596,12 +2596,22 @@ Object.assign(CodemanApp.prototype, {
|
||||
* in the editor: a confirmed name is applied locally even after its editor is
|
||||
* gone, and the "already that name" check runs only once the earlier writes
|
||||
* have landed, so confirming the name still on screen is a real write.
|
||||
* Resolves { status: 'confirmed' | 'failed' | 'deleted' }; never rejects.
|
||||
* Resolves { status: 'confirmed' | 'failed' | 'deleted' }; never rejects,
|
||||
* and reports a failed write itself, since its editor may be gone by then.
|
||||
* `_inlineRenamePending` holds the newest queued name per session, so an
|
||||
* editor reopened over a write in flight starts from that name rather than
|
||||
* the one the server has not replaced yet.
|
||||
*/
|
||||
_queueInlineSessionName(sessionId, desiredName) {
|
||||
this._inlineRenameWrites ??= new Map();
|
||||
this._inlineRenamePending ??= new Map();
|
||||
const writes = this._inlineRenameWrites;
|
||||
const task = (writes.get(sessionId) || Promise.resolve()).then(async () => {
|
||||
const pending = this._inlineRenamePending;
|
||||
pending.set(sessionId, desiredName);
|
||||
// Chained from a settled promise, so one rejected write cannot stop the
|
||||
// writes queued behind it.
|
||||
const prev = (writes.get(sessionId) || Promise.resolve()).catch(() => {});
|
||||
const task = prev.then(async () => {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return { status: 'deleted' };
|
||||
if (session.name === desiredName) return { status: 'confirmed' };
|
||||
@@ -2612,15 +2622,26 @@ Object.assign(CodemanApp.prototype, {
|
||||
// A failure is a value, so a later write in the chain still runs.
|
||||
}
|
||||
if (!this.sessions.has(sessionId)) return { status: 'deleted' };
|
||||
if (confirmed === null) return { status: 'failed' };
|
||||
this._applyLocalSessionName(sessionId, confirmed);
|
||||
this.renderSessionTabs();
|
||||
if (confirmed === null) {
|
||||
this.showToast('Failed to rename', 'error');
|
||||
return { status: 'failed' };
|
||||
}
|
||||
try {
|
||||
this._applyLocalSessionName(sessionId, confirmed);
|
||||
this.renderSessionTabs();
|
||||
} catch (err) {
|
||||
// The server holds the name; a local repaint failing is not a failed write.
|
||||
console.error('[rename] applying the confirmed name failed', err);
|
||||
}
|
||||
return { status: 'confirmed' };
|
||||
});
|
||||
writes.set(sessionId, task);
|
||||
task.then(() => {
|
||||
if (writes.get(sessionId) === task) writes.delete(sessionId);
|
||||
});
|
||||
const cleanup = () => {
|
||||
if (writes.get(sessionId) !== task) return;
|
||||
writes.delete(sessionId);
|
||||
pending.delete(sessionId);
|
||||
};
|
||||
task.then(cleanup, cleanup);
|
||||
return task;
|
||||
},
|
||||
|
||||
@@ -2912,7 +2933,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
tabName.classList.add('tab-name-renaming');
|
||||
|
||||
const currentName = this.getSessionName(session);
|
||||
const parsed = parseSessionPrefix(session.name);
|
||||
// A rename still in flight is the user's last word, not the name the
|
||||
// server has yet to replace: start from it, and compare against it below.
|
||||
const shownName = this._inlineRenamePending?.get(sessionId) ?? session.name;
|
||||
const parsed = parseSessionPrefix(shownName);
|
||||
const originalContent = tabName.textContent;
|
||||
const originalChildren = [...tabName.childNodes].map((node) => node.cloneNode(true));
|
||||
const restoreOriginalChildren = () => {
|
||||
@@ -2933,13 +2957,17 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = parsed ? parsed.suffix : (session.name || '');
|
||||
input.value = parsed ? parsed.suffix : (shownName || '');
|
||||
input.placeholder = parsed ? 'Add description...' : currentName;
|
||||
input.className = 'tab-rename-input';
|
||||
// 80px is tuned for the narrow header tab; a full-width sidebar row can and
|
||||
// should give the whole line to the input.
|
||||
const renameWidth = tabName.closest('.tab-rail') ? 'auto' : this.isSessionSidebarActive?.() ? '100%' : '80px';
|
||||
input.style.cssText = `width: ${renameWidth}; min-width: 0; font-size: 0.75rem; padding: 2px 4px; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 3px; color: var(--text); outline: none;`;
|
||||
// should give the whole line to the input. The header editor may shrink to
|
||||
// nothing, while a rail or sidebar row always keeps room to type.
|
||||
const inRail = !!tabName.closest('.tab-rail');
|
||||
const inSidebar = !inRail && !!this.isSessionSidebarActive?.();
|
||||
const renameWidth = inRail ? 'auto' : inSidebar ? '100%' : '80px';
|
||||
const renameMinWidth = inRail || inSidebar ? '4rem' : '0';
|
||||
input.style.cssText = `width: ${renameWidth}; min-width: ${renameMinWidth}; font-size: 0.75rem; padding: 2px 4px; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 3px; color: var(--text); outline: none;`;
|
||||
|
||||
tabName.appendChild(input);
|
||||
input.focus();
|
||||
@@ -2989,7 +3017,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
const suffix = input.value.trim();
|
||||
const fullName = parsed ? parsed.prefix + (suffix ? ': ' + suffix : '') : suffix;
|
||||
if (fullName === session.name) restoreOriginalChildren();
|
||||
if (fullName === shownName) restoreOriginalChildren();
|
||||
else tabName.textContent = fullName || originalContent;
|
||||
|
||||
// Skip the API call if the session vanished between focus and blur. The
|
||||
@@ -2998,10 +3026,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (this.sessions.has(sessionId)) {
|
||||
const result = await this._queueInlineSessionName(sessionId, fullName);
|
||||
if (invalidated || this._activeRename !== renameHandle || !this.sessions.has(sessionId)) return;
|
||||
if (result.status === 'failed') {
|
||||
restoreOriginalChildren();
|
||||
this.showToast('Failed to rename', 'error');
|
||||
}
|
||||
// The queue reports a failure itself; the editor only puts its label back.
|
||||
if (result.status === 'failed') restoreOriginalChildren();
|
||||
}
|
||||
// Re-render tabs to restore full tab structure
|
||||
completeCurrentRename();
|
||||
|
||||
@@ -1867,15 +1867,14 @@ html[data-tab-orientation='vertical'] .tab-rail .session-tab .tab-name-prefix {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* The input always keeps room to type. `!important` beats the editor's inline
|
||||
`min-width: 0`, which the header strip's fixed-width editor relies on. */
|
||||
/* The input takes the rest of the row. Its 4rem floor is the editor's own
|
||||
inline min-width (startInlineRename picks it per layout). */
|
||||
:is(
|
||||
html[data-tab-orientation='vertical'] .tab-rail,
|
||||
html[data-session-list='sidebar'] .session-sidebar
|
||||
) .tab-name-renaming .tab-rename-input {
|
||||
flex: 1 1 0;
|
||||
width: auto;
|
||||
min-width: 4rem !important;
|
||||
}
|
||||
|
||||
/* A compact rail row has no room for both the editor and its adornments, so
|
||||
|
||||
Reference in New Issue
Block a user