mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Move action invariants behind action APIs
This commit is contained in:
parent
dbc00031b0
commit
3b48ab0b9b
4 changed files with 27 additions and 36 deletions
|
|
@ -58,7 +58,6 @@ export interface SlashCommandExecutionPorts {
|
|||
|
||||
export interface SlashCommandExecutionContext extends SlashCommandExecutionPorts {
|
||||
activeThreadId: string | null;
|
||||
busy: boolean;
|
||||
listedThreads: readonly Thread[];
|
||||
referThread: (thread: Thread, message: string) => Promise<ThreadReferenceInput | null>;
|
||||
supportedReasoningEfforts: () => readonly ReasoningEffort[];
|
||||
|
|
@ -88,18 +87,10 @@ function noActiveThreadToRollbackMessage(): string {
|
|||
return "No active thread to roll back.";
|
||||
}
|
||||
|
||||
function interruptBeforeRollbackMessage(): string {
|
||||
return "Interrupt the current turn before rolling back.";
|
||||
}
|
||||
|
||||
function noActiveThreadToCompactMessage(): string {
|
||||
return "No active thread to compact.";
|
||||
}
|
||||
|
||||
function finishBeforeArchivingThreadsMessage(): string {
|
||||
return "Finish or interrupt the current turn before archiving threads.";
|
||||
}
|
||||
|
||||
function modelOverrideMessage(model: string | null): string {
|
||||
return model === null ? "Model reset to default for subsequent turns." : `Model set to ${model} for subsequent turns.`;
|
||||
}
|
||||
|
|
@ -168,10 +159,6 @@ export async function executeSlashCommand(
|
|||
context.addSystemMessage(noActiveThreadToRollbackMessage());
|
||||
return;
|
||||
}
|
||||
if (context.busy) {
|
||||
context.addSystemMessage(interruptBeforeRollbackMessage());
|
||||
return;
|
||||
}
|
||||
await context.threadActions.rollbackThread(context.activeThreadId);
|
||||
return;
|
||||
case "compact":
|
||||
|
|
@ -182,11 +169,6 @@ export async function executeSlashCommand(
|
|||
await context.threadActions.compactThread(context.activeThreadId);
|
||||
return;
|
||||
case "archive": {
|
||||
if (context.busy) {
|
||||
context.addSystemMessage(finishBeforeArchivingThreadsMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
const thread = resolveThreadArgument(args, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export async function executeSlashCommandWithState(
|
|||
...host,
|
||||
activeThreadId: state.activeThreadId,
|
||||
listedThreads: state.listedThreads,
|
||||
busy: state.busy,
|
||||
referThread: (thread, message) => {
|
||||
if (!client) return Promise.resolve(null);
|
||||
return referencedThreadInput(host, client, thread, message);
|
||||
|
|
|
|||
|
|
@ -39,8 +39,10 @@ export interface GoalActions extends ThreadGoalSyncActions {
|
|||
|
||||
type GoalObjectiveSavePlan =
|
||||
| { kind: "reject"; message: string }
|
||||
| { kind: "save-existing"; threadId: string; objective: string; tokenBudget: number | null }
|
||||
| { kind: "start-thread-and-save"; objective: string; tokenBudget: number | null };
|
||||
| { kind: "save-existing"; threadId: string; objective: NormalizedGoalObjective; tokenBudget: number | null }
|
||||
| { kind: "start-thread-and-save"; objective: NormalizedGoalObjective; tokenBudget: number | null };
|
||||
|
||||
type NormalizedGoalObjective = string & { readonly __brand: "NormalizedGoalObjective" };
|
||||
|
||||
function emptyGoalObjectiveMessage(): string {
|
||||
return "Goal objective cannot be empty.";
|
||||
|
|
@ -94,15 +96,24 @@ async function setObjective(host: GoalActionsHost, threadId: string, objective:
|
|||
host.addSystemMessage(emptyGoalObjectiveMessage());
|
||||
return false;
|
||||
}
|
||||
return setNormalizedObjective(host, threadId, normalized, tokenBudget);
|
||||
}
|
||||
|
||||
async function setNormalizedObjective(
|
||||
host: GoalActionsHost,
|
||||
threadId: string,
|
||||
objective: NormalizedGoalObjective,
|
||||
tokenBudget: number | null,
|
||||
): Promise<boolean> {
|
||||
const current = host.stateStore.getState().activeThread.goal;
|
||||
const isNewGoal = current === null;
|
||||
const applied = await setGoal(host, threadId, {
|
||||
objective: normalized,
|
||||
objective,
|
||||
status: current?.status ?? "active",
|
||||
tokenBudget,
|
||||
});
|
||||
if (applied && isNewGoal) {
|
||||
await recordGoalUserMessage(host, threadId, normalized);
|
||||
await recordGoalUserMessage(host, threadId, objective);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
|
@ -114,7 +125,7 @@ async function saveObjective(host: GoalActionsHost, objective: string, tokenBudg
|
|||
host.addSystemMessage(plan.message);
|
||||
return false;
|
||||
case "save-existing":
|
||||
return setObjective(host, plan.threadId, plan.objective, plan.tokenBudget);
|
||||
return setNormalizedObjective(host, plan.threadId, plan.objective, plan.tokenBudget);
|
||||
case "start-thread-and-save":
|
||||
return startThreadAndSaveObjective(host, plan);
|
||||
}
|
||||
|
|
@ -181,9 +192,9 @@ function planGoalObjectiveSave(activeThreadId: string | null, objective: string,
|
|||
: { kind: "start-thread-and-save", objective: normalized, tokenBudget };
|
||||
}
|
||||
|
||||
function normalizedGoalObjective(objective: string): string | null {
|
||||
function normalizedGoalObjective(objective: string): NormalizedGoalObjective | null {
|
||||
const trimmed = objective.trim();
|
||||
return trimmed || null;
|
||||
return trimmed ? (trimmed as NormalizedGoalObjective) : null;
|
||||
}
|
||||
|
||||
async function startThreadAndSaveObjective(
|
||||
|
|
@ -195,7 +206,7 @@ async function startThreadAndSaveObjective(
|
|||
if (!client) return false;
|
||||
const response = await host.startThread(plan.objective, { syncGoal: false });
|
||||
const threadId = response?.threadId ?? null;
|
||||
return threadId ? await setObjective(host, threadId, plan.objective, plan.tokenBudget) : false;
|
||||
return threadId ? await setNormalizedObjective(host, threadId, plan.objective, plan.tokenBudget) : false;
|
||||
} catch (error) {
|
||||
host.addSystemMessage(errorMessage(error));
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCommandExecutionContext {
|
||||
return {
|
||||
activeThreadId: "thread-1",
|
||||
busy: false,
|
||||
listedThreads: [thread({ id: "thread-1", name: "Current" })],
|
||||
startNewThread: vi.fn().mockResolvedValue(undefined),
|
||||
startThreadForGoal: vi.fn().mockResolvedValue("thread-new"),
|
||||
|
|
@ -208,13 +207,13 @@ describe("slash commands", () => {
|
|||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("No active thread to roll back.");
|
||||
});
|
||||
|
||||
it("rejects /rollback while a turn is running", async () => {
|
||||
const ctx = context({ busy: true });
|
||||
it("delegates rollback running-turn checks to thread actions", async () => {
|
||||
const ctx = context();
|
||||
|
||||
await executeSlashCommand("rollback", "", ctx);
|
||||
|
||||
expect(ctx.threadActions.rollbackThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Interrupt the current turn before rolling back.");
|
||||
expect(ctx.threadActions.rollbackThread).toHaveBeenCalledWith("thread-1");
|
||||
expect(ctx.addSystemMessage).not.toHaveBeenCalledWith("Interrupt the current turn before rolling back.");
|
||||
});
|
||||
|
||||
it("rejects /rollback arguments", async () => {
|
||||
|
|
@ -412,13 +411,13 @@ describe("slash commands", () => {
|
|||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/archive requires a thread. Usage: /archive <thread>");
|
||||
});
|
||||
|
||||
it("rejects /archive while a turn is running", async () => {
|
||||
const ctx = context({ busy: true });
|
||||
it("delegates archive running-turn checks to thread actions", async () => {
|
||||
const ctx = context();
|
||||
|
||||
await executeSlashCommand("archive", "thread-1", ctx);
|
||||
|
||||
expect(ctx.threadActions.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Finish or interrupt the current turn before archiving threads.");
|
||||
expect(ctx.threadActions.archiveThread).toHaveBeenCalledWith("thread-1");
|
||||
expect(ctx.addSystemMessage).not.toHaveBeenCalledWith("Finish or interrupt the current turn before archiving threads.");
|
||||
});
|
||||
|
||||
it("reports ambiguous archive matches", async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue