feat(coding-agent): add dot continuation shortcut
158 unmodified linesExpand all159160161162163164165166167538 unmodified linesExpand all158 unmodified linesExpand all| `/tree` | Jump to any point in the session and continue from there || `/fork` | Create a new session from a previous user message || `/clone` | Duplicate the current active branch into a new session || `/compact [prompt]` | Manually compact context, optional custom instructions || `.` | Continue the current task through a hidden synthetic prompt || `/copy` | Copy last assistant message to clipboard || `/btw <question>`, `/side <question>` | Ask an inline side question without adding it to the session; replies continue the side conversation, esc returns || `/export [file]` | Export session to HTML file || `/share` | Upload as private GitHub gist with shareable HTML link |538 unmodified linesExpand all1- Added `.` as a hidden continuation shortcut for the current task.50 unmodified lines515253545556575859329 unmodified linesExpand all50 unmodified lines| `/tree` | Jump to any point in the session and continue from there || `/fork` | Create a new session from a previous user message || `/clone` | Duplicate the current active branch into a new session || `/compact [prompt]` | Manually compact context, optionally with custom instructions || `.` | Continue the current task through a hidden synthetic prompt || `/refine [instructions]` | Refine or roll back session-backed harness state || `/copy` | Copy last assistant message to clipboard || `/btw <question>`, `/side <question>` | Ask an inline side question without adding it to the session; replies continue the side conversation, esc returns || `/export [file]` | Export session to HTML |329 unmodified linesExpand all12345678794 unmodified linesExpand all8038048058068078088098108118128138148158168178188198208218228238248258268278288298308118318328338348358368378388398408418428438448458468478488492236 unmodified linesExpand allimport type { AgentMessage } from "@earendil-works/pi-agent-core";import { getModel } from "@earendil-works/pi-ai";import { describe, expect, it, vi } from "vitest";import { createManualContinuationMessage, MANUAL_CONTINUATION_PROMPT } from "../src/core/messages.js";import { MissingSessionCwdError } from "../src/core/session-cwd.js";import { SessionImportFileNotFoundError } from "../src/core/session-import-errors.js";import { DAEMON_REFINE_REQUEST_TIMEOUT_MS,794 unmodified linesExpand all queueIfBusy: true, }); });
it("forwards hidden custom prompt metadata", async () => { const fakeClient = new FakeDaemonClient(); const connection = new DaemonAgentConnection(asDaemonClient(fakeClient), "active-1"); const customMessage = createManualContinuationMessage(1);
await connection.prompt(MANUAL_CONTINUATION_PROMPT, { customMessage, expandPromptTemplates: false, });
expect(fakeClient.requests.at(-1)).toMatchObject({ type: "prompt", activeSessionId: "active-1", message: MANUAL_CONTINUATION_PROMPT, customMessage, expandPromptTemplates: false, }); });
it("forwards signal-backed prompts with a unique cancellable admission id", async () => { const fakeClient = new FakeDaemonClient(); const connection = new DaemonAgentConnection(asDaemonClient(fakeClient), "active-1"); const abort = new AbortController();
await connection.prompt("startup", { signal: abort.signal, queueIfBusy: true }); const customMessage = createManualContinuationMessage(1); await connection.prompt("startup", { signal: abort.signal, queueIfBusy: true, customMessage, expandPromptTemplates: false, });
expect(fakeClient.requests.at(-1)).toMatchObject({ type: "prompt", activeSessionId: "active-1", message: "startup", queueIfBusy: true, customMessage, expandPromptTemplates: false, admissionId: expect.stringMatching(/^prompt-admission:/), }); });
2236 unmodified linesExpand all2 unmodified lines34567891011162 unmodified linesExpand all174175176177178179180181182183184185186187188189190191192193194195196197198199200201202168 unmodified linesExpand all2 unmodified linesimport { describe, expect, it, vi } from "vitest";import type { AgentSessionEvent, AgentSessionEventListener, PromptOptions } from "../src/core/agent-session.js";import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.js";import { emptyGoalState } from "../src/core/goals.js";import { createManualContinuationMessage, MANUAL_CONTINUATION_PROMPT } from "../src/core/messages.js";import { InProcessAgentConnection } from "../src/modes/agent-connection/in-process-agent-connection.js";import type { AgentConnectionEvent, AgentConnectionState } from "../src/modes/agent-connection/types.js";
type RuntimeSession = AgentSessionRuntime["session"];162 unmodified linesExpand all ); finishTurn(); }, ); it("forwards hidden custom prompt metadata to the session", async () => { const session = createFakeSession("custom-prompt", []); const prompt = vi.fn((_message: string, options?: PromptOptions) => { options?.preflightResult?.(true); return Promise.resolve(); }); Object.assign(session.session, { prompt }); const connection = new InProcessAgentConnection(asRuntime(new FakeRuntime(session.session))); const customMessage = createManualContinuationMessage(1);
await connection.prompt(MANUAL_CONTINUATION_PROMPT, { customMessage, expandPromptTemplates: false, });
expect(prompt).toHaveBeenCalledWith( MANUAL_CONTINUATION_PROMPT, expect.objectContaining({ customMessage, expandPromptTemplates: false, skipInputHandlers: true }), ); });
it("forwards prompt admission cancellation to the session", async () => { const session = createFakeSession("prompt-cancellation", []); const prompt = vi.fn( (_message: string, options?: PromptOptions) =>168 unmodified linesExpand all12345678268 unmodified linesExpand all277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308581 unmodified linesExpand allimport { setKeybindings } from "@earendil-works/pi-tui";import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";import { KeybindingsManager } from "../src/core/keybindings.js";import { MANUAL_CONTINUATION_CUSTOM_TYPE, MANUAL_CONTINUATION_PROMPT } from "../src/core/messages.js";import type { ModelRegistry } from "../src/core/model-registry.js";import { SettingsManager } from "../src/core/settings-manager.js";import { DaemonAgentConnection } from "../src/modes/agent-connection/daemon-agent-connection.js";import type { AgentConnectionSavedSessionInfo } from "../src/modes/agent-connection/types.js";268 unmodified linesExpand all }); expect(DaemonAgentConnection.attach).not.toHaveBeenCalled(); });
it("sends a bare dot as a hidden continuation from the agents view", async () => { const request = vi.fn(async () => ({ success: true as const, data: undefined })); const self = { options: { config: {} }, requireClient: () => ({ request }), };
await invoke("sendPrompt", self, "active-1", ".", "steer");
expect(request).toHaveBeenCalledWith({ type: "prompt", activeSessionId: "active-1", message: ".", streamingBehavior: "steer", expandPromptTemplates: false, customMessage: expect.objectContaining({ role: "custom", customType: MANUAL_CONTINUATION_CUSTOM_TYPE, content: MANUAL_CONTINUATION_PROMPT, display: false, }), }); });
it("uses the opened session as the crash-path back target", async () => { const opened = summary({ sessionName: "opened" }); const previous = summary({ id: "previous", activeSessionId: "previous", sessionId: "previous" }); const runView = vi581 unmodified linesExpand all12345678548 unmodified linesExpand all557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598265 unmodified linesExpand allimport type { ImageContent } from "@earendil-works/pi-ai";import { describe, expect, it, type Mock, vi } from "vitest";import { KeybindingsManager } from "../src/core/keybindings.js";import { MANUAL_CONTINUATION_CUSTOM_TYPE, MANUAL_CONTINUATION_PROMPT } from "../src/core/messages.js";import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";import { ClientPromptStashStore, type PromptStashState } from "../src/modes/interactive/prompt-stash-state.js";import { QueueSelection } from "../src/modes/interactive/queue-selection.js";
548 unmodified linesExpand all expect(mode.promptStash).toBeUndefined(); expect(mode.editor.getText()).toBe("half-written draft"); });
it("submits a bare dot as a hidden manual continuation", async () => { const mode = createSubmitHarness();
await mode.defaultEditor.onSubmit?.(".");
expect(mode.agentConnection.prompt).toHaveBeenCalledWith(".", { streamingBehavior: "steer", queueIfBusy: true, images: [], expandPromptTemplates: false, customMessage: expect.objectContaining({ role: "custom", customType: MANUAL_CONTINUATION_CUSTOM_TYPE, content: MANUAL_CONTINUATION_PROMPT, display: false, }), }); expect(mode.editor.addToHistory).toHaveBeenCalledWith("."); });
it("keeps a bare dot literal when it carries an image", async () => { const image: ImageContent = { type: "image", data: "image-data", mimeType: "image/png" }; const mode = createSubmitHarness(); mode.collectImagesFor.mockReturnValue([image]);
await mode.defaultEditor.onSubmit?.(".");
expect(mode.agentConnection.prompt).toHaveBeenCalledWith(".", { streamingBehavior: "steer", queueIfBusy: true, images: [image], }); });
it("routes session-owned commands through canonical prompt admission", async () => { const mode = Object.assign(createSubmitHarness(), { isAgentCompacting: () => true });
for (const command of ["/compact focus on tools", "/refine --global", "/goal ship it", "/autonomous on"]) {265 unmodified linesExpand all4 unmodified lines567891011121314151617181920151 unmodified linesExpand all17217317417517617717817918018118218318418518618718818919019119219319419519619719819935 unmodified lines4 unmodified lines COMPACTION_OUTCOME_CUSTOM_TYPE, type CustomMessage, convertToLlm, createCompactionOutcomeMessage, createManualContinuationMessage, createSessionSlashCommandMessage, createSessionSlashCommandResultMessage, isCompactionOutcomeMessage, isSessionSlashCommandMessage, isSessionSlashCommandResultMessage, MANUAL_CONTINUATION_CUSTOM_TYPE, MANUAL_CONTINUATION_PROMPT, SESSION_SLASH_COMMAND_CUSTOM_TYPE, SESSION_SLASH_COMMAND_RESULT_CUSTOM_TYPE,} from "../src/core/messages.js";import { parseSessionSlashCommand } from "../src/core/slash-commands.js";151 unmodified linesExpand all ]), ).toEqual([]); });
test("converts manual continuation into hidden model context", () => { const message = createManualContinuationMessage(123);
expect(message).toEqual({ role: "custom", customType: MANUAL_CONTINUATION_CUSTOM_TYPE, content: MANUAL_CONTINUATION_PROMPT, display: false, timestamp: 123, }); expect(convertToLlm([message])).toEqual([ { role: "user", content: [{ type: "text", text: MANUAL_CONTINUATION_PROMPT }], timestamp: 123, }, ]); expect(buildConversationComponents([message], componentOptions)).toEqual([]); });
test("continues to include ordinary custom messages", () => { expect(convertToLlm([customMessage("extension_notice")])).toMatchObject([ { role: "user", content: [{ type: "text", text: "durable display text" }] }, ]);35 unmodified lines35 unmodified lines36373839404142434445464753 unmodified lines101102103104105106107108109110111112113114115116117118499 unmodified linesExpand all35 unmodified linesexport const RLM_CHILD_CONTRACT_NOTICE_CUSTOM_TYPE = "rlm_child_contract_notice";export const RLM_CHILD_FAILURE_CUSTOM_TYPE = "rlm_child_failure";export const RLM_CHILD_TERMINAL_NOTICE_CUSTOM_TYPE = "rlm_child_terminal_notice";export const BASH_HANDLE_COMPLETION_CUSTOM_TYPE = "bash_handle_completion";export const MANUAL_CONTINUATION_CUSTOM_TYPE = "manual_continuation";export const MANUAL_CONTINUATION_PROMPT = `<system-notice>Continue the current task from where you stopped. Resume the most recent unfinished step. Do not pause to summarize progress, reconfirm the plan, or ask whether to proceed.</system-notice>`;
export interface SessionSlashCommandDetails { command: SessionSlashCommand; commandEntryId?: string;53 unmodified lines exitCode: number; duration: number;}
export function createManualContinuationMessage(timestamp = Date.now()): CustomMessage { return { role: "custom", customType: MANUAL_CONTINUATION_CUSTOM_TYPE, content: MANUAL_CONTINUATION_PROMPT, display: false, timestamp, };}
export function createBashHandleCompletionMessage( details: BashHandleCompletionDetails, timestamp = Date.now(),): CustomMessage<BashHandleCompletionDetails> {499 unmodified linesExpand all123456789101112131415161718192021222324252627282930313233343536373839404142434445import { fauxAssistantMessage } from "@earendil-works/pi-ai";import { afterEach, describe, expect, it } from "vitest";import { createManualContinuationMessage, MANUAL_CONTINUATION_CUSTOM_TYPE, MANUAL_CONTINUATION_PROMPT,} from "../../src/core/messages.js";import { createHarness, getMessageText, type Harness } from "./harness.js";
describe("manual continuation", () => { const harnesses: Harness[] = [];
afterEach(() => { while (harnesses.length > 0) harnesses.pop()?.cleanup(); });
it("starts a model request without a visible user message", async () => { const harness = await createHarness(); harnesses.push(harness); let providerPrompt = ""; harness.appendResponses([ (context) => { providerPrompt = getMessageText(context.messages.at(-1)); return fauxAssistantMessage("continued work"); }, ]);
await harness.session.prompt(".", { customMessage: createManualContinuationMessage(123), expandPromptTemplates: false, skipInputHandlers: true, });
expect(providerPrompt).toBe(MANUAL_CONTINUATION_PROMPT); expect(harness.session.messages).toMatchObject([ { role: "custom", customType: MANUAL_CONTINUATION_CUSTOM_TYPE, display: false, }, { role: "assistant" }, ]); expect(harness.session.messages.some((message) => message.role === "user")).toBe(false); });});