From 45322eba5a9e46eafba3b82e37b68a02797b0bfb Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 13:30:27 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20de-jank=20/compact=20?= =?UTF-8?q?continue=20editing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/components/AIView.tsx | 8 +++- .../components/Messages/UserMessage.tsx | 6 ++- src/browser/utils/chatCommands.test.ts | 19 +++++++++ src/browser/utils/chatCommands.ts | 12 +++--- src/browser/utils/compaction/format.ts | 18 ++++++++ src/browser/utils/compaction/handler.test.ts | 42 +++++++++++++++++++ src/browser/utils/compaction/handler.ts | 8 +++- src/common/orpc/schemas/api.ts | 1 + src/common/types/message.ts | 2 +- src/node/services/workspaceService.ts | 10 ++++- 10 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 src/browser/utils/compaction/format.ts create mode 100644 src/browser/utils/compaction/handler.test.ts diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index 1f91d1ce02..25b9b1f3ab 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -60,6 +60,7 @@ import { CompactionWarning } from "./CompactionWarning"; import { ConcurrentLocalWarning } from "./ConcurrentLocalWarning"; import { BackgroundProcessesBanner } from "./BackgroundProcessesBanner"; import { useBackgroundBashHandlers } from "@/browser/hooks/useBackgroundBashHandlers"; +import { buildCompactionEditText } from "@/browser/utils/compaction/format"; import { checkAutoCompaction } from "@/browser/utils/compaction/autoCompactionCheck"; import { executeCompaction } from "@/browser/utils/chatCommands"; import { useProviderOptions } from "@/browser/hooks/useProviderOptions"; @@ -357,7 +358,12 @@ const AIViewInner: React.FC = ({ return; } - setEditingMessage({ id: lastUserMessage.historyId, content: lastUserMessage.content }); + setEditingMessage({ + id: lastUserMessage.historyId, + content: lastUserMessage.compactionRequest + ? buildCompactionEditText(lastUserMessage.compactionRequest) + : lastUserMessage.content, + }); setAutoScroll(false); // Show jump-to-bottom indicator // Scroll to the message being edited diff --git a/src/browser/components/Messages/UserMessage.tsx b/src/browser/components/Messages/UserMessage.tsx index a7fbbf2c9c..58ea76f967 100644 --- a/src/browser/components/Messages/UserMessage.tsx +++ b/src/browser/components/Messages/UserMessage.tsx @@ -7,6 +7,7 @@ import { TerminalOutput } from "./TerminalOutput"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; import { useCopyToClipboard } from "@/browser/hooks/useCopyToClipboard"; import { copyToClipboard } from "@/browser/utils/clipboard"; +import { buildCompactionEditText } from "@/browser/utils/compaction/format"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { VIM_ENABLED_KEY } from "@/common/constants/storage"; import { Clipboard, ClipboardCheck, Pencil } from "lucide-react"; @@ -48,7 +49,10 @@ export const UserMessage: React.FC = ({ const handleEdit = () => { if (onEdit && !isLocalCommandOutput) { - onEdit(message.historyId, content); + const editText = message.compactionRequest + ? buildCompactionEditText(message.compactionRequest) + : content; + onEdit(message.historyId, editText); } }; diff --git a/src/browser/utils/chatCommands.test.ts b/src/browser/utils/chatCommands.test.ts index 8c10a954f9..7c158f4328 100644 --- a/src/browser/utils/chatCommands.test.ts +++ b/src/browser/utils/chatCommands.test.ts @@ -166,6 +166,25 @@ describe("prepareCompactionMessage", () => { expect(metadata.parsed.continueMessage?.text).toBe("Continue with this"); }); + test("rawCommand excludes multiline continue payload", () => { + const sendMessageOptions = createBaseOptions(); + const { metadata } = prepareCompactionMessage({ + workspaceId: "ws-1", + maxOutputTokens: 2048, + model: "anthropic:claude-3-5-haiku", + continueMessage: { text: "Line 1\nLine 2" }, + sendMessageOptions, + }); + + if (metadata.type !== "compaction-request") { + throw new Error("Expected compaction metadata"); + } + + expect(metadata.rawCommand).toBe("/compact -t 2048 -m anthropic:claude-3-5-haiku"); + expect(metadata.rawCommand).not.toContain("Line 1"); + expect(metadata.rawCommand).not.toContain("\n"); + }); + test("omits default resume text from compaction prompt", () => { const sendMessageOptions = createBaseOptions(); const { messageText, metadata } = prepareCompactionMessage({ diff --git a/src/browser/utils/chatCommands.ts b/src/browser/utils/chatCommands.ts index f88beefd02..24d13cd733 100644 --- a/src/browser/utils/chatCommands.ts +++ b/src/browser/utils/chatCommands.ts @@ -671,7 +671,7 @@ export function prepareCompactionMessage(options: CompactionOptions): { const metadata: MuxFrontendMetadata = { type: "compaction-request", - rawCommand: formatCompactionCommand(options), + rawCommand: formatCompactionCommandLine(options), parsed: compactData, ...(options.source === "idle-compaction" && { source: options.source, @@ -719,9 +719,12 @@ export async function executeCompaction( } /** - * Format compaction command string for display + * Format compaction command *line* for display. + * + * Intentionally excludes the multiline continue payload; that content is stored in + * `muxMetadata.parsed.continueMessage` and is shown/edited separately. */ -function formatCompactionCommand(options: CompactionOptions): string { +function formatCompactionCommandLine(options: CompactionOptions): string { let cmd = "/compact"; if (options.maxOutputTokens) { cmd += ` -t ${options.maxOutputTokens}`; @@ -729,9 +732,6 @@ function formatCompactionCommand(options: CompactionOptions): string { if (options.model) { cmd += ` -m ${options.model}`; } - if (options.continueMessage) { - cmd += `\n${options.continueMessage.text}`; - } return cmd; } diff --git a/src/browser/utils/compaction/format.ts b/src/browser/utils/compaction/format.ts new file mode 100644 index 0000000000..402c6db1df --- /dev/null +++ b/src/browser/utils/compaction/format.ts @@ -0,0 +1,18 @@ +import type { CompactionRequestData } from "@/common/types/message"; + +/** + * Build the text shown in the editor when editing a /compact request. + * + * `rawCommand` is intentionally a single-line command (no multiline payload). + * If a continue message exists, we append its text on subsequent lines. + */ +export function buildCompactionEditText(request: { + rawCommand: string; + parsed: CompactionRequestData; +}): string { + const continueText = request.parsed.continueMessage?.text; + if (typeof continueText === "string" && continueText.trim().length > 0) { + return `${request.rawCommand}\n${continueText}`; + } + return request.rawCommand; +} diff --git a/src/browser/utils/compaction/handler.test.ts b/src/browser/utils/compaction/handler.test.ts new file mode 100644 index 0000000000..435fcdd1c3 --- /dev/null +++ b/src/browser/utils/compaction/handler.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test, mock } from "bun:test"; +import type { APIClient } from "@/browser/contexts/API"; +import { cancelCompaction } from "./handler"; + +describe("cancelCompaction", () => { + test("interrupts without restore-to-input and enters edit mode with full text", async () => { + const interruptStream = mock(() => Promise.resolve({ success: true })); + + const client = { + workspace: { + interruptStream, + }, + } as unknown as APIClient; + + const aggregator = { + getAllMessages: () => [ + { + id: "user-1", + role: "user", + metadata: { + muxMetadata: { + type: "compaction-request", + rawCommand: "/compact -t 100", + parsed: { continueMessage: { text: "Do the thing" } }, + }, + }, + }, + ], + } as unknown as Parameters[2]; + + const startEditingMessage = mock(() => undefined); + + const result = await cancelCompaction(client, "ws-1", aggregator, startEditingMessage); + + expect(result).toBe(true); + expect(interruptStream).toHaveBeenCalledWith({ + workspaceId: "ws-1", + options: { abandonPartial: true, restoreQueuedToInput: false }, + }); + expect(startEditingMessage).toHaveBeenCalledWith("user-1", "/compact -t 100\nDo the thing"); + }); +}); diff --git a/src/browser/utils/compaction/handler.ts b/src/browser/utils/compaction/handler.ts index 80895d0f6e..4ce2b713e6 100644 --- a/src/browser/utils/compaction/handler.ts +++ b/src/browser/utils/compaction/handler.ts @@ -7,6 +7,7 @@ import type { StreamingMessageAggregator } from "@/browser/utils/messages/StreamingMessageAggregator"; import type { APIClient } from "@/browser/contexts/API"; +import { buildCompactionEditText } from "./format"; /** * Check if the workspace is currently in a compaction stream @@ -42,7 +43,7 @@ export function getCompactionCommand(aggregator: StreamingMessageAggregator): st const muxMeta = compactionMsg.metadata?.muxMetadata; if (muxMeta?.type !== "compaction-request") return null; - return muxMeta.rawCommand ?? null; + return buildCompactionEditText(muxMeta); } /** @@ -78,7 +79,10 @@ export async function cancelCompaction( // Interrupt stream with abandonPartial flag // Backend detects this and skips compaction (Ctrl+C flow) - await client.workspace.interruptStream({ workspaceId, options: { abandonPartial: true } }); + await client.workspace.interruptStream({ + workspaceId, + options: { abandonPartial: true, restoreQueuedToInput: false }, + }); // Enter edit mode on the compaction-request message with original command // This lets user immediately edit the message or delete it diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index dfef9cb113..c3734459cb 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -306,6 +306,7 @@ export const workspace = { soft: z.boolean().optional(), abandonPartial: z.boolean().optional(), sendQueuedImmediately: z.boolean().optional(), + restoreQueuedToInput: z.boolean().optional(), }) .optional(), }), diff --git a/src/common/types/message.ts b/src/common/types/message.ts index b8b125d78c..98458b6c96 100644 --- a/src/common/types/message.ts +++ b/src/common/types/message.ts @@ -85,7 +85,7 @@ export type MuxFrontendMetadata = MuxFrontendMetadataBase & ( | { type: "compaction-request"; - rawCommand: string; // The original /compact command as typed by user (for display) + rawCommand: string; // The /compact command line for display (excludes multiline continue payload) parsed: CompactionRequestData; /** Source of compaction request: user-initiated (undefined) or idle-compaction (auto) */ source?: "idle-compaction"; diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 48d0739f6d..18a7551f86 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -1449,7 +1449,12 @@ export class WorkspaceService extends EventEmitter { async interruptStream( workspaceId: string, - options?: { soft?: boolean; abandonPartial?: boolean; sendQueuedImmediately?: boolean } + options?: { + soft?: boolean; + abandonPartial?: boolean; + sendQueuedImmediately?: boolean; + restoreQueuedToInput?: boolean; + } ): Promise> { try { const session = this.getOrCreateSession(workspaceId); @@ -1470,6 +1475,9 @@ export class WorkspaceService extends EventEmitter { if (options?.sendQueuedImmediately) { // Send queued messages immediately instead of restoring to input session.sendQueuedMessages(); + } else if (options?.restoreQueuedToInput === false) { + // Explicitly drop queued messages (used by cancel-compaction so we don't clobber edit mode) + session.clearQueue(); } else { // Restore queued messages to input box for user-initiated interrupts session.restoreQueueToInput(); From d4375d138e3f80947f2edc12e351972491e3f684 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 13:42:02 -0600 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20centralize=20edi?= =?UTF-8?q?table=20user=20message=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/components/AIView.tsx | 6 ++---- src/browser/components/Messages/UserMessage.tsx | 6 ++---- src/browser/utils/messages/messageUtils.ts | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index 25b9b1f3ab..b950e02fa1 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -29,6 +29,7 @@ import { shouldShowInterruptedBarrier, mergeConsecutiveStreamErrors, computeBashOutputGroupInfo, + getEditableUserMessageText, } from "@/browser/utils/messages/messageUtils"; import { BashOutputCollapsedIndicator } from "./tools/BashOutputCollapsedIndicator"; import { hasInterruptedStream } from "@/browser/utils/messages/retryEligibility"; @@ -60,7 +61,6 @@ import { CompactionWarning } from "./CompactionWarning"; import { ConcurrentLocalWarning } from "./ConcurrentLocalWarning"; import { BackgroundProcessesBanner } from "./BackgroundProcessesBanner"; import { useBackgroundBashHandlers } from "@/browser/hooks/useBackgroundBashHandlers"; -import { buildCompactionEditText } from "@/browser/utils/compaction/format"; import { checkAutoCompaction } from "@/browser/utils/compaction/autoCompactionCheck"; import { executeCompaction } from "@/browser/utils/chatCommands"; import { useProviderOptions } from "@/browser/hooks/useProviderOptions"; @@ -360,9 +360,7 @@ const AIViewInner: React.FC = ({ setEditingMessage({ id: lastUserMessage.historyId, - content: lastUserMessage.compactionRequest - ? buildCompactionEditText(lastUserMessage.compactionRequest) - : lastUserMessage.content, + content: getEditableUserMessageText(lastUserMessage), }); setAutoScroll(false); // Show jump-to-bottom indicator diff --git a/src/browser/components/Messages/UserMessage.tsx b/src/browser/components/Messages/UserMessage.tsx index 58ea76f967..7f467a32d6 100644 --- a/src/browser/components/Messages/UserMessage.tsx +++ b/src/browser/components/Messages/UserMessage.tsx @@ -7,7 +7,7 @@ import { TerminalOutput } from "./TerminalOutput"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; import { useCopyToClipboard } from "@/browser/hooks/useCopyToClipboard"; import { copyToClipboard } from "@/browser/utils/clipboard"; -import { buildCompactionEditText } from "@/browser/utils/compaction/format"; +import { getEditableUserMessageText } from "@/browser/utils/messages/messageUtils"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { VIM_ENABLED_KEY } from "@/common/constants/storage"; import { Clipboard, ClipboardCheck, Pencil } from "lucide-react"; @@ -49,9 +49,7 @@ export const UserMessage: React.FC = ({ const handleEdit = () => { if (onEdit && !isLocalCommandOutput) { - const editText = message.compactionRequest - ? buildCompactionEditText(message.compactionRequest) - : content; + const editText = getEditableUserMessageText(message); onEdit(message.historyId, editText); } }; diff --git a/src/browser/utils/messages/messageUtils.ts b/src/browser/utils/messages/messageUtils.ts index 877bbfa0e5..abfcee7aee 100644 --- a/src/browser/utils/messages/messageUtils.ts +++ b/src/browser/utils/messages/messageUtils.ts @@ -1,5 +1,21 @@ import type { DisplayedMessage } from "@/common/types/message"; import type { BashOutputToolArgs } from "@/common/types/tools"; +import { buildCompactionEditText } from "@/browser/utils/compaction/format"; + +/** + * Returns the text that should be placed into the ChatInput when editing a user message. + * + * For /compact requests, this reconstructs the full multiline command by appending the + * continue message to the stored single-line rawCommand. + */ +export function getEditableUserMessageText( + message: Extract +): string { + if (message.compactionRequest) { + return buildCompactionEditText(message.compactionRequest); + } + return message.content; +} /** * Type guard to check if a message is a bash_output tool call with valid args From 7c4f24a1eec1e8f3c055290fc7fa32fd01264766 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 15:51:27 -0600 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A4=96=20ci:=20fix=20unit=20tests=20a?= =?UTF-8?q?fter=20compaction=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/messages/compactionOptions.test.ts | 2 +- src/node/services/serverService.test.ts | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/browser/utils/messages/compactionOptions.test.ts b/src/browser/utils/messages/compactionOptions.test.ts index a6a257ec35..5c4f107392 100644 --- a/src/browser/utils/messages/compactionOptions.test.ts +++ b/src/browser/utils/messages/compactionOptions.test.ts @@ -74,7 +74,7 @@ describe("applyCompactionOverrides", () => { const result = applyCompactionOverrides(baseWithTools, compactData); expect(result.mode).toBe("compact"); - expect(result.toolPolicy).toEqual([{ regex_match: ".*", action: "disable" }]); // Tools always disabled for compaction + expect(result.toolPolicy).toEqual([{ regex_match: ".*", action: "disable" }]); }); it("applies all overrides together", () => { diff --git a/src/node/services/serverService.test.ts b/src/node/services/serverService.test.ts index 25676aaac8..19f9045153 100644 --- a/src/node/services/serverService.test.ts +++ b/src/node/services/serverService.test.ts @@ -77,22 +77,18 @@ describe("ServerService.startServer", () => { } test("cleans up server when lockfile acquisition fails", async () => { - // Skip on Windows where chmod doesn't work the same way - if (process.platform === "win32") { - return; - } - const service = new ServerService(); - // Make muxHome read-only so lockfile.acquire() will fail - await fs.chmod(tempDir, 0o444); + // Make muxHome a file (not a directory) so lockfile.acquire fails deterministically. + const muxHomeFile = path.join(tempDir, "not-a-dir"); + await fs.writeFile(muxHomeFile, "not a directory"); let thrownError: Error | null = null; try { // Start server - this should fail when trying to write lockfile await service.startServer({ - muxHome: tempDir, + muxHome: muxHomeFile, context: stubContext as ORPCContext, authToken: "test-token", port: 0, // random port @@ -103,7 +99,7 @@ describe("ServerService.startServer", () => { // Verify that an error was thrown expect(thrownError).not.toBeNull(); - expect(thrownError!.message).toMatch(/EACCES|permission denied/i); + expect(thrownError!.message).toMatch(/ENOTDIR|not a directory/i); // Verify the server is NOT left running expect(service.isServerRunning()).toBe(false); From a4867b978b82c953b5fceb6dc2d6e13f2668bdff Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 16:06:47 -0600 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20avoid=20restore-to-in?= =?UTF-8?q?put=20clobbering=20edit=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/components/ChatInput/index.tsx | 5 ++++- src/browser/utils/compaction/handler.test.ts | 19 ++++++++++++++----- src/browser/utils/compaction/handler.ts | 10 +++++----- .../utils/messages/compactionOptions.test.ts | 2 +- src/common/orpc/schemas/api.ts | 1 - src/common/types/message.ts | 2 +- src/node/services/serverService.test.ts | 14 +++++++++----- src/node/services/workspaceService.ts | 10 +--------- 8 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx index 8c506c6965..5bf988b0e1 100644 --- a/src/browser/components/ChatInput/index.tsx +++ b/src/browser/components/ChatInput/index.tsx @@ -654,6 +654,9 @@ const ChatInputInner: React.FC = (props) => { const { text, mode = "append", imageParts } = customEvent.detail; if (mode === "replace") { + if (editingMessage) { + return; + } restoreText(text); } else { appendText(text); @@ -666,7 +669,7 @@ const ChatInputInner: React.FC = (props) => { window.addEventListener(CUSTOM_EVENTS.INSERT_TO_CHAT_INPUT, handler as EventListener); return () => window.removeEventListener(CUSTOM_EVENTS.INSERT_TO_CHAT_INPUT, handler as EventListener); - }, [appendText, restoreText, restoreImages]); + }, [appendText, restoreText, restoreImages, editingMessage]); // Allow external components to open the Model Selector useEffect(() => { diff --git a/src/browser/utils/compaction/handler.test.ts b/src/browser/utils/compaction/handler.test.ts index 435fcdd1c3..63de65bf31 100644 --- a/src/browser/utils/compaction/handler.test.ts +++ b/src/browser/utils/compaction/handler.test.ts @@ -3,8 +3,13 @@ import type { APIClient } from "@/browser/contexts/API"; import { cancelCompaction } from "./handler"; describe("cancelCompaction", () => { - test("interrupts without restore-to-input and enters edit mode with full text", async () => { - const interruptStream = mock(() => Promise.resolve({ success: true })); + test("enters edit mode with full text before interrupting", async () => { + const calls: string[] = []; + + const interruptStream = mock(() => { + calls.push("interrupt"); + return Promise.resolve({ success: true }); + }); const client = { workspace: { @@ -28,15 +33,19 @@ describe("cancelCompaction", () => { ], } as unknown as Parameters[2]; - const startEditingMessage = mock(() => undefined); + const startEditingMessage = mock(() => { + calls.push("edit"); + return undefined; + }); const result = await cancelCompaction(client, "ws-1", aggregator, startEditingMessage); expect(result).toBe(true); + expect(startEditingMessage).toHaveBeenCalledWith("user-1", "/compact -t 100\nDo the thing"); expect(interruptStream).toHaveBeenCalledWith({ workspaceId: "ws-1", - options: { abandonPartial: true, restoreQueuedToInput: false }, + options: { abandonPartial: true }, }); - expect(startEditingMessage).toHaveBeenCalledWith("user-1", "/compact -t 100\nDo the thing"); + expect(calls).toEqual(["edit", "interrupt"]); }); }); diff --git a/src/browser/utils/compaction/handler.ts b/src/browser/utils/compaction/handler.ts index 4ce2b713e6..3034cc89d5 100644 --- a/src/browser/utils/compaction/handler.ts +++ b/src/browser/utils/compaction/handler.ts @@ -77,16 +77,16 @@ export async function cancelCompaction( return false; } + // Enter edit mode first so any subsequent restore-to-input event from the interrupt can't + // clobber the edit buffer. + startEditingMessage(compactionRequestMsg.id, command); + // Interrupt stream with abandonPartial flag // Backend detects this and skips compaction (Ctrl+C flow) await client.workspace.interruptStream({ workspaceId, - options: { abandonPartial: true, restoreQueuedToInput: false }, + options: { abandonPartial: true }, }); - // Enter edit mode on the compaction-request message with original command - // This lets user immediately edit the message or delete it - startEditingMessage(compactionRequestMsg.id, command); - return true; } diff --git a/src/browser/utils/messages/compactionOptions.test.ts b/src/browser/utils/messages/compactionOptions.test.ts index 5c4f107392..a6a257ec35 100644 --- a/src/browser/utils/messages/compactionOptions.test.ts +++ b/src/browser/utils/messages/compactionOptions.test.ts @@ -74,7 +74,7 @@ describe("applyCompactionOverrides", () => { const result = applyCompactionOverrides(baseWithTools, compactData); expect(result.mode).toBe("compact"); - expect(result.toolPolicy).toEqual([{ regex_match: ".*", action: "disable" }]); + expect(result.toolPolicy).toEqual([{ regex_match: ".*", action: "disable" }]); // Tools always disabled for compaction }); it("applies all overrides together", () => { diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index c3734459cb..dfef9cb113 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -306,7 +306,6 @@ export const workspace = { soft: z.boolean().optional(), abandonPartial: z.boolean().optional(), sendQueuedImmediately: z.boolean().optional(), - restoreQueuedToInput: z.boolean().optional(), }) .optional(), }), diff --git a/src/common/types/message.ts b/src/common/types/message.ts index 98458b6c96..b8b125d78c 100644 --- a/src/common/types/message.ts +++ b/src/common/types/message.ts @@ -85,7 +85,7 @@ export type MuxFrontendMetadata = MuxFrontendMetadataBase & ( | { type: "compaction-request"; - rawCommand: string; // The /compact command line for display (excludes multiline continue payload) + rawCommand: string; // The original /compact command as typed by user (for display) parsed: CompactionRequestData; /** Source of compaction request: user-initiated (undefined) or idle-compaction (auto) */ source?: "idle-compaction"; diff --git a/src/node/services/serverService.test.ts b/src/node/services/serverService.test.ts index 19f9045153..25676aaac8 100644 --- a/src/node/services/serverService.test.ts +++ b/src/node/services/serverService.test.ts @@ -77,18 +77,22 @@ describe("ServerService.startServer", () => { } test("cleans up server when lockfile acquisition fails", async () => { + // Skip on Windows where chmod doesn't work the same way + if (process.platform === "win32") { + return; + } + const service = new ServerService(); - // Make muxHome a file (not a directory) so lockfile.acquire fails deterministically. - const muxHomeFile = path.join(tempDir, "not-a-dir"); - await fs.writeFile(muxHomeFile, "not a directory"); + // Make muxHome read-only so lockfile.acquire() will fail + await fs.chmod(tempDir, 0o444); let thrownError: Error | null = null; try { // Start server - this should fail when trying to write lockfile await service.startServer({ - muxHome: muxHomeFile, + muxHome: tempDir, context: stubContext as ORPCContext, authToken: "test-token", port: 0, // random port @@ -99,7 +103,7 @@ describe("ServerService.startServer", () => { // Verify that an error was thrown expect(thrownError).not.toBeNull(); - expect(thrownError!.message).toMatch(/ENOTDIR|not a directory/i); + expect(thrownError!.message).toMatch(/EACCES|permission denied/i); // Verify the server is NOT left running expect(service.isServerRunning()).toBe(false); diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 18a7551f86..48d0739f6d 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -1449,12 +1449,7 @@ export class WorkspaceService extends EventEmitter { async interruptStream( workspaceId: string, - options?: { - soft?: boolean; - abandonPartial?: boolean; - sendQueuedImmediately?: boolean; - restoreQueuedToInput?: boolean; - } + options?: { soft?: boolean; abandonPartial?: boolean; sendQueuedImmediately?: boolean } ): Promise> { try { const session = this.getOrCreateSession(workspaceId); @@ -1475,9 +1470,6 @@ export class WorkspaceService extends EventEmitter { if (options?.sendQueuedImmediately) { // Send queued messages immediately instead of restoring to input session.sendQueuedMessages(); - } else if (options?.restoreQueuedToInput === false) { - // Explicitly drop queued messages (used by cancel-compaction so we don't clobber edit mode) - session.clearQueue(); } else { // Restore queued messages to input box for user-initiated interrupts session.restoreQueueToInput();