mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Improve settings Codex data refresh UI
This commit is contained in:
parent
3b6077ab51
commit
d72ceacd94
6 changed files with 122 additions and 42 deletions
|
|
@ -28,6 +28,26 @@ export type SettledSettingsData<T> =
|
|||
status: string;
|
||||
};
|
||||
|
||||
export type SettingsDataRefreshLifecycleState = { kind: "idle" } | { kind: "loading" } | { kind: "completed"; failedCount: number };
|
||||
|
||||
export type SettingsDataRefreshLifecycleEvent = { type: "started" } | { type: "completed"; failedCount: number };
|
||||
|
||||
export function transitionSettingsDataRefreshLifecycle(
|
||||
state: SettingsDataRefreshLifecycleState,
|
||||
event: SettingsDataRefreshLifecycleEvent,
|
||||
): SettingsDataRefreshLifecycleState {
|
||||
switch (event.type) {
|
||||
case "started":
|
||||
return state.kind === "loading" ? state : { kind: "loading" };
|
||||
case "completed":
|
||||
return { kind: "completed", failedCount: event.failedCount };
|
||||
}
|
||||
}
|
||||
|
||||
export function settingsDataRefreshLoading(state: SettingsDataRefreshLifecycleState): boolean {
|
||||
return state.kind === "loading";
|
||||
}
|
||||
|
||||
export async function loadSettingsData(client: AppServerClient, cwd: string): Promise<SettingsDataLoad> {
|
||||
const [modelsResult, hooksResult, archivedThreadsResult] = await Promise.allSettled([
|
||||
client.listModels(false),
|
||||
|
|
|
|||
|
|
@ -133,13 +133,9 @@ function renderArchiveExportSettings(containerEl: HTMLElement, state: ArchivedTh
|
|||
}
|
||||
|
||||
function renderArchivedThreadList(containerEl: HTMLElement, state: ArchivedThreadSectionState): void {
|
||||
const summary = containerEl.createEl("p", {
|
||||
containerEl.createEl("p", {
|
||||
cls: "setting-item-description codex-panel-settings__dynamic-list-summary",
|
||||
});
|
||||
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.`,
|
||||
text: "Restore archived threads to the active thread list.",
|
||||
});
|
||||
const list = containerEl.createDiv({ cls: "setting-items codex-panel-settings__dynamic-list codex-panel-settings__archived-list" });
|
||||
for (const thread of state.threads) {
|
||||
|
|
@ -163,10 +159,6 @@ function renderHooks(containerEl: HTMLElement, state: HookSectionState): void {
|
|||
if (state.hooks.length === 0) {
|
||||
containerEl.createEl("p", { cls: "setting-item-description", text: "No hooks found for this vault root." });
|
||||
} else {
|
||||
containerEl.createEl("p", {
|
||||
cls: "setting-item-description codex-panel-settings__dynamic-list-summary",
|
||||
text: `Loaded ${String(state.hooks.length)} hook${state.hooks.length === 1 ? "" : "s"} from Codex app server.`,
|
||||
});
|
||||
const list = containerEl.createDiv({ cls: "setting-items codex-panel-settings__dynamic-list codex-panel-settings__hook-list" });
|
||||
for (const hook of state.hooks) {
|
||||
renderHookRow(list, hook, state);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type App, Notice, type Plugin, PluginSettingTab, Setting } from "obsidian";
|
||||
import { type App, Notice, type Plugin, PluginSettingTab, Setting, setIcon } from "obsidian";
|
||||
|
||||
import type { AppServerClient } from "../app-server/client";
|
||||
import { withAppServerConnection } from "../app-server/connection-client";
|
||||
|
|
@ -10,7 +10,13 @@ import type { Thread } from "../generated/app-server/v2/Thread";
|
|||
import { findModelByIdOrName, REASONING_EFFORTS, sortedAvailableModels, supportedEffortsForModel } from "../runtime/model";
|
||||
import { archivedThreadDisplayTitle } from "../domain/threads/model";
|
||||
import { errorMessage } from "../utils";
|
||||
import { loadHookData, loadSettingsData } from "./data";
|
||||
import {
|
||||
loadHookData,
|
||||
loadSettingsData,
|
||||
settingsDataRefreshLoading,
|
||||
transitionSettingsDataRefreshLifecycle,
|
||||
type SettingsDataRefreshLifecycleState,
|
||||
} from "./data";
|
||||
import { renderArchivedThreadSection, renderHookSection } from "./dynamic-sections";
|
||||
import type { CodexPanelSettings } from "./model";
|
||||
|
||||
|
|
@ -24,9 +30,15 @@ function renderSettingsHeading(containerEl: HTMLElement, name: string): void {
|
|||
new Setting(containerEl).setClass("codex-panel-settings__section-heading").setHeading().setName(name);
|
||||
}
|
||||
|
||||
function settingsDataRefreshStatus(state: SettingsDataRefreshLifecycleState): string {
|
||||
if (state.kind === "loading") return "Refreshing Codex data...";
|
||||
if (state.kind === "completed" && state.failedCount > 0) return "Could not refresh all Codex data.";
|
||||
return "";
|
||||
}
|
||||
|
||||
export class CodexPanelSettingTab extends PluginSettingTab {
|
||||
private settingsDataAutoLoadStarted = false;
|
||||
private settingsDataLoading = false;
|
||||
private settingsDataRefreshLifecycle: SettingsDataRefreshLifecycleState = { kind: "idle" };
|
||||
private archivedThreads: Thread[] = [];
|
||||
private archivedThreadsLoaded = false;
|
||||
private archivedThreadsLoading = false;
|
||||
|
|
@ -54,6 +66,8 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
containerEl.empty();
|
||||
containerEl.addClass("codex-panel-settings");
|
||||
|
||||
this.renderHeaderActions(containerEl);
|
||||
|
||||
const configSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__general-section" });
|
||||
configSection.createEl("p", {
|
||||
cls: "setting-item-description codex-panel-settings__section-intro",
|
||||
|
|
@ -73,16 +87,6 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
new Setting(configSection)
|
||||
.setName("Codex data")
|
||||
.setDesc("Refresh models, hooks, and archived threads from Codex app server.")
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(this.settingsDataLoading ? "Refreshing..." : "Refresh Codex data")
|
||||
.setDisabled(this.settingsDataLoading)
|
||||
.onClick(() => void this.refreshSettingsData());
|
||||
});
|
||||
|
||||
const composerSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__composer-section" });
|
||||
renderSettingsHeading(composerSection, "Composer");
|
||||
new Setting(composerSection)
|
||||
|
|
@ -213,14 +217,33 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
this.maybeAutoLoadSettingsData();
|
||||
}
|
||||
|
||||
private renderHeaderActions(containerEl: HTMLElement): void {
|
||||
const header = containerEl.createDiv({ cls: "codex-panel-settings__header" });
|
||||
const status = settingsDataRefreshStatus(this.settingsDataRefreshLifecycle);
|
||||
header.createEl("span", {
|
||||
cls: "setting-item-description codex-panel-settings__refresh-status",
|
||||
text: status,
|
||||
});
|
||||
const button = header.createEl("button", {
|
||||
cls: "clickable-icon codex-panel-settings__refresh-button",
|
||||
});
|
||||
button.type = "button";
|
||||
button.disabled = this.settingsDataLoading();
|
||||
button.ariaLabel = this.settingsDataLoading() ? "Refreshing Codex data" : "Refresh Codex data";
|
||||
setIcon(button, "refresh-cw");
|
||||
button.addEventListener("click", () => {
|
||||
void this.refreshSettingsData();
|
||||
});
|
||||
}
|
||||
|
||||
private maybeAutoLoadSettingsData(): void {
|
||||
if (this.settingsDataAutoLoadStarted || this.settingsDataLoading) return;
|
||||
if (this.settingsDataAutoLoadStarted || this.settingsDataLoading()) return;
|
||||
this.settingsDataAutoLoadStarted = true;
|
||||
void this.refreshSettingsData();
|
||||
}
|
||||
|
||||
private async refreshSettingsData(): Promise<void> {
|
||||
this.settingsDataLoading = true;
|
||||
this.settingsDataRefreshLifecycle = transitionSettingsDataRefreshLifecycle(this.settingsDataRefreshLifecycle, { type: "started" });
|
||||
this.modelsLoading = true;
|
||||
this.archivedThreadsLoading = true;
|
||||
this.hooksLoading = true;
|
||||
|
|
@ -268,7 +291,10 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
this.hooksStatus = `Could not load hooks: ${message}`;
|
||||
this.archivedThreadsStatus = `Could not load archived threads: ${message}`;
|
||||
} finally {
|
||||
this.settingsDataLoading = false;
|
||||
this.settingsDataRefreshLifecycle = transitionSettingsDataRefreshLifecycle(this.settingsDataRefreshLifecycle, {
|
||||
type: "completed",
|
||||
failedCount,
|
||||
});
|
||||
this.modelsLoading = false;
|
||||
this.archivedThreadsLoading = false;
|
||||
this.hooksLoading = false;
|
||||
|
|
@ -279,6 +305,10 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}
|
||||
|
||||
private settingsDataLoading(): boolean {
|
||||
return settingsDataRefreshLoading(this.settingsDataRefreshLifecycle);
|
||||
}
|
||||
|
||||
private async loadHooks(): Promise<void> {
|
||||
this.hooksLoading = true;
|
||||
this.hooksStatus = "";
|
||||
|
|
|
|||
28
styles.css
28
styles.css
|
|
@ -671,6 +671,27 @@
|
|||
margin-top: var(--size-4-6);
|
||||
}
|
||||
|
||||
.codex-panel-settings__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--size-4-2);
|
||||
min-height: var(--input-height);
|
||||
}
|
||||
|
||||
.codex-panel-settings__refresh-status {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.codex-panel-settings__refresh-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--input-height);
|
||||
height: var(--input-height);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.codex-panel-settings__section-heading.setting-item-heading,
|
||||
.codex-panel-settings__dynamic-section-heading.setting-item-heading {
|
||||
margin-bottom: var(--size-4-2);
|
||||
|
|
@ -696,13 +717,6 @@
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,27 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AppServerClient } from "../../src/app-server/client";
|
||||
import { loadHookData, loadSettingsData } from "../../src/settings/data";
|
||||
import {
|
||||
loadHookData,
|
||||
loadSettingsData,
|
||||
settingsDataRefreshLoading,
|
||||
transitionSettingsDataRefreshLifecycle,
|
||||
} from "../../src/settings/data";
|
||||
|
||||
describe("settings data", () => {
|
||||
it("tracks settings data refresh lifecycle", () => {
|
||||
const idle = { kind: "idle" } as const;
|
||||
|
||||
const loading = transitionSettingsDataRefreshLifecycle(idle, { type: "started" });
|
||||
expect(loading).toEqual({ kind: "loading" });
|
||||
expect(settingsDataRefreshLoading(loading)).toBe(true);
|
||||
expect(transitionSettingsDataRefreshLifecycle(loading, { type: "started" })).toBe(loading);
|
||||
|
||||
const completed = transitionSettingsDataRefreshLifecycle(loading, { type: "completed", failedCount: 2 });
|
||||
expect(completed).toEqual({ kind: "completed", failedCount: 2 });
|
||||
expect(settingsDataRefreshLoading(completed)).toBe(false);
|
||||
});
|
||||
|
||||
it("uses only hook rows for the requested cwd", async () => {
|
||||
const client = {
|
||||
listHooks: vi.fn().mockResolvedValue({
|
||||
|
|
|
|||
|
|
@ -85,14 +85,15 @@ describe("settings tab", () => {
|
|||
await flushPromises();
|
||||
|
||||
expect(withAppServerConnectionMock).toHaveBeenCalledTimes(1);
|
||||
expect(buttonTexts(tab)).toContain("Refresh Codex data");
|
||||
expect(buttonLabels(tab)).toContain("Refresh Codex data");
|
||||
expect(buttonTexts(tab)).not.toContain("Refresh Codex data");
|
||||
expect(buttonTexts(tab)).not.toContain("Load models");
|
||||
expect(buttonTexts(tab)).not.toContain("Load hooks");
|
||||
expect(buttonTexts(tab)).not.toContain("Load archive list");
|
||||
expect(tab.containerEl.querySelector(".codex-panel-settings__header button")?.getAttribute("data-icon")).toBe("refresh-cw");
|
||||
expect(tab.containerEl.querySelector("h2")).toBeNull();
|
||||
expect(settingNames(tab)).toEqual([
|
||||
"Codex executable",
|
||||
"Codex data",
|
||||
"Composer",
|
||||
"Send shortcut",
|
||||
"Codex helpers",
|
||||
|
|
@ -165,7 +166,7 @@ describe("settings tab", () => {
|
|||
|
||||
tab.display();
|
||||
await flushPromises();
|
||||
clickButton(tab, "Refresh Codex data");
|
||||
clickButtonByLabel(tab, "Refresh Codex data");
|
||||
await flushPromises();
|
||||
|
||||
expect(withAppServerConnectionMock).toHaveBeenCalledTimes(2);
|
||||
|
|
@ -208,6 +209,7 @@ describe("settings tab", () => {
|
|||
|
||||
expect(tab.containerEl.textContent).not.toContain("Loaded 1 model.");
|
||||
expect(tab.containerEl.textContent).toContain("Could not load hooks: hooks unavailable");
|
||||
expect(tab.containerEl.querySelector(".codex-panel-settings__refresh-status")?.textContent).toBe("Could not refresh all Codex data.");
|
||||
expect(tab.containerEl.textContent).toContain("Archived thread");
|
||||
expect(notices).toEqual(["Could not refresh all Codex data."]);
|
||||
});
|
||||
|
|
@ -225,9 +227,9 @@ describe("settings tab", () => {
|
|||
tab.display();
|
||||
await flushPromises();
|
||||
|
||||
expect(tab.containerEl.textContent).toContain("Loaded 1 hook from Codex app server.");
|
||||
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.textContent).not.toContain("Loaded 1 hook from Codex app server.");
|
||||
expect(tab.containerEl.textContent).not.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",
|
||||
);
|
||||
|
|
@ -393,9 +395,13 @@ function buttonTexts(tab: CodexPanelSettingTab): string[] {
|
|||
return Array.from(tab.containerEl.querySelectorAll("button")).map((element) => element.textContent);
|
||||
}
|
||||
|
||||
function clickButton(tab: CodexPanelSettingTab, text: string): void {
|
||||
const button = Array.from(tab.containerEl.querySelectorAll("button")).find((element) => element.textContent === text);
|
||||
if (!button) throw new Error(`Could not find button: ${text}`);
|
||||
function buttonLabels(tab: CodexPanelSettingTab): string[] {
|
||||
return Array.from(tab.containerEl.querySelectorAll("button")).map((element) => element.ariaLabel ?? "");
|
||||
}
|
||||
|
||||
function clickButtonByLabel(tab: CodexPanelSettingTab, label: string): void {
|
||||
const button = Array.from(tab.containerEl.querySelectorAll("button")).find((element) => element.ariaLabel === label);
|
||||
if (!button) throw new Error(`Could not find button: ${label}`);
|
||||
button.click();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue