mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Extract chat thread actions
This commit is contained in:
parent
bc0e642f5c
commit
bf43f15c09
3 changed files with 168 additions and 97 deletions
125
src/features/chat/thread-actions.ts
Normal file
125
src/features/chat/thread-actions.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { Notice } from "obsidian";
|
||||
|
||||
import type { AppServerClient } from "../../app-server/client";
|
||||
import { exportArchivedThreadMarkdown } from "../../domain/threads/export";
|
||||
import { inheritedForkThreadName, upsertThread } from "../../domain/threads/model";
|
||||
import type { CodexPanelSettings } from "../../settings/model";
|
||||
import type { ArchiveExportAdapter } from "../../domain/threads/export";
|
||||
import type { ChatState } from "./chat-state";
|
||||
import type { ThreadHistoryLoader } from "./thread-history";
|
||||
import { rollbackCandidateFromItems } from "./rollback";
|
||||
|
||||
export interface ChatThreadActionControllerHost {
|
||||
state: ChatState;
|
||||
vaultPath: string;
|
||||
settings: () => CodexPanelSettings;
|
||||
archiveAdapter: () => ArchiveExportAdapter;
|
||||
ensureConnected: () => Promise<void>;
|
||||
currentClient: () => AppServerClient | null;
|
||||
history: ThreadHistoryLoader;
|
||||
addSystemMessage: (text: string) => void;
|
||||
setStatus: (status: string) => void;
|
||||
setComposerText: (text: string) => void;
|
||||
openThreadInNewView: (threadId: string) => Promise<unknown>;
|
||||
notifyThreadArchived: (threadId: string) => void;
|
||||
notifyThreadRenamed: (threadId: string, name: string) => void;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSharedThreadListFromOpenSurface: () => void;
|
||||
}
|
||||
|
||||
export class ChatThreadActionController {
|
||||
constructor(private readonly host: ChatThreadActionControllerHost) {}
|
||||
|
||||
async archiveThread(threadId: string): Promise<void> {
|
||||
if (this.host.state.busy) {
|
||||
this.host.addSystemMessage("Finish or interrupt the current turn before archiving threads.");
|
||||
return;
|
||||
}
|
||||
const client = this.host.currentClient();
|
||||
if (!client) return;
|
||||
try {
|
||||
const settings = this.host.settings();
|
||||
if (settings.archiveExportEnabled) {
|
||||
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}.`);
|
||||
}
|
||||
await client.archiveThread(threadId);
|
||||
this.host.notifyThreadArchived(threadId);
|
||||
} catch (error) {
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async forkThread(threadId: string): Promise<void> {
|
||||
if (this.host.state.busy) {
|
||||
this.host.addSystemMessage("Finish or interrupt the current turn before forking threads.");
|
||||
return;
|
||||
}
|
||||
await this.host.ensureConnected();
|
||||
const client = this.host.currentClient();
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
const sourceName = inheritedForkThreadName(threadId, this.host.state.listedThreads);
|
||||
const response = await client.forkThread(threadId, this.host.vaultPath);
|
||||
const forkedThreadId = response.thread.id;
|
||||
if (sourceName) {
|
||||
try {
|
||||
await client.setThreadName(forkedThreadId, sourceName);
|
||||
this.host.notifyThreadRenamed(forkedThreadId, sourceName);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not copy the source thread name: ${message}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.host.openThreadInNewView(forkedThreadId);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not open it in a new panel: ${message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async rollbackThread(threadId: string): Promise<void> {
|
||||
if (this.host.state.busy) {
|
||||
this.host.addSystemMessage("Interrupt the current turn before rolling back.");
|
||||
return;
|
||||
}
|
||||
await this.host.ensureConnected();
|
||||
const client = this.host.currentClient();
|
||||
if (!client) return;
|
||||
|
||||
const candidate = rollbackCandidateFromItems(this.host.state.displayItems);
|
||||
if (!candidate) {
|
||||
this.host.addSystemMessage("No completed turn to roll back.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.host.setStatus("Rolling back latest turn...");
|
||||
const response = await client.rollbackThread(threadId);
|
||||
this.host.state.activeThreadId = response.thread.id;
|
||||
this.host.state.activeThreadCwd = response.thread.cwd;
|
||||
this.host.state.activeTurnId = null;
|
||||
this.host.state.tokenUsage = null;
|
||||
this.host.state.historyCursor = null;
|
||||
this.host.state.turnDiffs.clear();
|
||||
this.host.state.listedThreads = upsertThread(this.host.state.listedThreads, response.thread);
|
||||
await this.host.history.loadLatest(response.thread.id);
|
||||
this.host.setComposerText(candidate.text);
|
||||
this.host.addSystemMessage("Rolled back the latest turn. Local file changes were not reverted.");
|
||||
this.host.setStatus("Rolled back latest turn.");
|
||||
this.host.notifyActiveThreadIdentityChanged();
|
||||
await this.host.refreshThreads();
|
||||
this.host.refreshSharedThreadListFromOpenSurface();
|
||||
} catch (error) {
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.host.setStatus("Rollback failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +172,9 @@ export class ThreadRenameController {
|
|||
threadId,
|
||||
readTurns: (id, cursor, limit, sortDirection) => client.threadTurnsList(id, cursor, limit, sortDirection),
|
||||
});
|
||||
return context ?? (this.host.state.activeThreadId === threadId ? firstNamingContextFromDisplayItems(this.host.state.displayItems) : null);
|
||||
return (
|
||||
context ?? (this.host.state.activeThreadId === threadId ? firstNamingContextFromDisplayItems(this.host.state.displayItems) : null)
|
||||
);
|
||||
}
|
||||
|
||||
private async generateTitle(context: ThreadNamingContext): Promise<string | null> {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {
|
|||
} from "../../runtime/collaboration-mode";
|
||||
import { ChatController } from "./chat-controller";
|
||||
import { connectionDiagnosticSections, diagnosticAlertLevel } from "./diagnostics";
|
||||
import { rollbackCandidateFromItems } from "./rollback";
|
||||
import { contextSummary, effectiveConfigSections, rateLimitSummary } from "../../runtime/view";
|
||||
import {
|
||||
autoReviewActive,
|
||||
|
|
@ -58,8 +57,7 @@ import { questionDefaultAnswer, type PendingUserInput } from "./user-input/model
|
|||
import { ChatComposerController } from "./chat-composer-controller";
|
||||
import { attachHookRunsToTurn } from "./hook-display";
|
||||
import { clearActiveThreadState, clearConnectionScopedState, createChatState, type ChatState } from "./chat-state";
|
||||
import { codexPanelDisplayTitle, getThreadTitle, inheritedForkThreadName, upsertThread } from "../../domain/threads/model";
|
||||
import { exportArchivedThreadMarkdown } from "../../domain/threads/export";
|
||||
import { codexPanelDisplayTitle, getThreadTitle, upsertThread } from "../../domain/threads/model";
|
||||
import {
|
||||
referencedThreadDisplay,
|
||||
referencedThreadPrompt,
|
||||
|
|
@ -74,6 +72,7 @@ import type { ChatTurnDiffViewState } from "./ui/turn-diff";
|
|||
import { ChatMessageRenderer, type ChatMessageScrollIntent } from "./chat-message-renderer";
|
||||
import type { OpenCodexPanelSnapshot } from "../../runtime/open-panel-snapshot";
|
||||
import type { SharedSessionMetadata } from "../../runtime/shared-app-server-state";
|
||||
import { ChatThreadActionController } from "./thread-actions";
|
||||
|
||||
export interface CodexChatHost {
|
||||
readonly settings: CodexPanelSettings;
|
||||
|
|
@ -103,6 +102,7 @@ export class CodexChatView extends ItemView {
|
|||
private readonly controller: ChatController;
|
||||
private readonly session: ChatSessionController;
|
||||
private readonly history: ThreadHistoryLoader;
|
||||
private readonly threadActions: ChatThreadActionController;
|
||||
private readonly threadRename: ThreadRenameController;
|
||||
private readonly state: ChatState = createChatState();
|
||||
private readonly viewId = `codex-panel-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
|
|
@ -144,7 +144,7 @@ export class CodexChatView extends ItemView {
|
|||
return value;
|
||||
},
|
||||
loadOlderTurns: () => void this.history.loadOlder(),
|
||||
rollbackThread: (threadId) => void this.rollbackThread(threadId),
|
||||
rollbackThread: (threadId) => void this.threadActions.rollbackThread(threadId),
|
||||
implementPlan: (item) => void this.implementPlan(item),
|
||||
openTurnDiff: (state) => void this.plugin.openTurnDiff(state),
|
||||
pendingRequestsSignature: () => this.pendingRequestsSignature(),
|
||||
|
|
@ -243,6 +243,38 @@ export class CodexChatView extends ItemView {
|
|||
this.threadRename.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
});
|
||||
this.threadActions = new ChatThreadActionController({
|
||||
state: this.state,
|
||||
vaultPath: this.plugin.vaultPath,
|
||||
settings: () => this.plugin.settings,
|
||||
archiveAdapter: () => this.app.vault.adapter,
|
||||
ensureConnected: () => this.ensureConnected(),
|
||||
currentClient: () => this.client,
|
||||
history: this.history,
|
||||
addSystemMessage: (text) => {
|
||||
this.addSystemMessage(text);
|
||||
},
|
||||
setStatus: (status) => {
|
||||
this.setStatus(status);
|
||||
},
|
||||
setComposerText: (text) => {
|
||||
this.setComposerText(text);
|
||||
},
|
||||
openThreadInNewView: (threadId) => this.plugin.openThreadInNewView(threadId),
|
||||
notifyThreadArchived: (threadId) => {
|
||||
this.plugin.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
this.plugin.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
notifyActiveThreadIdentityChanged: () => {
|
||||
this.notifyActiveThreadIdentityChanged();
|
||||
},
|
||||
refreshThreads: () => this.refreshThreads(),
|
||||
refreshSharedThreadListFromOpenSurface: () => {
|
||||
this.plugin.refreshSharedThreadListFromOpenSurface();
|
||||
},
|
||||
});
|
||||
this.threadRename = new ThreadRenameController({
|
||||
state: this.state,
|
||||
vaultPath: this.plugin.vaultPath,
|
||||
|
|
@ -777,12 +809,12 @@ export class CodexChatView extends ItemView {
|
|||
startNewThread: () => this.startNewThread(),
|
||||
resumeThread: (threadId) => this.selectThread(threadId),
|
||||
referThread: (thread, message) => this.referencedThreadInput(thread, message),
|
||||
forkThread: (threadId) => this.forkThread(threadId),
|
||||
rollbackThread: (threadId) => this.rollbackThread(threadId),
|
||||
forkThread: (threadId) => this.threadActions.forkThread(threadId),
|
||||
rollbackThread: (threadId) => this.threadActions.rollbackThread(threadId),
|
||||
compactThread: async (threadId) => {
|
||||
await this.client?.compactThread(threadId);
|
||||
},
|
||||
archiveThread: (threadId) => this.archiveThread(threadId),
|
||||
archiveThread: (threadId) => this.threadActions.archiveThread(threadId),
|
||||
busy: this.state.busy,
|
||||
toggleFastMode: () => this.toggleFastMode(),
|
||||
toggleCollaborationMode: () => this.toggleCollaborationMode(),
|
||||
|
|
@ -1183,7 +1215,7 @@ export class CodexChatView extends ItemView {
|
|||
this.state.openDetails.delete("history");
|
||||
void this.selectThread(threadId);
|
||||
},
|
||||
archiveThread: (threadId) => void this.archiveThread(threadId),
|
||||
archiveThread: (threadId) => void this.threadActions.archiveThread(threadId),
|
||||
startRenameThread: (threadId) => {
|
||||
this.threadRename.start(threadId);
|
||||
},
|
||||
|
|
@ -1519,94 +1551,6 @@ export class CodexChatView extends ItemView {
|
|||
);
|
||||
}
|
||||
|
||||
private async archiveThread(threadId: string): Promise<void> {
|
||||
if (this.state.busy) {
|
||||
this.addSystemMessage("Finish or interrupt the current turn before archiving threads.");
|
||||
return;
|
||||
}
|
||||
if (!this.client) return;
|
||||
try {
|
||||
if (this.plugin.settings.archiveExportEnabled) {
|
||||
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);
|
||||
this.plugin.notifyThreadArchived(threadId);
|
||||
} catch (error) {
|
||||
this.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
private async forkThread(threadId: string): Promise<void> {
|
||||
if (this.state.busy) {
|
||||
this.addSystemMessage("Finish or interrupt the current turn before forking threads.");
|
||||
return;
|
||||
}
|
||||
await this.ensureConnected();
|
||||
if (!this.client) return;
|
||||
|
||||
try {
|
||||
const sourceName = inheritedForkThreadName(threadId, this.state.listedThreads);
|
||||
const response = await this.client.forkThread(threadId, this.plugin.vaultPath);
|
||||
const forkedThreadId = response.thread.id;
|
||||
if (sourceName) {
|
||||
try {
|
||||
await this.client.setThreadName(forkedThreadId, sourceName);
|
||||
this.plugin.notifyThreadRenamed(forkedThreadId, sourceName);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.addSystemMessage(`Forked thread ${forkedThreadId}, but could not copy the source thread name: ${message}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.plugin.openThreadInNewView(forkedThreadId);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.addSystemMessage(`Forked thread ${forkedThreadId}, but could not open it in a new panel: ${message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
private async rollbackThread(threadId: string): Promise<void> {
|
||||
if (this.state.busy) {
|
||||
this.addSystemMessage("Interrupt the current turn before rolling back.");
|
||||
return;
|
||||
}
|
||||
await this.ensureConnected();
|
||||
if (!this.client) return;
|
||||
|
||||
const candidate = rollbackCandidateFromItems(this.state.displayItems);
|
||||
if (!candidate) {
|
||||
this.addSystemMessage("No completed turn to roll back.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.setStatus("Rolling back latest turn...");
|
||||
const response = await this.client.rollbackThread(threadId);
|
||||
this.state.activeThreadId = response.thread.id;
|
||||
this.state.activeThreadCwd = response.thread.cwd;
|
||||
this.state.activeTurnId = null;
|
||||
this.state.tokenUsage = null;
|
||||
this.state.historyCursor = null;
|
||||
this.state.turnDiffs.clear();
|
||||
this.state.listedThreads = upsertThread(this.state.listedThreads, response.thread);
|
||||
await this.history.loadLatest(response.thread.id);
|
||||
this.setComposerText(candidate.text);
|
||||
this.addSystemMessage("Rolled back the latest turn. Local file changes were not reverted.");
|
||||
this.setStatus("Rolled back latest turn.");
|
||||
this.notifyActiveThreadIdentityChanged();
|
||||
await this.refreshThreads();
|
||||
this.plugin.refreshSharedThreadListFromOpenSurface();
|
||||
} catch (error) {
|
||||
this.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.setStatus("Rollback failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private queueMessagesBottomScroll(): void {
|
||||
this.state.messagesPinnedToBottom = true;
|
||||
this.nextMessageScrollIntent = "force-bottom";
|
||||
|
|
|
|||
Loading…
Reference in a new issue