Skip to content
Merged
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
139 changes: 45 additions & 94 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { useMode } from "@/contexts/ModeContext";
import { ChatToggles } from "./ChatToggles";
import { useSendMessageOptions } from "@/hooks/useSendMessageOptions";
import { getModelKey, getInputKey } from "@/constants/storage";
import { forkWorkspace } from "@/utils/workspaceFork";
import {
handleNewCommand,
handleCompactCommand,
forkWorkspace,
prepareCompactionMessage,
type CommandHandlerContext,
} from "@/utils/chatCommands";
import { ToggleGroup } from "./ToggleGroup";
import { CUSTOM_EVENTS } from "@/constants/events";
import type { UIMode } from "@/types/mode";
Expand All @@ -31,10 +37,7 @@ import {
} from "@/utils/imageHandling";

import type { ThinkingLevel } from "@/types/thinking";
import type { CmuxFrontendMetadata, CompactionRequestData } from "@/types/message";
import type { SendMessageOptions } from "@/types/ipc";
import { applyCompactionOverrides } from "@/utils/messages/compactionOptions";
import { resolveCompactionModel } from "@/utils/messages/compactionModelPreference";
import type { CmuxFrontendMetadata } from "@/types/message";
import { useTelemetry } from "@/hooks/useTelemetry";
import { setTelemetryEnabled } from "@/telemetry";

Expand Down Expand Up @@ -159,49 +162,6 @@ export interface ChatInputProps {
}

// Helper function to convert parsed command to display toast
/**
* Prepare compaction message from /compact command
* Returns the actual message text (summarization request), metadata, and options
*/
function prepareCompactionMessage(
command: string,
sendMessageOptions: SendMessageOptions
): {
messageText: string;
metadata: CmuxFrontendMetadata;
options: Partial<SendMessageOptions>;
} {
const parsed = parseCommand(command);
if (parsed?.type !== "compact") {
throw new Error("Not a compact command");
}

const targetWords = parsed.maxOutputTokens ? Math.round(parsed.maxOutputTokens / 1.3) : 2000;

const messageText = `Summarize this conversation into a compact form for a new Assistant to continue helping the user. Use approximately ${targetWords} words.`;

// Handle model preference (sticky globally)
const effectiveModel = resolveCompactionModel(parsed.model);

// Create compaction metadata (will be stored in user message)
const compactData: CompactionRequestData = {
model: effectiveModel,
maxOutputTokens: parsed.maxOutputTokens,
continueMessage: parsed.continueMessage,
};

const metadata: CmuxFrontendMetadata = {
type: "compaction-request",
rawCommand: command,
parsed: compactData,
};

// Apply compaction overrides using shared transformation function
// This same function is used by useResumeManager to ensure consistency
const options = applyCompactionOverrides(sendMessageOptions, compactData);

return { messageText, metadata, options };
}

export const ChatInput: React.FC<ChatInputProps> = ({
workspaceId,
Expand Down Expand Up @@ -572,51 +532,19 @@ export const ChatInput: React.FC<ChatInputProps> = ({

// Handle /compact command
if (parsed.type === "compact") {
setInput(""); // Clear input immediately
setIsSending(true);

try {
const {
messageText: compactionMessage,
metadata,
options,
} = prepareCompactionMessage(messageText, sendMessageOptions);

const result = await window.api.workspace.sendMessage(workspaceId, compactionMessage, {
...sendMessageOptions,
...options,
cmuxMetadata: metadata,
editMessageId: editingMessage?.id, // Support editing compaction messages
});
const context: CommandHandlerContext = {
workspaceId,
sendMessageOptions,
editMessageId: editingMessage?.id,
setInput,
setIsSending,
setToast,
onCancelEdit,
};

if (!result.success) {
console.error("Failed to initiate compaction:", result.error);
setToast(createErrorToast(result.error));
setInput(messageText); // Restore input on error
} else {
setToast({
id: Date.now().toString(),
type: "success",
message:
metadata.type === "compaction-request" && metadata.parsed.continueMessage
? "Compaction started. Will continue automatically after completion."
: "Compaction started. AI will summarize the conversation.",
});
// Clear editing state on success
if (editingMessage && onCancelEdit) {
onCancelEdit();
}
}
} catch (error) {
console.error("Compaction error:", error);
setToast({
id: Date.now().toString(),
type: "error",
message: error instanceof Error ? error.message : "Failed to start compaction",
});
const result = await handleCompactCommand(parsed, context);
if (!result.clearInput) {
setInput(messageText); // Restore input on error
} finally {
setIsSending(false);
}
return;
}
Expand Down Expand Up @@ -667,6 +595,23 @@ export const ChatInput: React.FC<ChatInputProps> = ({
return;
}

// Handle /new command
if (parsed.type === "new") {
const context: CommandHandlerContext = {
workspaceId,
sendMessageOptions,
setInput,
setIsSending,
setToast,
};

const result = await handleNewCommand(parsed, context);
if (!result.clearInput) {
setInput(messageText); // Restore input on error
}
return;
}

// Handle all other commands - show display toast
const commandToast = createCommandToast(parsed);
if (commandToast) {
Expand Down Expand Up @@ -719,11 +664,17 @@ export const ChatInput: React.FC<ChatInputProps> = ({
const {
messageText: regeneratedText,
metadata,
options,
} = prepareCompactionMessage(messageText, sendMessageOptions);
sendOptions,
} = prepareCompactionMessage({
workspaceId,
maxOutputTokens: parsed.maxOutputTokens,
continueMessage: parsed.continueMessage,
model: parsed.model,
sendMessageOptions,
});
actualMessageText = regeneratedText;
cmuxMetadata = metadata;
compactionOptions = options;
compactionOptions = sendOptions;
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
});
}, []);

// Listen for EXECUTE_COMMAND events
useEffect(() => {
const handleExecuteCommand = (e: Event) => {
const customEvent = e as CustomEvent<{ commandId: string }>;
const { commandId } = customEvent.detail;

const action = getActions().find((a) => a.id === commandId);
if (!action) {
console.warn(`Command not found: ${commandId}`);
return;
}

// Run the action directly
void action.run();
addRecent(action.id);
};

window.addEventListener(CUSTOM_EVENTS.EXECUTE_COMMAND, handleExecuteCommand);
return () => window.removeEventListener(CUSTOM_EVENTS.EXECUTE_COMMAND, handleExecuteCommand);
}, [getActions, startPrompt, addRecent]);

const handlePromptValue = useCallback(
(value: string) => {
let nextInitial: string | null = null;
Expand Down
33 changes: 33 additions & 0 deletions src/components/NewWorkspaceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useId, useState } from "react";
import styled from "@emotion/styled";
import { Modal, ModalInfo, ModalActions, CancelButton, PrimaryButton } from "./Modal";
import { TooltipWrapper, Tooltip } from "./Tooltip";
import { formatNewCommand } from "@/utils/chatCommands";

const FormGroup = styled.div`
margin-bottom: 20px;
Expand Down Expand Up @@ -61,6 +62,29 @@ const UnderlinedLabel = styled.span`
cursor: help;
`;

const CommandDisplay = styled.div`
margin-top: 20px;
padding: 12px;
background: #1e1e1e;
border: 1px solid #3e3e42;
border-radius: 4px;
font-family: "Menlo", "Monaco", "Courier New", monospace;
font-size: 13px;
color: #d4d4d4;
white-space: pre-wrap;
word-break: break-all;
`;

const CommandLabel = styled.div`
font-size: 12px;
color: #888;
margin-bottom: 8px;
font-family:
system-ui,
-apple-system,
sans-serif;
`;

interface NewWorkspaceModalProps {
isOpen: boolean;
projectName: string;
Expand Down Expand Up @@ -236,6 +260,15 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
</InfoCode>
</ModalInfo>

{branchName.trim() && (
<div>
<CommandLabel>Equivalent command:</CommandLabel>
<CommandDisplay>
{formatNewCommand(branchName.trim(), trunkBranch.trim() || undefined)}
</CommandDisplay>
</div>
)}

<ModalActions>
<CancelButton type="button" onClick={handleCancel} disabled={isLoading}>
Cancel
Expand Down
6 changes: 6 additions & 0 deletions src/constants/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export const CUSTOM_EVENTS = {
* Detail: { workspaceId: string, projectPath: string, projectName: string, workspacePath: string, branch: string }
*/
WORKSPACE_FORK_SWITCH: "cmux:workspaceForkSwitch",

/**
* Event to execute a command from the command palette
* Detail: { commandId: string }
*/
EXECUTE_COMMAND: "cmux:executeCommand",
} as const;

/**
Expand Down
Loading