Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/code/src/renderer/features/message-editor/commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AvailableCommand } from "@agentclientprotocol/sdk";
import { getSessionService } from "@features/sessions/service/service";
import { ANALYTICS_EVENTS, type FeedbackType } from "@shared/types/analytics";
import { track } from "@utils/analytics";
import { toast } from "@utils/toast";
Expand Down Expand Up @@ -51,6 +52,18 @@ const commands: CodeCommand[] = [
makeFeedbackCommand("good", "good", "Positive"),
makeFeedbackCommand("bad", "bad", "Negative"),
makeFeedbackCommand("feedback", "general", "General"),
{
name: "clear",
description: "Clear conversation history and start fresh",
async execute(_args, ctx) {
if (!ctx.repoPath || !ctx.taskId) {
toast.error("Cannot clear: no active session");
return;
}
await getSessionService().resetSession(ctx.taskId, ctx.repoPath);
toast.success("Conversation cleared");
},
},
];

export const CODE_COMMANDS: AvailableCommand[] = commands.map((cmd) => ({
Expand Down
84 changes: 79 additions & 5 deletions apps/code/src/renderer/features/sessions/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
} from "@features/sessions/stores/sessionStore";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { taskViewedApi } from "@features/sidebar/hooks/useTaskViewed";
import { workspaceApi } from "@features/workspace/hooks/useWorkspace";
import { POSTHOG_NOTIFICATIONS } from "@posthog/agent/acp-extensions";
import { DEFAULT_GATEWAY_MODEL } from "@posthog/agent/gateway-models";
import { getIsOnline } from "@renderer/stores/connectivityStore";
import { trpcClient } from "@renderer/trpc/client";
Expand Down Expand Up @@ -116,6 +118,8 @@ export class SessionService {
>();
/** AbortController for the current in-flight preview session start */
private previewAbort: AbortController | null = null;
/** Pending plan text to inject after session clear, keyed by taskRunId */
private pendingPlanContinuations = new Map<string, string>();
/** Active cloud task watchers, keyed by taskId */
private cloudTaskWatchers = new Map<
string,
Expand Down Expand Up @@ -858,8 +862,35 @@ export class SessionService {

taskViewedApi.markActivity(session.taskId);

// Process queued messages after turn completes - send all as one prompt
if (hasQueuedMessages) {
// Clear-and-continue from plan takes priority over queued messages
const pendingPlan = this.pendingPlanContinuations.get(taskRunId);
if (pendingPlan) {
this.pendingPlanContinuations.delete(taskRunId);
const { taskId } = session;
// Look up repoPath from workspace — session doesn't store it directly
workspaceApi
.get(taskId)
.then((workspace) => {
const repoPath = workspace?.worktreePath ?? workspace?.folderPath;
if (repoPath) {
return this.executeClearAndContinue(
taskId,
repoPath,
pendingPlan,
);
}
log.error("Cannot clear-and-continue: no workspace for task", {
taskId,
});
return undefined;
})
.catch((err) => {
log.error("Failed to clear and continue from plan", {
taskId,
error: err,
});
});
} else if (hasQueuedMessages) {
setTimeout(() => {
this.sendQueuedMessages(session.taskId).catch((err) => {
log.error("Failed to send queued messages", {
Expand Down Expand Up @@ -914,6 +945,21 @@ export class SessionService {
});
}
}

// Handle clear_and_continue — store plan text for post-turn injection
// extNotification may double the underscore prefix, so match both forms
if (
"method" in msg &&
(msg.method === POSTHOG_NOTIFICATIONS.CLEAR_AND_CONTINUE ||
msg.method === `_${POSTHOG_NOTIFICATIONS.CLEAR_AND_CONTINUE}`) &&
"params" in msg
) {
const params = msg.params as { plan?: string };
if (params?.plan) {
this.pendingPlanContinuations.set(taskRunId, params.plan);
log.info("Plan continuation queued", { taskRunId });
}
}
}

private handlePermissionRequest(
Expand Down Expand Up @@ -1747,11 +1793,39 @@ export class SessionService {

/**
* Start a fresh session for a task, abandoning the old conversation.
* Clears the backend sessionId so the next reconnect creates a new
* session instead of attempting to resume the stale one.
* Tears down the current session and creates a new task run so that
* S3 logs start clean — old messages won't reappear on restart.
*/
async resetSession(taskId: string, repoPath: string): Promise<void> {
await this.reconnectInPlace(taskId, repoPath, null);
const session = sessionStoreSetters.getSessionByTaskId(taskId);
if (!session) return;

const { taskTitle } = session;

await this.teardownSession(session.taskRunId);

const auth = this.getAuthCredentials();
if (!auth) {
throw new Error("Unable to reach server. Please check your connection.");
}

await this.createNewLocalSession(taskId, taskTitle, repoPath, auth);
}

/**
* Clear conversation history and re-inject an approved plan as the first prompt.
* Used by the "clear and continue from plan" flow.
*/
private async executeClearAndContinue(
taskId: string,
repoPath: string,
plan: string,
): Promise<void> {
await this.resetSession(taskId, repoPath);
await this.sendPrompt(
taskId,
`Continue implementing this approved plan:\n\n${plan}`,
);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"types": "./dist/agent.d.ts",
"import": "./dist/agent.js"
},
"./acp-extensions": {
"types": "./dist/acp-extensions.d.ts",
"import": "./dist/acp-extensions.js"
},
"./gateway-models": {
"types": "./dist/gateway-models.d.ts",
"import": "./dist/gateway-models.js"
Expand Down
3 changes: 3 additions & 0 deletions packages/agent/src/acp-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@ export const POSTHOG_NOTIFICATIONS = {

/** Marks a boundary for log compaction */
COMPACT_BOUNDARY: "_posthog/compact_boundary",

/** Request to clear session history and continue from an approved plan */
CLEAR_AND_CONTINUE: "_posthog/clear_and_continue",
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
RequestPermissionResponse,
} from "@agentclientprotocol/sdk";
import type { PermissionUpdate } from "@anthropic-ai/claude-agent-sdk";
import { POSTHOG_NOTIFICATIONS } from "../../../acp-extensions";
import { text } from "../../../utils/acp-content";
import type { Logger } from "../../../utils/logger";
import { toolInfoFromToolUse } from "../conversion/tool-use-to-acp";
Expand Down Expand Up @@ -190,6 +191,44 @@ async function applyPlanApproval(
};
}

// Clear history and continue from plan — approve, switch mode, then signal renderer
if (
response.outcome?.outcome === "selected" &&
response.outcome.optionId === "clearAndContinue"
) {
const planText = extractPlanText(updatedInput);

session.permissionMode = "default";
await session.query.setPermissionMode("default");
await context.client.sessionUpdate({
sessionId: context.sessionId,
update: {
sessionUpdate: "current_mode_update",
currentModeId: "default",
},
});
await context.updateConfigOption("mode", "default");

// Signal the renderer to clear the session and re-inject the plan
if (planText) {
await context.client.extNotification(
POSTHOG_NOTIFICATIONS.CLEAR_AND_CONTINUE,
{
sessionId: context.sessionId,
plan: planText,
},
);
}

return {
behavior: "allow",
updatedInput,
updatedPermissions: context.suggestions ?? [
{ type: "setMode", mode: "default", destination: "localSettings" },
],
};
}

const customInput = (response._meta as Record<string, unknown> | undefined)
?.customInput as string | undefined;
const feedback = customInput?.trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export function buildExitPlanModePermissionOptions(): PermissionOption[] {
name: "Yes, and manually approve edits",
optionId: "default",
},
{
kind: "allow_once",
name: "Yes, clear history and continue from plan",
optionId: "clearAndContinue",
},
{
kind: "reject_once",
name: "No, and tell the agent what to do differently",
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/adapters/claude/session/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AvailableCommand } from "@agentclientprotocol/sdk";
import type { SlashCommand } from "@anthropic-ai/claude-agent-sdk";

const UNSUPPORTED_COMMANDS = [
"clear",
"context",
"cost",
"keybindings-help",
Expand Down
1 change: 1 addition & 0 deletions packages/agent/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default defineConfig([
{
entry: [
"src/index.ts",
"src/acp-extensions.ts",
"src/agent.ts",
"src/gateway-models.ts",
"src/posthog-api.ts",
Expand Down