mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add archive slash command
This commit is contained in:
parent
3abe494149
commit
275ce96405
8 changed files with 92 additions and 4 deletions
|
|
@ -29,5 +29,5 @@ Use this skill for Codex Panel release work. `docs/release.md` is the public pro
|
|||
## Failure Handling
|
||||
|
||||
- If `release:prepare`, `release:check`, or `release:preflight` fails, fix the cause and rerun the explicit command.
|
||||
- If the tag-triggered workflow fails before GitHub Release creation, fix the release commit, move the local tag with `git tag -f X.Y.Z`, and force-update the remote tag.
|
||||
- If the tag-triggered workflow fails before GitHub Release creation, fix the release commit, move the local tag with `jj tag set --allow-move -r main X.Y.Z`, and force-update the remote tag.
|
||||
- If a GitHub Release already exists or was partially created, inspect the state before taking action; do not assume local asset upload is the right recovery path.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Use `Codex Panel: New chat` to start a fresh thread. Use `Codex Panel: Open new
|
|||
|
||||
Codex Panel supports the app-server-backed Codex workflows that fit a persistent side panel:
|
||||
|
||||
- Start, resume, rename, fork, roll back, compact, and archive threads (`/new`, `/resume`, `/fork`, `/rollback`, `/compact`).
|
||||
- Start, resume, rename, fork, roll back, compact, and archive threads (`/new`, `/resume`, `/fork`, `/rollback`, `/compact`, `/archive`).
|
||||
- Complete slash commands (`/help`) and enabled Codex skills from the composer.
|
||||
- Reference another non-archived thread without switching away from the current one (`/refer <thread> <message>`). Codex receives up to 20 recent turns, limited to user input and final Codex responses.
|
||||
- Toggle Plan mode, optionally sending the same message after switching (`/plan <message>`), then answer questions and copy or implement proposed plans.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const SLASH_COMMANDS = [
|
|||
{ command: "/fork", detail: "Fork the active Codex thread." },
|
||||
{ command: "/rollback", detail: "Drop the latest turn and restore its prompt to the composer." },
|
||||
{ command: "/compact", detail: "Compact the current conversation context." },
|
||||
{ command: "/archive", detail: "Archive the active or selected Codex thread." },
|
||||
{ command: "/fast", detail: "Toggle fast service tier for subsequent turns." },
|
||||
{ command: "/plan", detail: "Toggle Plan mode, optionally sending a message." },
|
||||
{ command: "/status", detail: "Show current session, context, and usage limits." },
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export function activeSlashCommandSuggestions(beforeCursor: string): ComposerSug
|
|||
}
|
||||
|
||||
export function activeThreadCommandSuggestions(beforeCursor: string, threads: Thread[]): ComposerSuggestion[] | null {
|
||||
const match = beforeCursor.match(/(?:^|\n)\/(?:resume|refer)\s+([^\s\n]{0,120})$/);
|
||||
const match = beforeCursor.match(/(?:^|\n)\/(?:resume|refer|archive)\s+([^\s\n]{0,120})$/);
|
||||
if (!match || match.index === undefined) return null;
|
||||
|
||||
const rawQuery = match[1] ?? "";
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export interface SlashCommandExecutionContext {
|
|||
forkThread: (threadId: string) => Promise<void>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
compactThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string) => Promise<void>;
|
||||
toggleFastMode: () => void;
|
||||
toggleCollaborationMode: () => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
|
|
@ -131,6 +132,29 @@ export async function executeSlashCommand(
|
|||
return;
|
||||
}
|
||||
|
||||
if (command === "archive") {
|
||||
if (context.busy) {
|
||||
context.addSystemMessage("Finish or interrupt the current turn before archiving threads.");
|
||||
return;
|
||||
}
|
||||
if (!args) {
|
||||
if (!context.activeThreadId) {
|
||||
context.addSystemMessage("No active thread to archive.");
|
||||
return;
|
||||
}
|
||||
await context.archiveThread(context.activeThreadId);
|
||||
return;
|
||||
}
|
||||
|
||||
const thread = resolveThreadArgument(args, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
return;
|
||||
}
|
||||
await context.archiveThread(thread.thread.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "fast") {
|
||||
context.toggleFastMode();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -544,6 +544,7 @@ export class CodexPanelView extends ItemView {
|
|||
compactThread: async (threadId) => {
|
||||
await this.client?.compactThread(threadId);
|
||||
},
|
||||
archiveThread: (threadId) => this.archiveThread(threadId),
|
||||
busy: this.state.busy,
|
||||
toggleFastMode: () => this.toggleFastMode(),
|
||||
toggleCollaborationMode: () => this.toggleCollaborationMode(),
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ describe("composer suggestions", () => {
|
|||
expect(parseSlashCommand("/resume thread-1")).toEqual({ command: "resume", args: "thread-1" });
|
||||
expect(parseSlashCommand("/refer thread-1 続きです")).toEqual({ command: "refer", args: "thread-1 続きです" });
|
||||
expect(parseSlashCommand("/fork")).toEqual({ command: "fork", args: "" });
|
||||
expect(parseSlashCommand("/archive thread-1")).toEqual({ command: "archive", args: "thread-1" });
|
||||
expect(parseSlashCommand("/doctor")).toEqual({ command: "doctor", args: "" });
|
||||
expect(parseSlashCommand("/fast now")).toEqual({ command: "fast", args: "now" });
|
||||
expect(parseSlashCommand("/plan")).toEqual({ command: "plan", args: "" });
|
||||
|
|
@ -100,7 +101,7 @@ describe("composer suggestions", () => {
|
|||
expect(activeComposerSuggestions("/help", notes, [])).toEqual([]);
|
||||
});
|
||||
|
||||
it("suggests recent threads for /resume and /refer arguments", () => {
|
||||
it("suggests recent threads for /resume, /refer, and /archive arguments", () => {
|
||||
const threads = [
|
||||
thread({ id: "019abcde-0000-7000-8000-000000000001", name: "Codex Panel実装" }),
|
||||
thread({ id: "019abcde-0000-7000-8000-000000000002", name: "別件" }),
|
||||
|
|
@ -129,6 +130,13 @@ describe("composer suggestions", () => {
|
|||
appendSpaceOnInsert: true,
|
||||
});
|
||||
expect(activeComposerSuggestions("/refer 019abcde-0000-7000-8000-000000000001 ", notes, [], threads)).toEqual([]);
|
||||
expect(activeComposerSuggestions("/archive codex", notes, [], threads)[0]).toMatchObject({
|
||||
display: "Codex Panel実装",
|
||||
detail: "019abcde",
|
||||
replacement: "019abcde-0000-7000-8000-000000000001",
|
||||
appendSpaceOnInsert: true,
|
||||
});
|
||||
expect(activeComposerSuggestions("/archive 019abcde-0000-7000-8000-000000000001 ", notes, [], threads)).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not suggest threads for /fork arguments", () => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCo
|
|||
forkThread: vi.fn().mockResolvedValue(undefined),
|
||||
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
||||
compactThread: vi.fn().mockResolvedValue(undefined),
|
||||
archiveThread: vi.fn().mockResolvedValue(undefined),
|
||||
toggleFastMode: vi.fn(),
|
||||
toggleCollaborationMode: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
|
|
@ -222,6 +223,59 @@ describe("slash commands", () => {
|
|||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Compaction requested.");
|
||||
});
|
||||
|
||||
it("archives the active thread for bare /archive", async () => {
|
||||
const ctx = context({ activeThreadId: "active-thread" });
|
||||
|
||||
await executeSlashCommand("archive", "", ctx);
|
||||
|
||||
expect(ctx.archiveThread).toHaveBeenCalledWith("active-thread");
|
||||
});
|
||||
|
||||
it("archives a selected thread by id argument", async () => {
|
||||
const ctx = context({
|
||||
listedThreads: [thread({ id: "thread-alpha", name: "Alpha" }), thread({ id: "thread-beta", name: "Beta" })],
|
||||
});
|
||||
|
||||
await executeSlashCommand("archive", "thread-beta", ctx);
|
||||
|
||||
expect(ctx.archiveThread).toHaveBeenCalledWith("thread-beta");
|
||||
});
|
||||
|
||||
it("rejects /archive without an active thread", async () => {
|
||||
const ctx = context({ activeThreadId: null });
|
||||
|
||||
await executeSlashCommand("archive", "", ctx);
|
||||
|
||||
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("No active thread to archive.");
|
||||
});
|
||||
|
||||
it("rejects /archive while a turn is running", async () => {
|
||||
const ctx = context({ busy: true });
|
||||
|
||||
await executeSlashCommand("archive", "thread-1", ctx);
|
||||
|
||||
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Finish or interrupt the current turn before archiving threads.");
|
||||
});
|
||||
|
||||
it("reports ambiguous archive matches", async () => {
|
||||
const ctx = context({
|
||||
listedThreads: [thread({ id: "thread-alpha", name: "Draft" }), thread({ id: "thread-beta", name: "Draft notes" })],
|
||||
});
|
||||
|
||||
await executeSlashCommand("archive", "Draft", ctx);
|
||||
|
||||
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Multiple matching threads: Draft, Draft notes");
|
||||
});
|
||||
|
||||
it("documents archive", () => {
|
||||
expect(slashCommandHelpLines().find((line) => line.startsWith("/archive"))).toBe(
|
||||
"/archive - Archive the active or selected Codex thread.",
|
||||
);
|
||||
});
|
||||
|
||||
it("documents that /plan can take a message", () => {
|
||||
expect(slashCommandHelpLines().find((line) => line.startsWith("/plan"))).toBe(
|
||||
"/plan - Toggle Plan mode, optionally sending a message.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue