Unify thread rename editing behavior

This commit is contained in:
murashit 2026-05-26 22:36:21 +09:00
parent 375ab9e2b3
commit b3cb68ffbc
7 changed files with 42 additions and 72 deletions

View file

@ -53,6 +53,10 @@ export class ThreadRenameController {
};
}
isEditing(): boolean {
return this.renameThreadId !== null;
}
start(threadId: string): void {
const thread = this.thread(threadId);
if (!thread) return;

View file

@ -391,6 +391,9 @@ function renderThreadRenameRow(parent: HTMLElement, thread: ToolbarThreadRow, ac
actions.cancelRenameThread(thread.threadId);
}
};
input.onblur = () => {
if (!(thread.rename?.generating ?? false)) actions.saveRenameThread(thread.threadId, input.value);
};
},
});
input.win.setTimeout(() => {
@ -400,24 +403,13 @@ function renderThreadRenameRow(parent: HTMLElement, thread: ToolbarThreadRow, ac
}
}, 0);
const save = createToolbarButton(parent, "check", "Save thread name");
save.addClass("codex-panel__thread-action");
save.disabled = thread.rename?.generating ?? false;
save.onclick = (event) => {
event.stopPropagation();
actions.saveRenameThread(thread.threadId, input.value);
};
const cancel = createToolbarButton(parent, "x", "Cancel rename");
cancel.addClass("codex-panel__thread-action");
cancel.onclick = (event) => {
event.stopPropagation();
actions.cancelRenameThread(thread.threadId);
};
const autoName = createToolbarButton(parent, thread.rename?.generating ? "loader" : "sparkles", "Auto-name thread");
autoName.addClass("codex-panel__thread-action");
autoName.disabled = thread.rename?.generating ?? false;
autoName.onpointerdown = (event) => {
event.preventDefault();
event.stopPropagation();
};
autoName.onclick = (event) => {
event.stopPropagation();
actions.autoNameThread(thread.threadId);

View file

@ -1390,6 +1390,8 @@ export class CodexChatView extends ItemView {
if (insideToolbarPanel && this.containerEl.contains(insideToolbarPanel)) return;
}
if (this.threadRename.isEditing()) return;
this.closeToolbarPanel();
}

View file

@ -112,18 +112,12 @@ function renderRenameRow(parent: HTMLElement, row: ThreadsRowModel, actions: Thr
actions.cancelRename(row.thread.id);
}
};
input.onblur = () => {
if (!row.rename.generating) actions.saveRename(row.thread.id, input.value);
};
const actionsGroup = parent.createDiv({ cls: "codex-panel-threads__actions codex-panel-threads__rename-actions" });
const save = iconButton(actionsGroup, "check", "Save thread name", "codex-panel-threads__row-button");
save.disabled = row.rename.generating;
save.onclick = () => {
actions.saveRename(row.thread.id, input.value);
};
const cancel = iconButton(actionsGroup, "x", "Cancel rename", "codex-panel-threads__row-button");
cancel.onclick = () => {
actions.cancelRename(row.thread.id);
};
const autoName = iconButton(
actionsGroup,
row.rename.generating ? "loader" : "sparkles",
@ -131,7 +125,12 @@ function renderRenameRow(parent: HTMLElement, row: ThreadsRowModel, actions: Thr
"codex-panel-threads__row-button",
);
autoName.disabled = row.rename.generating;
autoName.onclick = () => {
autoName.onpointerdown = (event) => {
event.preventDefault();
event.stopPropagation();
};
autoName.onclick = (event) => {
event.stopPropagation();
actions.autoNameThread(row.thread.id);
};
input.win.setTimeout(() => {

View file

@ -274,38 +274,22 @@
box-sizing: border-box;
min-width: 0;
width: 100%;
height: calc(var(--codex-panel-size-icon-s) + var(--codex-panel-panel-gap) * 2);
padding-right: calc(
var(--codex-panel-size-icon-s) * 3 + var(--codex-panel-panel-gap) * 6 + var(--codex-panel-toolbar-button-gap) * 2 +
var(--codex-panel-item-gap)
);
min-height: calc(var(--codex-panel-size-icon-s) + var(--codex-panel-panel-gap) * 2);
padding-right: calc(var(--codex-panel-size-icon-s) + var(--codex-panel-panel-gap) * 2 + var(--codex-panel-item-gap));
}
.codex-panel-threads__rename-field {
position: relative;
display: flex;
flex: 1 1 auto;
align-items: center;
min-width: 0;
min-height: calc(var(--codex-panel-size-icon-s) + var(--codex-panel-panel-gap) * 2);
height: calc(var(--codex-panel-size-icon-s) + var(--codex-panel-panel-gap) * 2);
margin-left: calc(var(--codex-panel-item-gap) * -1);
margin-right: var(--codex-panel-item-gap);
padding: 0 var(--codex-panel-item-gap);
overflow: visible;
font-size: var(--font-ui-small);
font-weight: var(--nav-item-weight, var(--font-normal));
line-height: var(--line-height-tight);
}
.codex-panel-threads__rename-field::before {
position: absolute;
inset: calc(var(--codex-panel-control-gap) * -1) 0;
border-radius: var(--nav-item-radius, var(--codex-panel-card-radius));
background: var(--background-modifier-hover);
content: "";
}
.codex-panel-threads__rename-actions {
opacity: 1;
}
@ -334,9 +318,12 @@
}
.codex-panel-threads__rename-input.codex-panel-threads__rename-input:focus,
.codex-panel-threads__rename-input.codex-panel-threads__rename-input:focus-visible {
.codex-panel-threads__rename-input.codex-panel-threads__rename-input:focus-visible,
.codex-panel-threads__rename-input.codex-panel-threads__rename-input:hover,
.codex-panel-threads__rename-input.codex-panel-threads__rename-input:active {
border: 0;
box-shadow: none;
background: transparent;
outline: none;
}
@ -1162,7 +1149,7 @@
}
.codex-panel__thread-row--renaming {
grid-template-columns: minmax(0, 1fr) auto auto auto;
grid-template-columns: minmax(0, 1fr) auto;
}
.codex-panel__thread:hover:not(:where(:disabled, .is-disabled)),
@ -1176,26 +1163,12 @@
}
.codex-panel__thread-rename {
position: relative;
box-sizing: border-box;
min-height: var(--codex-panel-size-nav-item);
padding-block: var(--size-4-1);
cursor: text;
}
.codex-panel__thread-rename::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: calc(var(--codex-panel-size-icon-s) + var(--size-4-2));
border-radius: var(--radius-s);
background: var(--background-modifier-hover);
content: "";
}
.codex-panel__thread-rename-field {
position: relative;
display: flex;
flex: 1 1 auto;
align-self: stretch;
@ -1203,9 +1176,6 @@
min-width: 0;
min-height: 0;
height: auto;
margin-left: calc(var(--codex-panel-item-gap) * -1);
margin-right: 0;
padding: 0 var(--codex-panel-item-gap);
overflow: visible;
font-size: var(--codex-panel-toolbar-text-size);
line-height: var(--codex-panel-toolbar-line-height);

View file

@ -1871,10 +1871,12 @@ describe("toolbar renderer decisions", () => {
input.dispatchEvent(new Event("input", { bubbles: true }));
expect(updateRenameDraft).toHaveBeenCalledWith("editing", "New title");
parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')?.click();
input.dispatchEvent(new FocusEvent("blur"));
expect(saveRenameThread).toHaveBeenCalledWith("editing", "New title");
parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')?.click();
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
expect(cancelRenameThread).toHaveBeenCalledWith("editing");
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.click();
expect(autoNameThread).toHaveBeenCalledWith("editing");
});
@ -1902,9 +1904,9 @@ describe("toolbar renderer decisions", () => {
);
expect(parent.querySelector<HTMLInputElement>(".codex-panel__thread-rename-input")?.disabled).toBe(false);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')?.disabled).toBe(true);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.disabled).toBe(true);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')?.disabled).toBe(false);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
});
});
@ -1978,7 +1980,7 @@ describe("threads view renderer decisions", () => {
const input = expectPresent(parent.querySelector<HTMLInputElement>(".codex-panel-threads__rename-input"));
input.value = "New name";
input.dispatchEvent(new Event("input"));
parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')?.click();
input.dispatchEvent(new FocusEvent("blur"));
expect(actions.updateRename).toHaveBeenCalledWith("thread", "New name");
expect(actions.saveRename).toHaveBeenCalledWith("thread", "New name");
@ -1998,8 +2000,9 @@ describe("threads view renderer decisions", () => {
expect(parent.querySelector<HTMLElement>(".codex-panel-threads__rename-form")).toBeTruthy();
const actionsGroup = expectPresent(parent.querySelector<HTMLElement>(".codex-panel-threads__rename-actions"));
expect(actionsGroup.querySelectorAll(".codex-panel-threads__row-button")).toHaveLength(3);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')?.disabled).toBe(false);
expect(actionsGroup.querySelectorAll(".codex-panel-threads__row-button")).toHaveLength(1);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.click();
expect(actions.autoNameThread).toHaveBeenCalledWith("thread");
@ -2017,9 +2020,9 @@ describe("threads view renderer decisions", () => {
renderThreadsView(parent, { status: "1 thread", loading: false, rows: [row] }, threadsViewActions());
expect(parent.querySelector<HTMLInputElement>(".codex-panel-threads__rename-input")?.disabled).toBe(false);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')?.disabled).toBe(true);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.disabled).toBe(true);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')?.disabled).toBe(false);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
});
});

View file

@ -272,7 +272,7 @@ describe("CodexThreadsView", () => {
expect(input).not.toBeNull();
if (!input) return;
input.value = "Renamed thread";
view.containerEl.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')?.click();
input.dispatchEvent(new FocusEvent("blur"));
await vi.waitFor(() => {
expect(setThreadName).toHaveBeenCalledWith("thread", "Renamed thread");