2026-06-13 09:26:15 +00:00
|
|
|
import { Notice } from "obsidian";
|
|
|
|
|
|
2026-06-17 03:55:54 +00:00
|
|
|
import type { AppServerClientAccess } from "../../app-server/connection/client-access";
|
2026-06-15 21:46:29 +00:00
|
|
|
import type { AppServerObservedQueryResult } from "../../app-server/query/cache";
|
2026-06-21 06:55:02 +00:00
|
|
|
import { observedQueryData, observedQueryInitialError, observedQueryInitialLoading } from "../../app-server/query/observed-result";
|
2026-06-16 02:19:45 +00:00
|
|
|
import { isStaleAppServerSharedQueryContextError } from "../../app-server/query/shared-queries";
|
2026-06-17 03:55:54 +00:00
|
|
|
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
2026-06-13 09:26:15 +00:00
|
|
|
import type { Thread } from "../../domain/threads/model";
|
2026-06-17 03:55:54 +00:00
|
|
|
import type { OpenCodexPanelSnapshot } from "../../workspace/panel-coordinator";
|
2026-06-20 22:37:01 +00:00
|
|
|
import type { ThreadCatalogActiveReader, ThreadCatalogThreadManagementEvents } from "../../workspace/thread-catalog";
|
2026-06-17 03:55:54 +00:00
|
|
|
import type { ArchiveExportAdapter, ArchiveExportSettings } from "../../domain/threads/archive-markdown";
|
2026-06-16 02:32:07 +00:00
|
|
|
import { createThreadOperations, type ThreadOperations } from "../threads/thread-operations";
|
|
|
|
|
import { createThreadTitleService, type ThreadTitleService } from "../threads/thread-title-service";
|
2026-06-13 09:26:15 +00:00
|
|
|
import { renderThreadsView, unmountThreadsView } from "./renderer";
|
|
|
|
|
import {
|
|
|
|
|
threadRows,
|
2026-06-21 04:51:44 +00:00
|
|
|
transitionThreadsRenameState,
|
2026-06-13 09:26:15 +00:00
|
|
|
type ThreadsGeneratingRenameState,
|
2026-06-21 04:51:44 +00:00
|
|
|
type ThreadsRenameLifecycleEvent,
|
2026-06-13 09:26:15 +00:00
|
|
|
type ThreadsRenameState,
|
|
|
|
|
} from "./state";
|
|
|
|
|
import {
|
|
|
|
|
createThreadsViewDeferredTasks,
|
|
|
|
|
transitionThreadsViewRefreshLifecycle,
|
|
|
|
|
type ActiveThreadsViewRefresh,
|
|
|
|
|
type ThreadsViewDeferredTasks,
|
|
|
|
|
type ThreadsViewRefreshLifecycleState,
|
|
|
|
|
} from "./view-lifecycle";
|
|
|
|
|
|
|
|
|
|
export interface CodexThreadsHost {
|
2026-06-17 04:46:16 +00:00
|
|
|
readonly settings: CodexThreadsSettingsAccess;
|
2026-06-13 09:26:15 +00:00
|
|
|
readonly vaultPath: string;
|
2026-06-17 03:55:54 +00:00
|
|
|
readonly clientAccess: AppServerClientAccess;
|
2026-06-15 06:00:14 +00:00
|
|
|
readonly threadCatalog: ThreadsThreadCatalog;
|
2026-06-13 09:26:15 +00:00
|
|
|
openNewPanel(): Promise<unknown>;
|
|
|
|
|
openThreadInAvailableView(threadId: string): Promise<void>;
|
|
|
|
|
getOpenPanelSnapshots(): OpenCodexPanelSnapshot[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 22:37:01 +00:00
|
|
|
type ThreadsThreadCatalog = ThreadCatalogActiveReader & ThreadCatalogThreadManagementEvents;
|
2026-06-15 06:00:14 +00:00
|
|
|
|
2026-06-17 04:46:16 +00:00
|
|
|
export interface CodexThreadsSettingsAccess {
|
|
|
|
|
archiveExportEnabled(): boolean;
|
|
|
|
|
codexPath(): string;
|
|
|
|
|
threadNamingModel(): string | null;
|
|
|
|
|
threadNamingEffort(): ReasoningEffort | null;
|
|
|
|
|
archiveExportSettings(): ArchiveExportSettings;
|
2026-06-17 03:55:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:26:15 +00:00
|
|
|
export interface CodexThreadsSessionEnvironment {
|
|
|
|
|
root: HTMLElement;
|
|
|
|
|
host: CodexThreadsHost;
|
|
|
|
|
registerPointerDown(handler: (event: PointerEvent) => void): void;
|
|
|
|
|
archiveAdapter(): ArchiveExportAdapter;
|
|
|
|
|
viewWindow(): Window | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ThreadsViewStatus =
|
|
|
|
|
| { kind: "idle" }
|
|
|
|
|
| { kind: "loading"; message: string }
|
|
|
|
|
| { kind: "empty"; message: string }
|
|
|
|
|
| { kind: "log"; message: string }
|
|
|
|
|
| { kind: "error"; message: string };
|
|
|
|
|
|
|
|
|
|
export class CodexThreadsSession {
|
2026-06-15 02:32:45 +00:00
|
|
|
private readonly operations: ThreadOperations;
|
|
|
|
|
private readonly titleService: ThreadTitleService;
|
2026-06-13 09:26:15 +00:00
|
|
|
private readonly deferredTasks: ThreadsViewDeferredTasks;
|
|
|
|
|
private refreshLifecycle: ThreadsViewRefreshLifecycleState = { kind: "idle" };
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
constructor(private readonly environment: CodexThreadsSessionEnvironment) {
|
|
|
|
|
this.deferredTasks = createThreadsViewDeferredTasks(() => this.viewWindow());
|
2026-06-16 02:32:07 +00:00
|
|
|
this.operations = createThreadOperations({
|
2026-06-17 03:55:54 +00:00
|
|
|
clientAccess: this.host.clientAccess,
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
archiveAdapter: () => this.environment.archiveAdapter(),
|
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-06-17 04:46:16 +00:00
|
|
|
codexPath: () => this.host.settings.codexPath(),
|
2026-06-17 03:55:54 +00:00
|
|
|
vaultPath: this.host.vaultPath,
|
2026-06-17 04:46:16 +00:00
|
|
|
threadNamingModel: () => this.host.settings.threadNamingModel(),
|
|
|
|
|
threadNamingEffort: () => this.host.settings.threadNamingEffort(),
|
2026-06-17 03:55:54 +00:00
|
|
|
clientAccess: this.host.clientAccess,
|
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 {
|
|
|
|
|
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 {
|
|
|
|
|
this.refreshLifecycle = transitionThreadsViewRefreshLifecycle(this.refreshLifecycle, { type: "invalidated" });
|
|
|
|
|
this.deferredTasks.clearAll();
|
2026-06-15 08:44:34 +00:00
|
|
|
this.unsubscribeThreads?.();
|
|
|
|
|
this.unsubscribeThreads = null;
|
2026-06-13 09:26:15 +00:00
|
|
|
unmountThreadsView(this.environment.root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async refresh(): Promise<void> {
|
|
|
|
|
const refresh = this.startRefresh();
|
2026-06-21 06:55:02 +00:00
|
|
|
if (!this.currentThreadsData()) {
|
|
|
|
|
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-06-13 09:26:15 +00:00
|
|
|
if (this.isStaleRefresh(refresh)) return;
|
|
|
|
|
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-06-16 02:19:45 +00:00
|
|
|
if (isStaleAppServerSharedQueryContextError(error)) return;
|
2026-06-21 06:55:02 +00:00
|
|
|
if (!this.currentThreadsData()) {
|
|
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
}
|
2026-06-13 09:26:15 +00:00
|
|
|
} finally {
|
|
|
|
|
this.finishRefresh(refresh);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshLiveState(): void {
|
|
|
|
|
this.scheduleRender();
|
|
|
|
|
}
|
|
|
|
|
|
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-15 21:46:29 +00:00
|
|
|
private receiveObservedThreadsResult(result: AppServerObservedQueryResult<readonly Thread[]>): void {
|
2026-06-21 06:55:02 +00:00
|
|
|
const data = observedQueryData(result);
|
|
|
|
|
if (data) {
|
|
|
|
|
this.receiveObservedThreads(data);
|
2026-06-15 21:46:29 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-06-21 06:55:02 +00:00
|
|
|
const currentData = this.currentThreadsData();
|
|
|
|
|
if (observedQueryInitialLoading(result, currentData)) {
|
2026-06-15 21:46:29 +00:00
|
|
|
this.status = { kind: "loading", message: "Loading threads..." };
|
|
|
|
|
this.render();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-21 06:55:02 +00:00
|
|
|
const initialError = observedQueryInitialError(result, currentData);
|
|
|
|
|
if (initialError) {
|
|
|
|
|
this.status = { kind: "error", message: initialError.message };
|
2026-06-15 21:46:29 +00:00
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 06:55:02 +00:00
|
|
|
private currentThreadsData(): readonly Thread[] | null {
|
|
|
|
|
return this.threadsLoaded ? this.threads : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:26:15 +00:00
|
|
|
private get host(): CodexThreadsHost {
|
|
|
|
|
return this.environment.host;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startRefresh(): ActiveThreadsViewRefresh {
|
|
|
|
|
const refresh: ActiveThreadsViewRefresh = { kind: "loading" };
|
|
|
|
|
this.refreshLifecycle = transitionThreadsViewRefreshLifecycle(this.refreshLifecycle, { type: "started", refresh });
|
|
|
|
|
return refresh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private finishRefresh(refresh: ActiveThreadsViewRefresh): void {
|
|
|
|
|
if (this.isStaleRefresh(refresh)) return;
|
|
|
|
|
this.refreshLifecycle = transitionThreadsViewRefreshLifecycle(this.refreshLifecycle, { type: "finished", refresh });
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isStaleRefresh(refresh: ActiveThreadsViewRefresh): boolean {
|
|
|
|
|
return this.refreshLifecycle !== refresh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private render(): void {
|
|
|
|
|
renderThreadsView(
|
|
|
|
|
this.environment.root,
|
|
|
|
|
{
|
|
|
|
|
status: threadsViewStatusText(this.status),
|
|
|
|
|
loading: this.refreshLifecycle.kind === "loading",
|
|
|
|
|
rows: threadRows(
|
|
|
|
|
this.threads,
|
|
|
|
|
this.host.getOpenPanelSnapshots(),
|
|
|
|
|
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(),
|
|
|
|
|
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 {
|
|
|
|
|
this.deferredTasks.scheduleRender(() => {
|
|
|
|
|
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> {
|
|
|
|
|
const editingState = this.renameStates.get(threadId);
|
|
|
|
|
if (!editingState || editingState.kind === "generating") return;
|
|
|
|
|
try {
|
|
|
|
|
if (this.renameStates.get(threadId) !== editingState) return;
|
2026-06-15 05:44:31 +00:00
|
|
|
const result = await this.operations.renameThread(threadId, value);
|
2026-06-21 08:24:04 +00:00
|
|
|
if (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) {
|
|
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async autoNameThread(threadId: string): Promise<void> {
|
2026-06-21 04:51:44 +00:00
|
|
|
const previousState = this.renameStates.get(threadId);
|
|
|
|
|
const generatingState = this.transitionRenameState(threadId, {
|
|
|
|
|
type: "auto-name-started",
|
|
|
|
|
generationToken: this.nextRenameGenerationToken,
|
|
|
|
|
});
|
|
|
|
|
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-06-21 04:51:44 +00:00
|
|
|
this.transitionRenameState(threadId, { type: "auto-name-generated", generatingState, title });
|
2026-06-13 09:26:15 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
if (this.renameStates.get(threadId) === generatingState) {
|
|
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
this.finishAutoNameThread(threadId, generatingState);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.viewWindow() as Window & { Element: typeof Element };
|
|
|
|
|
if (target instanceof viewWindow.Element) {
|
|
|
|
|
const archiveConfirm = target.closest(".codex-panel-threads__archive-confirm");
|
|
|
|
|
if (archiveConfirm && this.environment.root.contains(archiveConfirm)) return;
|
|
|
|
|
}
|
|
|
|
|
this.archiveConfirmThreadId = null;
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async archiveThread(threadId: string, saveMarkdown: boolean): Promise<void> {
|
|
|
|
|
try {
|
2026-06-15 02:32:45 +00:00
|
|
|
const result = await this.operations.archiveThread(threadId, {
|
2026-06-14 13:36:50 +00:00
|
|
|
saveMarkdown,
|
2026-06-15 02:32:45 +00:00
|
|
|
closeOpenPanels: true,
|
2026-06-14 13:36:50 +00:00
|
|
|
});
|
2026-06-15 02:32:45 +00:00
|
|
|
if (!result) return;
|
2026-06-13 09:26:15 +00:00
|
|
|
if (this.archiveConfirmThreadId === threadId) this.archiveConfirmThreadId = null;
|
|
|
|
|
this.renameStates.delete(threadId);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private finishAutoNameThread(threadId: string, generatingState: ThreadsGeneratingRenameState): void {
|
2026-06-21 04:51:44 +00:00
|
|
|
const previousState = this.renameStates.get(threadId);
|
|
|
|
|
const nextState = this.transitionRenameState(threadId, { type: "auto-name-finished", generatingState });
|
|
|
|
|
if (nextState !== previousState) this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private transitionRenameState(threadId: string, event: ThreadsRenameLifecycleEvent): ThreadsRenameState | undefined {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function threadsViewStatusText(status: ThreadsViewStatus): string | null {
|
|
|
|
|
return status.kind === "idle" ? null : status.message;
|
|
|
|
|
}
|