Add reconnect slash command

This commit is contained in:
murashit 2026-06-05 13:41:14 +09:00
parent 6db8038649
commit 127d72b77a
10 changed files with 48 additions and 8 deletions

View file

@ -38,7 +38,7 @@ Use **Codex Panel: Open thread...** to search non-archived threads from a picker
Codex Panel supports Codex App Server workflows that fit a persistent panel in Obsidian:
- Manage thread history from the panel or slash commands: clear (`/clear`), resume (`/resume`), rename (`/rename`), auto-name, fork (`/fork`), roll back (`/rollback`), compact (`/compact`), and archive (`/archive`) Codex threads.
- Manage thread history from slash commands or the panel: clear (`/clear`), resume (`/resume`), reconnect (`/reconnect`), rename (`/rename`), auto-name, fork (`/fork`), roll back (`/rollback`), compact (`/compact`), and archive (`/archive`) Codex threads.
- Compose with Codex-aware completions for slash commands, enabled skills (`$skill-name`), recent threads, models, and supported reasoning efforts; use `/help` to show available slash commands.
- Reference another non-archived thread without switching away from the current one (`/refer`).
- Control subsequent turns from slash commands, with the same state visible and lightly adjustable in the composer status row: Plan mode (`/plan`), fast mode (`/fast`), approval auto-review (`/auto-review`), model (`/model`), and reasoning effort (`/reasoning`).

View file

@ -209,6 +209,7 @@ export function createChatViewControllerAssembly(host: ChatViewControllerAssembl
rollbackThread: (threadId) => threadActions.rollbackThread(threadId),
archiveThread: (threadId) => threadActions.archiveThread(threadId),
renameThread: (threadId, name) => threadRename.rename(threadId, name),
reconnect: () => reconnectActions.reconnectPanel(),
},
runtime: {
toggleFastMode: () => runtimeSettings.toggleFastMode(),

View file

@ -31,6 +31,13 @@ export const SLASH_COMMANDS = [
surface: "panelAction",
detail: "Resume a recent Codex thread.",
},
{
command: "/reconnect",
usage: "/reconnect",
argsKind: "none",
surface: "panelAction",
detail: "Reconnect to Codex app-server and resume the active thread.",
},
{
command: "/refer",
usage: "/refer <thread> <message>",

View file

@ -19,7 +19,7 @@ export interface ChatReconnectControllerHost {
export class ChatReconnectController {
constructor(private readonly host: ChatReconnectControllerHost) {}
async reconnectFromToolbar(): Promise<void> {
async reconnectPanel(): Promise<void> {
const threadId = this.host.threadState.activeThreadId();
this.host.panelState.closePanels();
this.host.invalidateConnectionWork();

View file

@ -19,6 +19,7 @@ export interface SlashCommandThreadPort {
rollbackThread: (threadId: string) => Promise<void>;
archiveThread: (threadId: string) => Promise<void>;
renameThread: (threadId: string, name: string) => Promise<void>;
reconnect: () => Promise<void>;
}
export interface SlashCommandRuntimePort {
@ -53,18 +54,23 @@ export class SlashCommandController {
constructor(private readonly host: SlashCommandControllerHost) {}
async execute(command: SlashCommandName, args: string): Promise<SlashCommandExecutionResult | undefined> {
const client = this.host.currentClient();
if (!client) return;
const state = this.host.state.snapshot();
const client = this.host.currentClient();
if (!client && command !== "reconnect") return;
return runSlashCommand(command, args, {
activeThreadId: state.activeThreadId,
listedThreads: state.listedThreads,
startNewThread: () => this.host.threads.startNewThread(),
resumeThread: (threadId) => this.host.threads.resumeThread(threadId),
referThread: (thread, message) => this.referencedThreadInput(client, thread, message),
reconnect: () => this.host.threads.reconnect(),
referThread: (thread, message) => {
if (!client) return Promise.resolve(null);
return this.referencedThreadInput(client, thread, message);
},
forkThread: (threadId) => this.host.threads.forkThread(threadId),
rollbackThread: (threadId) => this.host.threads.rollbackThread(threadId),
compactThread: async (threadId) => {
if (!client) return;
await client.compactThread(threadId);
},
archiveThread: (threadId) => this.host.threads.archiveThread(threadId),

View file

@ -24,6 +24,7 @@ export interface SlashCommandExecutionContext {
compactThread: (threadId: string) => Promise<void>;
archiveThread: (threadId: string, saveMarkdown?: boolean) => Promise<void>;
renameThread: (threadId: string, name: string) => Promise<void>;
reconnect: () => Promise<void>;
toggleFastMode: () => void | Promise<void>;
toggleCollaborationMode: () => void | Promise<void>;
toggleAutoReview: () => void | Promise<void>;
@ -76,6 +77,11 @@ export async function executeSlashCommand(
return;
}
if (command === "reconnect") {
await context.reconnect();
return;
}
if (command === "refer") {
const parsed = parseReferArgs(args);
if (!parsed) {

View file

@ -590,7 +590,7 @@ export class CodexChatView extends ItemView {
toggleStatusPanel: () => {
this.toolbarPanels.toggleStatus();
},
connect: () => void this.reconnectActions.reconnectFromToolbar(),
connect: () => void this.reconnectActions.reconnectPanel(),
refreshStatus: () => void this.refreshStatusPanel(),
resumeThread: (threadId) => void this.threadSelection.selectThreadFromToolbar(threadId),
startArchiveThread: (threadId) => {

View file

@ -20,6 +20,7 @@ function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCo
compactThread: vi.fn().mockResolvedValue(undefined),
archiveThread: vi.fn().mockResolvedValue(undefined),
renameThread: vi.fn().mockResolvedValue(undefined),
reconnect: vi.fn().mockResolvedValue(undefined),
toggleFastMode: vi.fn(),
toggleCollaborationMode: vi.fn(),
toggleAutoReview: vi.fn(),
@ -91,6 +92,14 @@ describe("slash commands", () => {
expect(ctx.resumeThread).toHaveBeenCalledWith("latest");
});
it("reconnects the panel for /reconnect", async () => {
const ctx = context();
await executeSlashCommand("reconnect", "", ctx);
expect(ctx.reconnect).toHaveBeenCalledOnce();
});
it("resumes a thread by id argument", async () => {
const ctx = context({
listedThreads: [thread({ id: "thread-alpha", name: "Alpha" }), thread({ id: "thread-beta", name: "Beta" })],
@ -363,6 +372,7 @@ describe("slash commands", () => {
title: "Panel actions",
rows: expect.arrayContaining([
{ key: "/clear", value: "Clear the current panel and start a fresh Codex thread." },
{ key: "/reconnect", value: "Reconnect to Codex app-server and resume the active thread." },
{ key: "/archive <thread>", value: "Archive the selected Codex thread." },
{ key: "/rename <thread> <name>", value: "Rename the selected Codex thread." },
]),

View file

@ -49,7 +49,7 @@ describe("ChatReconnectController", () => {
const { host, stateStore } = createHost();
const controller = new ChatReconnectController(host);
await controller.reconnectFromToolbar();
await controller.reconnectPanel();
expect(stateStore.getState().openDetails.size).toBe(0);
expect(host.invalidateConnectionWork).toHaveBeenCalledOnce();
@ -69,7 +69,7 @@ describe("ChatReconnectController", () => {
});
const controller = new ChatReconnectController(host);
await controller.reconnectFromToolbar();
await controller.reconnectPanel();
expect(host.addSystemMessage).toHaveBeenCalledWith("resume failed");
});

View file

@ -59,6 +59,7 @@ function createHost(overrides: SlashCommandHostOverrides = {}) {
rollbackThread: vi.fn().mockResolvedValue(undefined),
archiveThread: vi.fn().mockResolvedValue(undefined),
renameThread: vi.fn().mockResolvedValue(undefined),
reconnect: vi.fn().mockResolvedValue(undefined),
...threadOverrides,
};
const runtime: SlashCommandRuntimePort = {
@ -128,6 +129,15 @@ describe("SlashCommandController", () => {
expect(host.status.setStatus).toHaveBeenCalledWith("Compaction requested.");
});
it("runs reconnect even when there is no current app-server client", async () => {
const { host } = createHost({ currentClient: () => null });
const controller = new SlashCommandController(host);
await controller.execute("reconnect", "");
expect(host.threads.reconnect).toHaveBeenCalledOnce();
});
it("reports unreadable referenced threads", async () => {
const { host, stateStore, threadTurnsList } = createHost();
stateStore.dispatch({