mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add two-step archive actions in thread lists
This commit is contained in:
parent
b3cb68ffbc
commit
81cf4b79d5
12 changed files with 335 additions and 53 deletions
|
|
@ -22,7 +22,7 @@ export interface SlashCommandExecutionContext {
|
|||
forkThread: (threadId: string) => Promise<void>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
compactThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string, saveMarkdown?: boolean) => Promise<void>;
|
||||
toggleFastMode: () => void | Promise<void>;
|
||||
toggleCollaborationMode: () => void | Promise<void>;
|
||||
toggleAutoReview: () => void | Promise<void>;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export interface ChatThreadActionControllerHost {
|
|||
export class ChatThreadActionController {
|
||||
constructor(private readonly host: ChatThreadActionControllerHost) {}
|
||||
|
||||
async archiveThread(threadId: string): Promise<void> {
|
||||
async archiveThread(threadId: string, saveMarkdown = this.host.settings().archiveExportEnabled): Promise<void> {
|
||||
if (this.host.state.busy) {
|
||||
this.host.addSystemMessage("Finish or interrupt the current turn before archiving threads.");
|
||||
return;
|
||||
|
|
@ -40,7 +40,7 @@ export class ChatThreadActionController {
|
|||
if (!client) return;
|
||||
try {
|
||||
const settings = this.host.settings();
|
||||
if (settings.archiveExportEnabled) {
|
||||
if (saveMarkdown) {
|
||||
const response = await client.readThread(threadId, true);
|
||||
const result = await exportArchivedThreadMarkdown(response.thread, settings, this.host.archiveAdapter());
|
||||
new Notice(`Saved archived thread to ${result.path}.`);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export interface ToolbarThreadRow {
|
|||
selected: boolean;
|
||||
disabled: boolean;
|
||||
canArchive: boolean;
|
||||
archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean };
|
||||
rename: {
|
||||
draft: string;
|
||||
generating: boolean;
|
||||
|
|
@ -75,7 +76,8 @@ export interface ToolbarActions {
|
|||
refreshDiagnostics: () => void;
|
||||
refreshThreads: () => void;
|
||||
resumeThread: (threadId: string) => void;
|
||||
archiveThread: (threadId: string) => void;
|
||||
startArchiveThread: (threadId: string) => void;
|
||||
archiveThread: (threadId: string, saveMarkdown: boolean) => void;
|
||||
startRenameThread: (threadId: string) => void;
|
||||
updateRenameDraft: (threadId: string, value: string) => void;
|
||||
saveRenameThread: (threadId: string, value: string) => void;
|
||||
|
|
@ -107,7 +109,7 @@ export function toolbarSignature(model: ToolbarViewModel): string {
|
|||
(thread) =>
|
||||
`${thread.threadId}:${thread.title}:${String(thread.selected)}:${String(thread.disabled)}:${String(thread.canArchive)}:${thread.rename?.draft ?? ""}:${String(
|
||||
thread.rename?.generating ?? false,
|
||||
)}`,
|
||||
)}:${String(thread.archiveConfirm?.active ?? false)}:${String(thread.archiveConfirm?.defaultSaveMarkdown ?? false)}`,
|
||||
),
|
||||
modelChoices: model.modelChoices.map(
|
||||
(choice) => `${choice.label}:${String(choice.selected)}:${String(choice.disabled)}:${choice.meta ?? ""}`,
|
||||
|
|
@ -326,7 +328,13 @@ function renderThreadList(parent: HTMLElement, threads: ToolbarThreadRow[], acti
|
|||
|
||||
for (const thread of threads) {
|
||||
const row = threadsEl.createDiv({
|
||||
cls: ["codex-panel__thread-row", thread.rename ? "codex-panel__thread-row--renaming" : ""].filter(Boolean).join(" "),
|
||||
cls: [
|
||||
"codex-panel__thread-row",
|
||||
thread.rename ? "codex-panel__thread-row--renaming" : "",
|
||||
archiveConfirmState(thread).active ? "codex-panel__thread-row--archive-confirming" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
});
|
||||
if (thread.rename) {
|
||||
renderThreadRenameRow(row, thread, actions);
|
||||
|
|
@ -342,26 +350,61 @@ function renderThreadList(parent: HTMLElement, threads: ToolbarThreadRow[], acti
|
|||
},
|
||||
});
|
||||
|
||||
const rename = createToolbarButton(row, "pencil", "Rename thread");
|
||||
rename.addClass("codex-panel__thread-action");
|
||||
rename.disabled = thread.disabled;
|
||||
rename.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.startRenameThread(thread.threadId);
|
||||
};
|
||||
|
||||
if (thread.canArchive) {
|
||||
const action = createToolbarButton(row, "archive", "Archive thread");
|
||||
action.addClass("codex-panel__thread-action");
|
||||
action.disabled = thread.disabled;
|
||||
action.onclick = (event) => {
|
||||
if (!archiveConfirmState(thread).active) {
|
||||
const rename = createToolbarButton(row, "pencil", "Rename thread");
|
||||
rename.addClass("codex-panel__thread-action");
|
||||
rename.disabled = thread.disabled;
|
||||
rename.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.archiveThread(thread.threadId);
|
||||
actions.startRenameThread(thread.threadId);
|
||||
};
|
||||
}
|
||||
|
||||
if (thread.canArchive) renderArchiveActions(row, thread, actions);
|
||||
}
|
||||
}
|
||||
|
||||
function renderArchiveActions(parent: HTMLElement, thread: ToolbarThreadRow, actions: ToolbarActions): void {
|
||||
const archiveConfirm = archiveConfirmState(thread);
|
||||
if (!archiveConfirm.active) {
|
||||
const action = createToolbarButton(parent, "archive", "Archive thread");
|
||||
action.addClass("codex-panel__thread-action");
|
||||
action.disabled = thread.disabled;
|
||||
action.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.startArchiveThread(thread.threadId);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
parent.addClass("codex-panel__archive-confirm");
|
||||
const defaultSaveMarkdown = archiveConfirm.defaultSaveMarkdown;
|
||||
const alternate = archiveModeButton(parent, !defaultSaveMarkdown, false);
|
||||
alternate.disabled = thread.disabled;
|
||||
alternate.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.archiveThread(thread.threadId, !defaultSaveMarkdown);
|
||||
};
|
||||
const primary = archiveModeButton(parent, defaultSaveMarkdown, true);
|
||||
primary.disabled = thread.disabled;
|
||||
primary.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.archiveThread(thread.threadId, defaultSaveMarkdown);
|
||||
};
|
||||
}
|
||||
|
||||
function archiveConfirmState(thread: ToolbarThreadRow): { active: boolean; defaultSaveMarkdown: boolean } {
|
||||
return thread.archiveConfirm ?? { active: false, defaultSaveMarkdown: false };
|
||||
}
|
||||
|
||||
function archiveModeButton(parent: HTMLElement, saveMarkdown: boolean, primary: boolean): HTMLButtonElement {
|
||||
const label = saveMarkdown ? "Save and archive thread" : "Archive thread without saving";
|
||||
const button = createToolbarButton(parent, saveMarkdown ? "save" : "trash", label);
|
||||
button.addClass("codex-panel__thread-action");
|
||||
button.addClass(primary ? "codex-panel__archive-default" : "codex-panel__archive-alternate");
|
||||
return button;
|
||||
}
|
||||
|
||||
function renderThreadRenameRow(parent: HTMLElement, thread: ToolbarThreadRow, actions: ToolbarActions): void {
|
||||
let input!: HTMLInputElement;
|
||||
createToolbarPanelRow(parent, thread.title, {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export class CodexChatView extends ItemView {
|
|||
private scheduledRestoredThreadHydrationTimer: number | null = null;
|
||||
private scheduledRenderTimer: number | null = null;
|
||||
private scheduledDiagnosticsTimer: number | null = null;
|
||||
private archiveConfirmThreadId: string | null = null;
|
||||
private connectingPromise: Promise<void> | null = null;
|
||||
private connectionGeneration = 0;
|
||||
private resumeGeneration = 0;
|
||||
|
|
@ -1238,7 +1239,10 @@ export class CodexChatView extends ItemView {
|
|||
this.state.openDetails.delete("history");
|
||||
void this.selectThread(threadId);
|
||||
},
|
||||
archiveThread: (threadId) => void this.threadActions.archiveThread(threadId),
|
||||
startArchiveThread: (threadId) => {
|
||||
this.startArchiveThread(threadId);
|
||||
},
|
||||
archiveThread: (threadId, saveMarkdown) => void this.archiveThread(threadId, saveMarkdown),
|
||||
startRenameThread: (threadId) => {
|
||||
this.threadRename.start(threadId);
|
||||
},
|
||||
|
|
@ -1290,6 +1294,10 @@ export class CodexChatView extends ItemView {
|
|||
selected: threadId === this.state.activeThreadId,
|
||||
disabled: this.state.busy && threadId !== this.state.activeThreadId,
|
||||
canArchive: true,
|
||||
archiveConfirm: {
|
||||
active: this.archiveConfirmThreadId === threadId,
|
||||
defaultSaveMarkdown: this.plugin.settings.archiveExportEnabled,
|
||||
},
|
||||
rename: this.threadRename.editState(threadId),
|
||||
};
|
||||
}),
|
||||
|
|
@ -1376,10 +1384,22 @@ export class CodexChatView extends ItemView {
|
|||
this.addSystemMessage("Finish or interrupt the current turn before switching threads.");
|
||||
return;
|
||||
}
|
||||
this.archiveConfirmThreadId = null;
|
||||
if (await this.plugin.focusThreadInOpenView(threadId)) return;
|
||||
await this.resumeThread(threadId);
|
||||
}
|
||||
|
||||
private startArchiveThread(threadId: string): void {
|
||||
this.archiveConfirmThreadId = threadId;
|
||||
this.scheduleRender();
|
||||
}
|
||||
|
||||
private async archiveThread(threadId: string, saveMarkdown: boolean): Promise<void> {
|
||||
if (this.archiveConfirmThreadId === threadId) this.archiveConfirmThreadId = null;
|
||||
await this.threadActions.archiveThread(threadId, saveMarkdown);
|
||||
this.scheduleRender();
|
||||
}
|
||||
|
||||
private closeToolbarPanelOnOutsidePointer(event: PointerEvent): void {
|
||||
if (!this.hasOpenToolbarPanel()) return;
|
||||
|
||||
|
|
@ -1387,7 +1407,18 @@ export class CodexChatView extends ItemView {
|
|||
const viewWindow = this.containerEl.doc.defaultView;
|
||||
if (viewWindow && target instanceof viewWindow.Element) {
|
||||
const insideToolbarPanel = target.closest(".codex-panel__toolbar-primary, .codex-panel__toolbar-panel");
|
||||
if (insideToolbarPanel && this.containerEl.contains(insideToolbarPanel)) return;
|
||||
if (insideToolbarPanel && this.containerEl.contains(insideToolbarPanel)) {
|
||||
if (this.archiveConfirmThreadId && !target.closest(".codex-panel__archive-confirm")) {
|
||||
this.archiveConfirmThreadId = null;
|
||||
this.scheduleRender();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.archiveConfirmThreadId) {
|
||||
this.archiveConfirmThreadId = null;
|
||||
this.scheduleRender();
|
||||
}
|
||||
|
||||
if (this.threadRename.isEditing()) return;
|
||||
|
|
@ -1405,6 +1436,7 @@ export class CodexChatView extends ItemView {
|
|||
this.state.openDetails.delete("history");
|
||||
this.state.openDetails.delete("status-panel");
|
||||
this.state.runtimePicker = null;
|
||||
this.archiveConfirmThreadId = null;
|
||||
this.scheduleRender();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ export interface ThreadsViewActions {
|
|||
saveRename: (threadId: string, value: string) => void;
|
||||
cancelRename: (threadId: string) => void;
|
||||
autoNameThread: (threadId: string) => void;
|
||||
archiveThread: (threadId: string) => void;
|
||||
startArchive: (threadId: string) => void;
|
||||
archiveThread: (threadId: string, saveMarkdown: boolean) => void;
|
||||
}
|
||||
|
||||
export function renderThreadsView(parent: HTMLElement, model: ThreadsViewModel, actions: ThreadsViewActions): void {
|
||||
|
|
@ -57,11 +58,13 @@ function renderThreadRow(parent: HTMLElement, row: ThreadsRowModel, actions: Thr
|
|||
});
|
||||
if (row.live) item.addClass(`codex-panel-threads__row--${row.live.status}`);
|
||||
if (row.rename.active) item.addClass("codex-panel-threads__row--renaming");
|
||||
if (archiveConfirmState(row).active) item.addClass("codex-panel-threads__row--archive-confirming");
|
||||
item.onclick = () => {
|
||||
if (archiveConfirmState(row).active) return;
|
||||
actions.openThread(row.thread.id);
|
||||
};
|
||||
item.onkeydown = (event) => {
|
||||
if (row.rename.active) return;
|
||||
if (row.rename.active || archiveConfirmState(row).active) return;
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
actions.openThread(row.thread.id);
|
||||
|
|
@ -76,18 +79,60 @@ function renderThreadRow(parent: HTMLElement, row: ThreadsRowModel, actions: Thr
|
|||
titleLine.createSpan({ cls: "codex-panel-threads__row-title", text: row.title });
|
||||
|
||||
const controls = item.createDiv({ cls: "codex-panel-threads__actions" });
|
||||
const rename = iconButton(controls, "pencil", "Rename thread", "codex-panel-threads__row-button");
|
||||
rename.onclick = (event) => {
|
||||
if (!archiveConfirmState(row).active) {
|
||||
const rename = iconButton(controls, "pencil", "Rename thread", "codex-panel-threads__row-button");
|
||||
rename.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.startRename(row.thread.id, row.thread.name ?? row.title);
|
||||
};
|
||||
}
|
||||
renderArchiveActions(controls, row, actions);
|
||||
}
|
||||
|
||||
function renderArchiveActions(parent: HTMLElement, row: ThreadsRowModel, actions: ThreadsViewActions): void {
|
||||
const archiveConfirm = archiveConfirmState(row);
|
||||
if (!archiveConfirm.active) {
|
||||
const archive = iconButton(parent, "archive", "Archive thread", "codex-panel-threads__row-button");
|
||||
archive.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.startArchive(row.thread.id);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
parent.addClass("codex-panel-threads__archive-confirm");
|
||||
const defaultSaveMarkdown = archiveConfirm.defaultSaveMarkdown;
|
||||
const alternate = archiveModeButton(parent, !defaultSaveMarkdown, false);
|
||||
alternate.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.startRename(row.thread.id, row.thread.name ?? row.title);
|
||||
actions.archiveThread(row.thread.id, !defaultSaveMarkdown);
|
||||
};
|
||||
const archive = iconButton(controls, "archive", "Archive thread", "codex-panel-threads__row-button");
|
||||
archive.onclick = (event) => {
|
||||
const primary = archiveModeButton(parent, defaultSaveMarkdown, true);
|
||||
primary.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
actions.archiveThread(row.thread.id);
|
||||
actions.archiveThread(row.thread.id, defaultSaveMarkdown);
|
||||
};
|
||||
}
|
||||
|
||||
function archiveConfirmState(row: ThreadsRowModel): { active: boolean; defaultSaveMarkdown: boolean } {
|
||||
return row.archiveConfirm ?? { active: false, defaultSaveMarkdown: false };
|
||||
}
|
||||
|
||||
function archiveModeButton(parent: HTMLElement, saveMarkdown: boolean, primary: boolean): HTMLButtonElement {
|
||||
const label = saveMarkdown ? "Save and archive thread" : "Archive thread without saving";
|
||||
const icon = saveMarkdown ? "save" : "trash";
|
||||
const button = iconButton(
|
||||
parent,
|
||||
icon,
|
||||
label,
|
||||
["codex-panel-threads__row-button", primary ? "codex-panel-threads__archive-default" : "codex-panel-threads__archive-alternate"]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
);
|
||||
button.setAttr("aria-label", label);
|
||||
return button;
|
||||
}
|
||||
|
||||
function renderRenameRow(parent: HTMLElement, row: ThreadsRowModel, actions: ThreadsViewActions): void {
|
||||
parent.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface ThreadsRowModel {
|
|||
title: string;
|
||||
live: ThreadsLiveState | null;
|
||||
rename: { active: boolean; draft: string; generating: boolean };
|
||||
archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean };
|
||||
}
|
||||
|
||||
const STATUS_PRIORITY: Record<ThreadsLiveStatus, number> = {
|
||||
|
|
@ -32,6 +33,8 @@ export function threadRows(
|
|||
snapshots: OpenCodexPanelSnapshot[],
|
||||
renameDrafts: ReadonlyMap<string, string>,
|
||||
autoNameThreadId: string | null = null,
|
||||
archiveConfirmThreadId: string | null = null,
|
||||
defaultArchiveSaveMarkdown = false,
|
||||
): ThreadsRowModel[] {
|
||||
const snapshotsByThread = snapshotsForThreads(snapshots);
|
||||
return [...threads]
|
||||
|
|
@ -47,6 +50,10 @@ export function threadRows(
|
|||
draft: renameDrafts.get(thread.id) ?? thread.name ?? getThreadTitle(thread),
|
||||
generating: autoNameThreadId === thread.id,
|
||||
},
|
||||
archiveConfirm: {
|
||||
active: archiveConfirmThreadId === thread.id,
|
||||
defaultSaveMarkdown: defaultArchiveSaveMarkdown,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export class CodexThreadsView extends ItemView {
|
|||
private readonly renameDrafts = new Map<string, string>();
|
||||
private renameAutoNameThreadId: string | null = null;
|
||||
private renameAutoNameGeneration = 0;
|
||||
private archiveConfirmThreadId: string | null = null;
|
||||
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
|
|
@ -77,6 +78,9 @@ export class CodexThreadsView extends ItemView {
|
|||
}
|
||||
|
||||
override async onOpen(): Promise<void> {
|
||||
this.registerDomEvent(this.containerEl.doc, "pointerdown", (event) => {
|
||||
this.cancelArchiveConfirmOnOutsidePointer(event);
|
||||
});
|
||||
const cachedThreads = this.plugin.cachedThreadList();
|
||||
if (cachedThreads) {
|
||||
this.threads = cachedThreads;
|
||||
|
|
@ -160,7 +164,14 @@ export class CodexThreadsView extends ItemView {
|
|||
{
|
||||
status: this.status,
|
||||
loading: this.loading,
|
||||
rows: threadRows(this.threads, this.plugin.getOpenPanelSnapshots(), this.renameDrafts, this.renameAutoNameThreadId),
|
||||
rows: threadRows(
|
||||
this.threads,
|
||||
this.plugin.getOpenPanelSnapshots(),
|
||||
this.renameDrafts,
|
||||
this.renameAutoNameThreadId,
|
||||
this.archiveConfirmThreadId,
|
||||
this.plugin.settings.archiveExportEnabled,
|
||||
),
|
||||
},
|
||||
{
|
||||
refresh: () => void this.refresh(),
|
||||
|
|
@ -177,7 +188,10 @@ export class CodexThreadsView extends ItemView {
|
|||
this.cancelRename(threadId);
|
||||
},
|
||||
autoNameThread: (threadId) => void this.autoNameThread(threadId),
|
||||
archiveThread: (threadId) => void this.archiveThread(threadId),
|
||||
startArchive: (threadId) => {
|
||||
this.startArchive(threadId);
|
||||
},
|
||||
archiveThread: (threadId, saveMarkdown) => void this.archiveThread(threadId, saveMarkdown),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -209,6 +223,7 @@ export class CodexThreadsView extends ItemView {
|
|||
}
|
||||
|
||||
private async openThread(threadId: string): Promise<void> {
|
||||
this.archiveConfirmThreadId = null;
|
||||
await this.plugin.openThreadInAvailableView(threadId);
|
||||
}
|
||||
|
||||
|
|
@ -217,6 +232,7 @@ export class CodexThreadsView extends ItemView {
|
|||
}
|
||||
|
||||
private startRename(threadId: string, value: string): void {
|
||||
this.archiveConfirmThreadId = null;
|
||||
this.renameAutoNameGeneration += 1;
|
||||
this.renameAutoNameThreadId = null;
|
||||
this.renameDrafts.set(threadId, value);
|
||||
|
|
@ -291,16 +307,34 @@ export class CodexThreadsView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
private async archiveThread(threadId: string): Promise<void> {
|
||||
private startArchive(threadId: string): void {
|
||||
this.archiveConfirmThreadId = threadId;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private cancelArchiveConfirmOnOutsidePointer(event: PointerEvent): void {
|
||||
if (!this.archiveConfirmThreadId) return;
|
||||
const target = event.target;
|
||||
const viewWindow = this.containerEl.doc.defaultView;
|
||||
if (viewWindow && target instanceof viewWindow.Element) {
|
||||
const archiveConfirm = target.closest(".codex-panel-threads__archive-confirm");
|
||||
if (archiveConfirm && this.containerEl.contains(archiveConfirm)) return;
|
||||
}
|
||||
this.archiveConfirmThreadId = null;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private async archiveThread(threadId: string, saveMarkdown: boolean): Promise<void> {
|
||||
try {
|
||||
await this.ensureConnected();
|
||||
if (!this.client) return;
|
||||
if (this.plugin.settings.archiveExportEnabled) {
|
||||
if (saveMarkdown) {
|
||||
const response = await this.client.readThread(threadId, true);
|
||||
const result = await exportArchivedThreadMarkdown(response.thread, this.plugin.settings, this.app.vault.adapter);
|
||||
new Notice(`Saved archived thread to ${result.path}.`);
|
||||
}
|
||||
await this.client.archiveThread(threadId);
|
||||
if (this.archiveConfirmThreadId === threadId) this.archiveConfirmThreadId = null;
|
||||
this.renameDrafts.delete(threadId);
|
||||
this.plugin.notifyThreadArchived(threadId);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,10 @@ export function renderArchivedThreadSection(containerEl: HTMLElement, state: Arc
|
|||
new Setting(section)
|
||||
.setClass("codex-panel-settings__dynamic-section-heading")
|
||||
.setHeading()
|
||||
.setName("Archive actions")
|
||||
.setDesc("Save threads to notes before archiving, or restore archived threads.");
|
||||
.setName("Thread archiving")
|
||||
.setDesc(
|
||||
"Choose the default archive behavior and configure saved thread notes. Thread lists offer both archive choices; slash commands use the default.",
|
||||
);
|
||||
|
||||
renderArchiveExportSettings(section, state);
|
||||
|
||||
|
|
@ -83,9 +85,9 @@ export function renderArchivedThreadSection(containerEl: HTMLElement, state: Arc
|
|||
|
||||
function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedThreadSectionState): void {
|
||||
new Setting(containerEl)
|
||||
.setName("Save before archiving")
|
||||
.setName("Save note by default")
|
||||
.setDesc(
|
||||
"Save a markdown note before archiving. If saving fails, the thread stays active. Frontmatter includes title, thread_id, created, and optional tags.",
|
||||
"When on, the default archive action saves a markdown note before archiving. When off, the default archives without saving. If saving fails, the thread stays active. Frontmatter includes title, thread_id, created, and optional tags.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(state.exportEnabled).onChange((value) => {
|
||||
|
|
@ -94,7 +96,7 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
|
|||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Save folder")
|
||||
.setName("Saved note folder")
|
||||
.setDesc("Vault-relative folder for saved thread notes. The folder is created when needed.")
|
||||
.addText((text) => {
|
||||
text
|
||||
|
|
@ -106,7 +108,7 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
|
|||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Save filename")
|
||||
.setName("Saved note filename")
|
||||
.setDesc("Filename template. Variables: {{date}}, {{time}}, {{title}}, {{id}}, {{shortId}}. Existing files get a numeric suffix.")
|
||||
.addText((text) => {
|
||||
text
|
||||
|
|
@ -118,7 +120,7 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
|
|||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Save tags")
|
||||
.setName("Saved note tags")
|
||||
.setDesc("Comma-separated fixed tags for saved notes. Leave empty to omit tags.")
|
||||
.addText((text) => {
|
||||
text
|
||||
|
|
@ -131,11 +133,13 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
|
|||
}
|
||||
|
||||
function renderArchivedThreadList(containerEl: HTMLElement, state: ArchivedThreadSectionState): void {
|
||||
containerEl.createEl("p", {
|
||||
const summary = containerEl.createEl("p", {
|
||||
cls: "setting-item-description codex-panel-settings__dynamic-list-summary",
|
||||
text: `Restore archived threads to the thread list. Loaded ${String(state.threads.length)} archived thread${
|
||||
state.threads.length === 1 ? "" : "s"
|
||||
} from Codex app server.`,
|
||||
});
|
||||
summary.createSpan({ text: "Restore archived threads to the active thread list." });
|
||||
summary.createSpan({
|
||||
cls: "codex-panel-settings__dynamic-list-count",
|
||||
text: `Loaded ${String(state.threads.length)} archived thread${state.threads.length === 1 ? "" : "s"} from Codex app server.`,
|
||||
});
|
||||
const list = containerEl.createDiv({ cls: "setting-items codex-panel-settings__dynamic-list codex-panel-settings__archived-list" });
|
||||
for (const thread of state.threads) {
|
||||
|
|
|
|||
27
styles.css
27
styles.css
|
|
@ -294,6 +294,10 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.codex-panel-threads__row--archive-confirming .codex-panel-threads__actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.codex-panel-threads__rename-input.codex-panel-threads__rename-input {
|
||||
position: relative;
|
||||
appearance: none;
|
||||
|
|
@ -627,11 +631,25 @@
|
|||
padding: 0 var(--size-4-4);
|
||||
}
|
||||
|
||||
.codex-panel-settings__section-intro,
|
||||
.codex-panel-settings__dynamic-list-summary {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.codex-panel-settings__dynamic-list-summary {
|
||||
padding: 0 var(--size-4-4);
|
||||
margin: 0 0 var(--size-2-3);
|
||||
}
|
||||
|
||||
.codex-panel-settings__dynamic-list-count {
|
||||
display: block;
|
||||
margin-top: var(--size-2-1);
|
||||
color: var(--text-faint);
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.codex-panel-settings__dynamic-list {
|
||||
margin: var(--size-2-3) 0 0;
|
||||
overflow: auto;
|
||||
|
|
@ -1152,6 +1170,10 @@
|
|||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.codex-panel__thread-row--archive-confirming {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.codex-panel__thread:hover:not(:where(:disabled, .is-disabled)),
|
||||
.codex-panel__thread.is-checked {
|
||||
background: transparent;
|
||||
|
|
@ -1242,6 +1264,11 @@
|
|||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.codex-panel__thread-row--archive-confirming .codex-panel__thread-action {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.codex-panel__thread-action:hover,
|
||||
.codex-panel__thread-action:focus,
|
||||
.codex-panel__thread-action:focus-visible,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ function threadsViewActions() {
|
|||
saveRename: vi.fn(),
|
||||
cancelRename: vi.fn(),
|
||||
autoNameThread: vi.fn(),
|
||||
startArchive: vi.fn(),
|
||||
archiveThread: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
|
@ -1908,6 +1909,47 @@ describe("toolbar renderer decisions", () => {
|
|||
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.disabled).toBe(true);
|
||||
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("renders toolbar archive confirmation with the default action on the right", () => {
|
||||
const parent = document.createElement("div");
|
||||
const startArchiveThread = vi.fn();
|
||||
const archiveThread = vi.fn();
|
||||
|
||||
renderToolbar(
|
||||
parent,
|
||||
toolbarModel({
|
||||
historyOpen: true,
|
||||
openPanel: "history",
|
||||
threads: [
|
||||
{
|
||||
title: "Thread",
|
||||
threadId: "thread",
|
||||
selected: true,
|
||||
disabled: false,
|
||||
canArchive: true,
|
||||
archiveConfirm: { active: true, defaultSaveMarkdown: true },
|
||||
rename: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
toolbarActions({ startArchiveThread, archiveThread }),
|
||||
);
|
||||
|
||||
const confirm = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__archive-confirm"));
|
||||
const archiveButtons = [
|
||||
...confirm.querySelectorAll<HTMLButtonElement>(".codex-panel__archive-alternate, .codex-panel__archive-default"),
|
||||
];
|
||||
expect(archiveButtons.map((button) => button.getAttribute("aria-label"))).toEqual([
|
||||
"Archive thread without saving",
|
||||
"Save and archive thread",
|
||||
]);
|
||||
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Rename thread"]')).toBeNull();
|
||||
archiveButtons[0]?.click();
|
||||
expect(archiveThread).toHaveBeenCalledWith("thread", false);
|
||||
archiveButtons[1]?.click();
|
||||
expect(archiveThread).toHaveBeenCalledWith("thread", true);
|
||||
expect(startArchiveThread).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("threads view renderer decisions", () => {
|
||||
|
|
@ -1965,6 +2007,51 @@ describe("threads view renderer decisions", () => {
|
|||
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Open in new panel"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("renders threads view archive confirmation with the default action on the right", () => {
|
||||
const parent = document.createElement("div");
|
||||
const actions = threadsViewActions();
|
||||
const row: ThreadsRowModel = {
|
||||
thread: threadFixture({ id: "thread", name: "Thread" }),
|
||||
title: "Thread",
|
||||
live: null,
|
||||
rename: { active: false, draft: "Thread", generating: false },
|
||||
archiveConfirm: { active: true, defaultSaveMarkdown: false },
|
||||
};
|
||||
|
||||
renderThreadsView(parent, { status: "1 thread", loading: false, rows: [row] }, actions);
|
||||
|
||||
const confirm = expectPresent(parent.querySelector<HTMLElement>(".codex-panel-threads__archive-confirm"));
|
||||
const archiveButtons = [
|
||||
...confirm.querySelectorAll<HTMLButtonElement>(".codex-panel-threads__archive-alternate, .codex-panel-threads__archive-default"),
|
||||
];
|
||||
expect(archiveButtons.map((button) => button.getAttribute("aria-label"))).toEqual([
|
||||
"Save and archive thread",
|
||||
"Archive thread without saving",
|
||||
]);
|
||||
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Rename thread"]')).toBeNull();
|
||||
archiveButtons[0]?.click();
|
||||
expect(actions.archiveThread).toHaveBeenCalledWith("thread", true);
|
||||
archiveButtons[1]?.click();
|
||||
expect(actions.archiveThread).toHaveBeenCalledWith("thread", false);
|
||||
});
|
||||
|
||||
it("starts threads view archive confirmation before archiving", () => {
|
||||
const parent = document.createElement("div");
|
||||
const actions = threadsViewActions();
|
||||
const row: ThreadsRowModel = {
|
||||
thread: threadFixture({ id: "thread", name: "Thread" }),
|
||||
title: "Thread",
|
||||
live: null,
|
||||
rename: { active: false, draft: "Thread", generating: false },
|
||||
};
|
||||
|
||||
renderThreadsView(parent, { status: "1 thread", loading: false, rows: [row] }, actions);
|
||||
parent.querySelector<HTMLButtonElement>('[aria-label="Archive thread"]')?.click();
|
||||
|
||||
expect(actions.startArchive).toHaveBeenCalledWith("thread");
|
||||
expect(actions.archiveThread).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders rename rows and saves entered values", () => {
|
||||
const parent = document.createElement("div");
|
||||
const actions = threadsViewActions();
|
||||
|
|
@ -2457,6 +2544,7 @@ function toolbarActions(overrides: Partial<Parameters<typeof renderToolbar>[2]>
|
|||
refreshDiagnostics: vi.fn(),
|
||||
refreshThreads: vi.fn(),
|
||||
resumeThread: vi.fn(),
|
||||
startArchiveThread: vi.fn(),
|
||||
archiveThread: vi.fn(),
|
||||
startRenameThread: vi.fn(),
|
||||
updateRenameDraft: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ describe("CodexThreadsView", () => {
|
|||
|
||||
await view.refresh();
|
||||
view.containerEl.querySelector<HTMLButtonElement>('[aria-label="Archive thread"]')?.click();
|
||||
view.containerEl.querySelector<HTMLButtonElement>('[aria-label="Archive thread without saving"]')?.click();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(archiveThread).toHaveBeenCalledWith("thread");
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ describe("settings tab", () => {
|
|||
"Codex helpers",
|
||||
"Automatic thread naming",
|
||||
"Selection rewrite",
|
||||
"Archive actions",
|
||||
"Thread archiving",
|
||||
"Hook status",
|
||||
]);
|
||||
});
|
||||
|
|
@ -126,9 +126,9 @@ describe("settings tab", () => {
|
|||
|
||||
tab.display();
|
||||
const toggle = tab.containerEl.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||
const folder = inputForSetting(tab, "Save folder");
|
||||
const filename = inputForSetting(tab, "Save filename");
|
||||
const tags = inputForSetting(tab, "Save tags");
|
||||
const folder = inputForSetting(tab, "Saved note folder");
|
||||
const filename = inputForSetting(tab, "Saved note filename");
|
||||
const tags = inputForSetting(tab, "Saved note tags");
|
||||
if (!toggle || !folder || !filename || !tags) throw new Error("Missing archive export controls");
|
||||
|
||||
toggle.checked = true;
|
||||
|
|
@ -143,8 +143,9 @@ describe("settings tab", () => {
|
|||
|
||||
expect(saveSettings).toHaveBeenCalledTimes(4);
|
||||
expect(tab.containerEl.textContent).toContain("title, thread_id, created, and optional tags");
|
||||
expect(settingDesc(tab, "Save before archiving")).toContain("If saving fails");
|
||||
expect(settingDesc(tab, "Save tags")).toContain("Leave empty to omit tags");
|
||||
expect(settingDesc(tab, "Save note by default")).toContain("default archive action");
|
||||
expect(settingDesc(tab, "Save note by default")).toContain("If saving fails");
|
||||
expect(settingDesc(tab, "Saved note tags")).toContain("Leave empty to omit tags");
|
||||
});
|
||||
|
||||
it("refreshes models, hooks, and archived threads from the global refresh button", async () => {
|
||||
|
|
@ -225,13 +226,13 @@ describe("settings tab", () => {
|
|||
await flushPromises();
|
||||
|
||||
expect(tab.containerEl.textContent).toContain("Loaded 1 hook from Codex app server.");
|
||||
expect(tab.containerEl.textContent).toContain("Restore archived threads to the thread list.");
|
||||
expect(tab.containerEl.textContent).toContain("Restore archived threads to the active thread list.");
|
||||
expect(tab.containerEl.textContent).toContain("Loaded 1 archived thread from Codex app server.");
|
||||
expect(tab.containerEl.querySelector(".codex-panel-settings__hook-section .setting-item-heading")?.textContent).toContain(
|
||||
"Hook status",
|
||||
);
|
||||
expect(tab.containerEl.querySelector(".codex-panel-settings__archived-section .setting-item-heading")?.textContent).toContain(
|
||||
"Archive actions",
|
||||
"Thread archiving",
|
||||
);
|
||||
expect(tab.containerEl.querySelectorAll(".codex-panel-settings__hook-list .setting-item")).toHaveLength(1);
|
||||
expect(tab.containerEl.querySelectorAll(".codex-panel-settings__archived-list .setting-item")).toHaveLength(1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue