2026-06-13 09:26:15 +00:00
|
|
|
import { Notice } from "obsidian";
|
|
|
|
|
|
2026-07-14 12:15:21 +00:00
|
|
|
import { type AppServerQueryContext, appServerQueryContextRawEquals } from "../../app-server/query/keys";
|
2026-06-30 14:47:59 +00:00
|
|
|
import type { ObservedResult } from "../../app-server/query/observed-result";
|
2026-07-04 07:49:05 +00:00
|
|
|
import { observedInitialError, observedInitialLoading } from "../../app-server/query/observed-result";
|
2026-07-16 12:44:28 +00:00
|
|
|
import { isStaleAppServerResourceContextError } from "../../app-server/query/resource-store";
|
2026-06-17 03:55:54 +00:00
|
|
|
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
2026-06-24 06:13:35 +00:00
|
|
|
import type { ArchiveExportSettings } from "../../domain/threads/archive-markdown";
|
2026-06-13 09:26:15 +00:00
|
|
|
import type { Thread } from "../../domain/threads/model";
|
2026-07-16 09:50:25 +00:00
|
|
|
import type { ThreadRenameLifecycleEvent } from "../../domain/threads/rename-lifecycle";
|
2026-07-15 04:45:00 +00:00
|
|
|
import { DeferredTask } from "../../shared/runtime/deferred-task";
|
2026-07-10 03:15:22 +00:00
|
|
|
import { OwnerLifetime } from "../../shared/runtime/owner-lifetime";
|
2026-07-10 03:54:24 +00:00
|
|
|
import type { ThreadCatalogEventSink, ThreadCatalogPaginatedActiveReader } from "../threads/catalog/thread-catalog";
|
2026-06-30 02:49:23 +00:00
|
|
|
import type { ArchiveExportDestination } from "../threads/workflows/archive-export";
|
2026-07-12 09:18:11 +00:00
|
|
|
import type { ThreadOperationsTransport, ThreadTitleTransport } from "../threads/workflows/ports";
|
2026-07-13 11:56:18 +00:00
|
|
|
import type { ThreadNameMutationCoordinator } from "../threads/workflows/thread-name-mutation-coordinator";
|
2026-06-27 12:25:13 +00:00
|
|
|
import { createThreadOperations, type ThreadOperations } from "../threads/workflows/thread-operations";
|
|
|
|
|
import { createThreadTitleService, type ThreadTitleService } from "../threads/workflows/thread-title-service";
|
2026-06-27 02:36:00 +00:00
|
|
|
import { isThreadsArchiveConfirmPointer, renderThreadsViewShell, unmountThreadsViewShell } from "./shell.dom";
|
2026-07-16 09:50:25 +00:00
|
|
|
import { type ThreadsRenameState, type ThreadsViewPanelActivity, threadRows, transitionThreadsRenameState } from "./state";
|
2026-06-26 11:23:37 +00:00
|
|
|
export interface ThreadsViewHost {
|
|
|
|
|
readonly settings: ThreadsViewSettingsAccess;
|
2026-06-13 09:26:15 +00:00
|
|
|
readonly vaultPath: string;
|
2026-06-26 11:23:37 +00:00
|
|
|
readonly threadCatalog: ThreadsViewThreadCatalog;
|
2026-07-13 11:56:18 +00:00
|
|
|
readonly threadNameMutations: ThreadNameMutationCoordinator;
|
2026-07-12 09:18:11 +00:00
|
|
|
readonly threadOperationsTransport: ThreadOperationsTransport;
|
|
|
|
|
readonly threadTitleTransport: ThreadTitleTransport;
|
2026-06-13 09:26:15 +00:00
|
|
|
openNewPanel(): Promise<unknown>;
|
|
|
|
|
openThreadInAvailableView(threadId: string): Promise<void>;
|
2026-06-27 11:40:42 +00:00
|
|
|
openPanelActivities(): readonly ThreadsViewPanelActivity[];
|
2026-06-30 03:09:14 +00:00
|
|
|
closeOpenPanelsForThread(threadId: string): void;
|
2026-06-13 09:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 03:54:24 +00:00
|
|
|
type ThreadsViewThreadCatalog = ThreadCatalogPaginatedActiveReader & ThreadCatalogEventSink;
|
2026-06-15 06:00:14 +00:00
|
|
|
|
2026-06-26 11:23:37 +00:00
|
|
|
export interface ThreadsViewSettingsAccess {
|
2026-06-17 04:46:16 +00:00
|
|
|
archiveExportEnabled(): boolean;
|
|
|
|
|
codexPath(): string;
|
|
|
|
|
threadNamingModel(): string | null;
|
|
|
|
|
threadNamingEffort(): ReasoningEffort | null;
|
|
|
|
|
archiveExportSettings(): ArchiveExportSettings;
|
2026-06-17 03:55:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-26 11:23:37 +00:00
|
|
|
export interface ThreadsViewSessionEnvironment {
|
2026-06-13 09:26:15 +00:00
|
|
|
root: HTMLElement;
|
2026-06-26 11:23:37 +00:00
|
|
|
host: ThreadsViewHost;
|
2026-06-13 09:26:15 +00:00
|
|
|
registerPointerDown(handler: (event: PointerEvent) => void): void;
|
2026-06-24 06:13:35 +00:00
|
|
|
archiveDestination(): ArchiveExportDestination;
|
2026-06-21 08:38:55 +00:00
|
|
|
vaultConfigDir(): string;
|
2026-06-13 09:26:15 +00:00
|
|
|
viewWindow(): Window | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ThreadsViewStatus =
|
|
|
|
|
| { kind: "idle" }
|
|
|
|
|
| { kind: "loading"; message: string }
|
|
|
|
|
| { kind: "empty"; message: string }
|
|
|
|
|
| { kind: "log"; message: string }
|
|
|
|
|
| { kind: "error"; message: string };
|
|
|
|
|
|
2026-06-26 11:23:37 +00:00
|
|
|
export class ThreadsViewSession {
|
2026-07-10 03:15:22 +00:00
|
|
|
private readonly lifetime = new OwnerLifetime();
|
2026-06-15 02:32:45 +00:00
|
|
|
private readonly operations: ThreadOperations;
|
|
|
|
|
private readonly titleService: ThreadTitleService;
|
2026-07-15 04:45:00 +00:00
|
|
|
private readonly renderTask: DeferredTask;
|
|
|
|
|
private activeRefresh: object | null = null;
|
2026-06-13 09:26:15 +00:00
|
|
|
private status: ThreadsViewStatus = { kind: "idle" };
|
|
|
|
|
private threads: readonly Thread[] = [];
|
2026-06-21 06:55:02 +00:00
|
|
|
private threadsLoaded = false;
|
2026-06-13 09:26:15 +00:00
|
|
|
private readonly renameStates = new Map<string, ThreadsRenameState>();
|
2026-06-21 04:51:44 +00:00
|
|
|
private nextRenameGenerationToken = 1;
|
2026-06-15 08:44:34 +00:00
|
|
|
private unsubscribeThreads: (() => void) | null = null;
|
2026-06-13 09:26:15 +00:00
|
|
|
private archiveConfirmThreadId: string | null = null;
|
2026-07-14 12:15:21 +00:00
|
|
|
private observedAppServerContext: AppServerQueryContext;
|
|
|
|
|
private operationGeneration = 0;
|
2026-06-13 09:26:15 +00:00
|
|
|
|
2026-06-26 11:23:37 +00:00
|
|
|
constructor(private readonly environment: ThreadsViewSessionEnvironment) {
|
2026-07-14 12:15:21 +00:00
|
|
|
this.observedAppServerContext = this.currentAppServerContext();
|
2026-07-15 04:45:00 +00:00
|
|
|
this.renderTask = new DeferredTask(() => this.viewWindow(), 0);
|
2026-06-16 02:32:07 +00:00
|
|
|
this.operations = createThreadOperations({
|
2026-07-12 09:18:11 +00:00
|
|
|
transport: this.host.threadOperationsTransport,
|
2026-07-13 11:56:18 +00:00
|
|
|
nameMutations: this.host.threadNameMutations,
|
2026-06-17 03:55:54 +00:00
|
|
|
archiveExport: {
|
2026-06-17 04:46:16 +00:00
|
|
|
settings: () => this.host.settings.archiveExportSettings(),
|
|
|
|
|
enabled: () => this.host.settings.archiveExportEnabled(),
|
2026-06-15 02:32:45 +00:00
|
|
|
vaultPath: this.host.vaultPath,
|
2026-06-21 08:38:55 +00:00
|
|
|
vaultConfigDir: this.environment.vaultConfigDir(),
|
2026-06-15 02:32:45 +00:00
|
|
|
},
|
2026-06-24 06:13:35 +00:00
|
|
|
archiveDestination: () => this.environment.archiveDestination(),
|
2026-06-15 06:00:14 +00:00
|
|
|
catalog: this.host.threadCatalog,
|
2026-06-15 02:32:45 +00:00
|
|
|
notice: (message) => {
|
|
|
|
|
new Notice(message);
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-06-16 02:32:07 +00:00
|
|
|
this.titleService = createThreadTitleService({
|
2026-07-12 09:18:11 +00:00
|
|
|
transport: this.host.threadTitleTransport,
|
2026-06-15 02:32:45 +00:00
|
|
|
});
|
2026-06-15 00:30:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:26:15 +00:00
|
|
|
open(): void {
|
2026-07-10 03:15:22 +00:00
|
|
|
this.lifetime.activate();
|
2026-06-13 09:26:15 +00:00
|
|
|
this.environment.registerPointerDown((event) => {
|
|
|
|
|
this.cancelArchiveConfirmOnOutsidePointer(event);
|
|
|
|
|
});
|
2026-06-20 03:10:43 +00:00
|
|
|
const activeThreadsSnapshot = this.host.threadCatalog.activeSnapshot();
|
2026-06-15 08:44:34 +00:00
|
|
|
if (activeThreadsSnapshot) {
|
|
|
|
|
this.threads = activeThreadsSnapshot;
|
2026-06-21 06:55:02 +00:00
|
|
|
this.threadsLoaded = true;
|
2026-06-13 09:26:15 +00:00
|
|
|
}
|
2026-06-20 03:10:43 +00:00
|
|
|
this.unsubscribeThreads = this.host.threadCatalog.observeActive((result) => {
|
2026-06-15 21:46:29 +00:00
|
|
|
this.receiveObservedThreadsResult(result);
|
2026-06-15 08:44:34 +00:00
|
|
|
});
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
void this.refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(): void {
|
2026-07-10 03:15:22 +00:00
|
|
|
this.lifetime.dispose();
|
2026-07-16 12:44:28 +00:00
|
|
|
this.titleService.invalidate();
|
2026-07-15 04:45:00 +00:00
|
|
|
this.activeRefresh = null;
|
|
|
|
|
this.renderTask.clear();
|
2026-06-15 08:44:34 +00:00
|
|
|
this.unsubscribeThreads?.();
|
|
|
|
|
this.unsubscribeThreads = null;
|
2026-06-26 11:23:37 +00:00
|
|
|
unmountThreadsViewShell(this.environment.root);
|
2026-06-13 09:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async refresh(): Promise<void> {
|
2026-07-10 03:15:22 +00:00
|
|
|
const lifetime = this.lifetime.signal();
|
|
|
|
|
if (!this.lifetime.isCurrent(lifetime)) return;
|
2026-06-13 09:26:15 +00:00
|
|
|
const refresh = this.startRefresh();
|
2026-06-26 11:23:37 +00:00
|
|
|
if (!this.currentThreadsSnapshot()) {
|
2026-06-21 06:55:02 +00:00
|
|
|
this.status = { kind: "loading", message: "Loading threads..." };
|
|
|
|
|
}
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
try {
|
2026-06-20 03:10:43 +00:00
|
|
|
const threads = await this.host.threadCatalog.refreshActive();
|
2026-07-10 03:15:22 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime) || this.isStaleRefresh(refresh)) return;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.threads = threads;
|
2026-06-21 06:55:02 +00:00
|
|
|
this.threadsLoaded = true;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.status = threads.length === 0 ? { kind: "empty", message: "No threads" } : { kind: "idle" };
|
|
|
|
|
} catch (error) {
|
2026-07-10 03:15:22 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime)) return;
|
2026-07-16 12:44:28 +00:00
|
|
|
if (isStaleAppServerResourceContextError(error)) return;
|
2026-06-26 11:23:37 +00:00
|
|
|
if (!this.currentThreadsSnapshot()) {
|
2026-06-21 06:55:02 +00:00
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
}
|
2026-06-13 09:26:15 +00:00
|
|
|
} finally {
|
|
|
|
|
this.finishRefresh(refresh);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 03:54:24 +00:00
|
|
|
async loadMore(): Promise<void> {
|
|
|
|
|
const lifetime = this.lifetime.signal();
|
2026-07-15 04:45:00 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime) || !this.host.threadCatalog.hasMoreActive() || this.activeRefresh) return;
|
2026-07-10 03:54:24 +00:00
|
|
|
const refresh = this.startRefresh();
|
|
|
|
|
this.render();
|
|
|
|
|
try {
|
|
|
|
|
const threads = await this.host.threadCatalog.loadMoreActive();
|
|
|
|
|
if (!this.lifetime.isCurrent(lifetime) || this.isStaleRefresh(refresh)) return;
|
|
|
|
|
this.threads = threads;
|
|
|
|
|
this.threadsLoaded = true;
|
|
|
|
|
this.status = threads.length === 0 ? { kind: "empty", message: "No threads" } : { kind: "idle" };
|
|
|
|
|
} catch (error) {
|
2026-07-16 12:44:28 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime) || isStaleAppServerResourceContextError(error)) return;
|
2026-07-10 03:54:24 +00:00
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
} finally {
|
|
|
|
|
this.finishRefresh(refresh);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:26:15 +00:00
|
|
|
refreshLiveState(): void {
|
|
|
|
|
this.scheduleRender();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 12:15:21 +00:00
|
|
|
prepareAppServerContextChange(): void {
|
|
|
|
|
this.operationGeneration += 1;
|
2026-07-16 12:44:28 +00:00
|
|
|
this.titleService.invalidate();
|
2026-07-15 04:45:00 +00:00
|
|
|
this.activeRefresh = null;
|
|
|
|
|
this.renderTask.clear();
|
2026-07-14 12:15:21 +00:00
|
|
|
this.threads = [];
|
|
|
|
|
this.threadsLoaded = false;
|
|
|
|
|
this.renameStates.clear();
|
|
|
|
|
this.archiveConfirmThreadId = null;
|
|
|
|
|
this.status = { kind: "idle" };
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshSettings(): void {
|
|
|
|
|
const nextContext = this.currentAppServerContext();
|
|
|
|
|
if (appServerQueryContextRawEquals(this.observedAppServerContext, nextContext)) {
|
|
|
|
|
this.render();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.observedAppServerContext = nextContext;
|
|
|
|
|
const snapshot = this.host.threadCatalog.activeSnapshot();
|
|
|
|
|
this.threads = snapshot ?? [];
|
|
|
|
|
this.threadsLoaded = snapshot !== null;
|
|
|
|
|
this.status = snapshot?.length === 0 ? { kind: "empty", message: "No threads" } : { kind: "idle" };
|
|
|
|
|
this.render();
|
|
|
|
|
void this.refresh();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 08:44:34 +00:00
|
|
|
private receiveObservedThreads(threads: readonly Thread[]): void {
|
2026-06-13 09:26:15 +00:00
|
|
|
this.threads = threads;
|
2026-06-21 06:55:02 +00:00
|
|
|
this.threadsLoaded = true;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.status = threads.length === 0 ? { kind: "empty", message: "No threads" } : { kind: "idle" };
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 11:23:37 +00:00
|
|
|
private receiveObservedThreadsResult(result: ObservedResult<readonly Thread[]>): void {
|
2026-07-04 07:49:05 +00:00
|
|
|
const observedThreads = result.value;
|
2026-06-26 11:23:37 +00:00
|
|
|
if (observedThreads) {
|
|
|
|
|
this.receiveObservedThreads(observedThreads);
|
2026-06-15 21:46:29 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-06-26 11:23:37 +00:00
|
|
|
const currentValue = this.currentThreadsSnapshot();
|
|
|
|
|
if (observedInitialLoading(result, currentValue)) {
|
2026-06-15 21:46:29 +00:00
|
|
|
this.status = { kind: "loading", message: "Loading threads..." };
|
|
|
|
|
this.render();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-26 11:23:37 +00:00
|
|
|
const initialError = observedInitialError(result, currentValue);
|
2026-06-21 06:55:02 +00:00
|
|
|
if (initialError) {
|
|
|
|
|
this.status = { kind: "error", message: initialError.message };
|
2026-06-15 21:46:29 +00:00
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 11:23:37 +00:00
|
|
|
private currentThreadsSnapshot(): readonly Thread[] | null {
|
2026-06-21 06:55:02 +00:00
|
|
|
return this.threadsLoaded ? this.threads : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 11:23:37 +00:00
|
|
|
private get host(): ThreadsViewHost {
|
2026-06-13 09:26:15 +00:00
|
|
|
return this.environment.host;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 04:45:00 +00:00
|
|
|
private startRefresh(): object {
|
|
|
|
|
const refresh = {};
|
|
|
|
|
this.activeRefresh = refresh;
|
2026-06-13 09:26:15 +00:00
|
|
|
return refresh;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 04:45:00 +00:00
|
|
|
private finishRefresh(refresh: object): void {
|
2026-06-13 09:26:15 +00:00
|
|
|
if (this.isStaleRefresh(refresh)) return;
|
2026-07-15 04:45:00 +00:00
|
|
|
this.activeRefresh = null;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 04:45:00 +00:00
|
|
|
private isStaleRefresh(refresh: object): boolean {
|
|
|
|
|
return this.activeRefresh !== refresh;
|
2026-06-13 09:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private render(): void {
|
2026-07-10 03:15:22 +00:00
|
|
|
if (!this.lifetime.isActive()) return;
|
2026-06-26 11:23:37 +00:00
|
|
|
renderThreadsViewShell(
|
2026-06-13 09:26:15 +00:00
|
|
|
this.environment.root,
|
|
|
|
|
{
|
2026-06-27 04:39:14 +00:00
|
|
|
status: this.status.kind === "idle" ? null : this.status.message,
|
2026-07-15 04:45:00 +00:00
|
|
|
loading: this.activeRefresh !== null,
|
2026-07-10 03:54:24 +00:00
|
|
|
hasMore: this.host.threadCatalog.hasMoreActive(),
|
2026-06-13 09:26:15 +00:00
|
|
|
rows: threadRows(
|
|
|
|
|
this.threads,
|
2026-06-27 11:40:42 +00:00
|
|
|
this.host.openPanelActivities(),
|
2026-06-13 09:26:15 +00:00
|
|
|
this.renameStates,
|
|
|
|
|
this.archiveConfirmThreadId,
|
2026-06-17 04:46:16 +00:00
|
|
|
this.host.settings.archiveExportEnabled(),
|
2026-06-13 09:26:15 +00:00
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
refresh: () => void this.refresh(),
|
2026-07-10 03:54:24 +00:00
|
|
|
loadMore: () => void this.loadMore(),
|
2026-06-13 09:26:15 +00:00
|
|
|
openNewPanel: () => void this.openNewPanel(),
|
|
|
|
|
openThread: (threadId) => void this.openThread(threadId),
|
|
|
|
|
startRename: (threadId, value) => {
|
|
|
|
|
this.startRename(threadId, value);
|
|
|
|
|
},
|
|
|
|
|
updateRename: (threadId, value) => {
|
|
|
|
|
this.updateRename(threadId, value);
|
|
|
|
|
},
|
|
|
|
|
saveRename: (threadId, value) => void this.saveRename(threadId, value),
|
|
|
|
|
cancelRename: (threadId) => {
|
|
|
|
|
this.cancelRename(threadId);
|
|
|
|
|
},
|
|
|
|
|
autoNameThread: (threadId) => void this.autoNameThread(threadId),
|
|
|
|
|
startArchive: (threadId) => {
|
|
|
|
|
this.startArchive(threadId);
|
|
|
|
|
},
|
|
|
|
|
archiveThread: (threadId, saveMarkdown) => void this.archiveThread(threadId, saveMarkdown),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private scheduleRender(): void {
|
2026-07-15 04:45:00 +00:00
|
|
|
this.renderTask.schedule(() => {
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async openThread(threadId: string): Promise<void> {
|
|
|
|
|
this.archiveConfirmThreadId = null;
|
|
|
|
|
await this.host.openThreadInAvailableView(threadId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async openNewPanel(): Promise<void> {
|
|
|
|
|
await this.host.openNewPanel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startRename(threadId: string, value: string): void {
|
|
|
|
|
this.archiveConfirmThreadId = null;
|
2026-06-21 04:51:44 +00:00
|
|
|
this.transitionRenameState(threadId, { type: "started", draft: value });
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateRename(threadId: string, value: string): void {
|
2026-06-21 04:51:44 +00:00
|
|
|
this.transitionRenameState(threadId, { type: "draft-updated", draft: value });
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cancelRename(threadId: string): void {
|
2026-06-21 04:51:44 +00:00
|
|
|
this.transitionRenameState(threadId, { type: "cancelled" });
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async saveRename(threadId: string, value: string): Promise<void> {
|
2026-07-10 03:15:22 +00:00
|
|
|
const lifetime = this.lifetime.signal();
|
2026-07-14 12:15:21 +00:00
|
|
|
const operationGeneration = this.operationGeneration;
|
2026-06-13 09:26:15 +00:00
|
|
|
const editingState = this.renameStates.get(threadId);
|
|
|
|
|
if (!editingState || editingState.kind === "generating") return;
|
|
|
|
|
try {
|
|
|
|
|
if (this.renameStates.get(threadId) !== editingState) return;
|
2026-07-14 12:15:21 +00:00
|
|
|
const operationIsCurrent = () => operationGeneration === this.operationGeneration;
|
|
|
|
|
const result = await this.operations.renameThread(threadId, value, {
|
|
|
|
|
shouldStart: operationIsCurrent,
|
|
|
|
|
shouldPublish: operationIsCurrent,
|
|
|
|
|
});
|
|
|
|
|
if (!operationIsCurrent()) return;
|
2026-07-10 03:15:22 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime) || this.renameStates.get(threadId) !== editingState) return;
|
2026-06-15 05:44:31 +00:00
|
|
|
if (!result) {
|
|
|
|
|
this.cancelRename(threadId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-13 09:26:15 +00:00
|
|
|
this.renameStates.delete(threadId);
|
2026-06-21 08:24:04 +00:00
|
|
|
this.render();
|
2026-06-13 09:26:15 +00:00
|
|
|
} catch (error) {
|
2026-07-10 03:15:22 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime)) return;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async autoNameThread(threadId: string): Promise<void> {
|
2026-07-10 03:15:22 +00:00
|
|
|
const lifetime = this.lifetime.signal();
|
2026-07-14 12:15:21 +00:00
|
|
|
const operationGeneration = this.operationGeneration;
|
2026-06-21 04:51:44 +00:00
|
|
|
const previousState = this.renameStates.get(threadId);
|
2026-07-16 09:50:25 +00:00
|
|
|
const generationToken = this.nextRenameGenerationToken;
|
2026-06-21 04:51:44 +00:00
|
|
|
const generatingState = this.transitionRenameState(threadId, {
|
2026-07-16 09:50:25 +00:00
|
|
|
type: "generation-started",
|
|
|
|
|
generationToken,
|
2026-06-21 04:51:44 +00:00
|
|
|
});
|
|
|
|
|
if (generatingState === previousState || generatingState?.kind !== "generating") return;
|
|
|
|
|
this.nextRenameGenerationToken += 1;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.render();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (this.renameStates.get(threadId) !== generatingState) return;
|
2026-06-15 02:32:45 +00:00
|
|
|
const title = await this.titleService.generateTitle(threadId);
|
2026-07-14 12:15:21 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime) || operationGeneration !== this.operationGeneration) return;
|
2026-07-16 09:50:25 +00:00
|
|
|
this.transitionRenameState(threadId, { type: "generation-succeeded", generationToken, draft: title });
|
2026-06-13 09:26:15 +00:00
|
|
|
} catch (error) {
|
2026-07-14 12:15:21 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime) || operationGeneration !== this.operationGeneration) return;
|
2026-06-13 09:26:15 +00:00
|
|
|
if (this.renameStates.get(threadId) === generatingState) {
|
|
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
2026-07-14 12:15:21 +00:00
|
|
|
if (this.lifetime.isCurrent(lifetime) && operationGeneration === this.operationGeneration) {
|
2026-07-16 09:50:25 +00:00
|
|
|
this.finishAutoNameThread(threadId, generationToken);
|
2026-07-14 12:15:21 +00:00
|
|
|
}
|
2026-06-13 09:26:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startArchive(threadId: string): void {
|
|
|
|
|
this.archiveConfirmThreadId = threadId;
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cancelArchiveConfirmOnOutsidePointer(event: PointerEvent): void {
|
|
|
|
|
if (!this.archiveConfirmThreadId) return;
|
2026-06-27 02:36:00 +00:00
|
|
|
if (isThreadsArchiveConfirmPointer(event, this.environment.root, this.viewWindow())) return;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.archiveConfirmThreadId = null;
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async archiveThread(threadId: string, saveMarkdown: boolean): Promise<void> {
|
2026-07-10 03:15:22 +00:00
|
|
|
const lifetime = this.lifetime.signal();
|
2026-07-14 12:15:21 +00:00
|
|
|
const operationGeneration = this.operationGeneration;
|
2026-06-13 09:26:15 +00:00
|
|
|
try {
|
2026-06-30 02:49:23 +00:00
|
|
|
await this.operations.archiveThread(threadId, {
|
2026-06-14 13:36:50 +00:00
|
|
|
saveMarkdown,
|
2026-07-14 12:15:21 +00:00
|
|
|
shouldPublish: () => operationGeneration === this.operationGeneration,
|
2026-06-14 13:36:50 +00:00
|
|
|
});
|
2026-07-14 12:15:21 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime) || operationGeneration !== this.operationGeneration) return;
|
2026-06-30 03:09:14 +00:00
|
|
|
this.host.closeOpenPanelsForThread(threadId);
|
2026-06-13 09:26:15 +00:00
|
|
|
if (this.archiveConfirmThreadId === threadId) this.archiveConfirmThreadId = null;
|
|
|
|
|
this.renameStates.delete(threadId);
|
|
|
|
|
} catch (error) {
|
2026-07-10 03:15:22 +00:00
|
|
|
if (!this.lifetime.isCurrent(lifetime)) return;
|
2026-06-13 09:26:15 +00:00
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 09:50:25 +00:00
|
|
|
private finishAutoNameThread(threadId: string, generationToken: number): void {
|
2026-06-21 04:51:44 +00:00
|
|
|
const previousState = this.renameStates.get(threadId);
|
2026-07-16 09:50:25 +00:00
|
|
|
const nextState = this.transitionRenameState(threadId, { type: "generation-finished", generationToken });
|
2026-06-21 04:51:44 +00:00
|
|
|
if (nextState !== previousState) this.render();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 09:50:25 +00:00
|
|
|
private transitionRenameState(threadId: string, event: ThreadRenameLifecycleEvent): ThreadsRenameState | undefined {
|
2026-06-21 04:51:44 +00:00
|
|
|
const nextState = transitionThreadsRenameState(this.renameStates.get(threadId), event);
|
|
|
|
|
if (nextState) {
|
|
|
|
|
this.renameStates.set(threadId, nextState);
|
|
|
|
|
} else {
|
|
|
|
|
this.renameStates.delete(threadId);
|
|
|
|
|
}
|
|
|
|
|
return nextState;
|
2026-06-13 09:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private viewWindow(): Window {
|
|
|
|
|
return this.environment.viewWindow() ?? window;
|
|
|
|
|
}
|
2026-07-14 12:15:21 +00:00
|
|
|
|
|
|
|
|
private currentAppServerContext(): AppServerQueryContext {
|
|
|
|
|
return { codexPath: this.host.settings.codexPath(), vaultPath: this.host.vaultPath };
|
|
|
|
|
}
|
2026-06-13 09:26:15 +00:00
|
|
|
}
|