Skip to content

Commit 3594896

Browse files
authored
🤖 refactor: dedupe create-workspace vs send-message keybinds (#1122)
Refactors shared UI logic between the workspace creation flow and the existing-workspace send flow. - Centralize ImageAttachment[] → ImagePart[] conversion (with optional validation) - Remove hardcoded cancel key label in creation placeholder (use KEYBINDS.CANCEL) - Unify send-in-flight state used for disabling creation controls + overlay Validation: - make static-check _Generated with `mux`_
1 parent 1ed1e4e commit 3594896

File tree

2 files changed

+58
-53
lines changed

2 files changed

+58
-53
lines changed

‎src/browser/components/ChatInput/index.tsx‎

Lines changed: 17 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { ImageAttachments, type ImageAttachment } from "../ImageAttachments";
6060
import {
6161
extractImagesFromClipboard,
6262
extractImagesFromDrop,
63+
imageAttachmentsToImageParts,
6364
processImageFiles,
6465
} from "@/browser/utils/imageHandling";
6566

@@ -241,10 +242,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
241242
() => createTokenCountResource(tokenCountPromise),
242243
[tokenCountPromise]
243244
);
244-
const hasTypedText = input.trim().length > 0;
245-
const hasImages = imageAttachments.length > 0;
246-
const hasReviews = attachedReviews.length > 0;
247-
const canSend = (hasTypedText || hasImages || hasReviews) && !disabled && !isSending;
245+
248246
// Setter for model - updates localStorage directly so useSendMessageOptions picks it up
249247
const setPreferredModel = useCallback(
250248
(model: string) => {
@@ -272,6 +270,12 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
272270
}
273271
);
274272

273+
const isSendInFlight = variant === "creation" ? creationState.isSending : isSending;
274+
const hasTypedText = input.trim().length > 0;
275+
const hasImages = imageAttachments.length > 0;
276+
const hasReviews = attachedReviews.length > 0;
277+
const canSend = (hasTypedText || hasImages || hasReviews) && !disabled && !isSendInFlight;
278+
275279
// When entering creation mode, initialize the project-scoped model to the
276280
// default so previous manual picks don't bleed into new creation flows.
277281
// Only runs once per creation session (not when defaultModel changes, which
@@ -640,12 +644,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
640644
// Route to creation handler for creation variant
641645
if (variant === "creation") {
642646
// Creation variant: simple message send + workspace creation
643-
setIsSending(true);
644-
// Convert image attachments to image parts
645-
const creationImageParts = imageAttachments.map((img) => ({
646-
url: img.url,
647-
mediaType: img.mediaType,
648-
}));
647+
const creationImageParts = imageAttachmentsToImageParts(imageAttachments);
649648
const ok = await creationState.handleSend(
650649
messageText,
651650
creationImageParts.length > 0 ? creationImageParts : undefined
@@ -659,7 +658,6 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
659658
inputRef.current.style.height = "";
660659
}
661660
}
662-
setIsSending(false);
663661
return;
664662
}
665663

@@ -1066,33 +1064,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
10661064

10671065
try {
10681066
// Prepare image parts if any
1069-
const imageParts = imageAttachments.map((img, index) => {
1070-
// Validate before sending to help with debugging
1071-
if (!img.url || typeof img.url !== "string") {
1072-
console.error(
1073-
`Image attachment [${index}] has invalid url:`,
1074-
typeof img.url,
1075-
img.url?.slice(0, 50)
1076-
);
1077-
}
1078-
if (!img.url?.startsWith("data:")) {
1079-
console.error(
1080-
`Image attachment [${index}] url is not a data URL:`,
1081-
img.url?.slice(0, 100)
1082-
);
1083-
}
1084-
if (!img.mediaType || typeof img.mediaType !== "string") {
1085-
console.error(
1086-
`Image attachment [${index}] has invalid mediaType:`,
1087-
typeof img.mediaType,
1088-
img.mediaType
1089-
);
1090-
}
1091-
return {
1092-
url: img.url,
1093-
mediaType: img.mediaType,
1094-
};
1095-
});
1067+
const imageParts = imageAttachmentsToImageParts(imageAttachments, { validate: true });
10961068

10971069
// When editing a /compact command, regenerate the actual summarization request
10981070
let actualMessageText = messageText;
@@ -1308,7 +1280,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
13081280
const placeholder = (() => {
13091281
// Creation variant has simple placeholder
13101282
if (variant === "creation") {
1311-
return `Type your first message to create a workspace... (${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send, Esc to cancel)`;
1283+
return `Type your first message to create a workspace... (${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send, ${formatKeybind(KEYBINDS.CANCEL)} to cancel)`;
13121284
}
13131285

13141286
// Workspace variant placeholders
@@ -1353,17 +1325,9 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
13531325
{variant === "creation" && (
13541326
<CreationCenterContent
13551327
projectName={props.projectName}
1356-
isSending={creationState.isSending || isSending}
1357-
workspaceName={
1358-
creationState.isSending || isSending
1359-
? creationState.creatingWithIdentity?.name
1360-
: undefined
1361-
}
1362-
workspaceTitle={
1363-
creationState.isSending || isSending
1364-
? creationState.creatingWithIdentity?.title
1365-
: undefined
1366-
}
1328+
isSending={isSendInFlight}
1329+
workspaceName={isSendInFlight ? creationState.creatingWithIdentity?.name : undefined}
1330+
workspaceTitle={isSendInFlight ? creationState.creatingWithIdentity?.title : undefined}
13671331
/>
13681332
)}
13691333

@@ -1444,7 +1408,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
14441408
onRuntimeModeChange={creationState.setRuntimeMode}
14451409
onSetDefaultRuntime={creationState.setDefaultRuntimeMode}
14461410
onSshHostChange={creationState.setSshHost}
1447-
disabled={creationState.isSending || isSending}
1411+
disabled={isSendInFlight}
14481412
projectName={props.projectName}
14491413
nameState={creationState.nameState}
14501414
/>
@@ -1486,7 +1450,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
14861450
onEscapeInNormalMode={handleEscapeInNormalMode}
14871451
suppressKeys={showCommandSuggestions ? COMMAND_SUGGESTION_KEYS : undefined}
14881452
placeholder={placeholder}
1489-
disabled={!editingMessage && (disabled || isSending)}
1453+
disabled={!editingMessage && (disabled || isSendInFlight)}
14901454
aria-label={editingMessage ? "Edit your last message" : "Message Claude"}
14911455
aria-autocomplete="list"
14921456
aria-controls={
@@ -1505,7 +1469,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
15051469
shouldShowUI={voiceInput.shouldShowUI}
15061470
requiresSecureContext={voiceInput.requiresSecureContext}
15071471
onToggle={voiceInput.toggle}
1508-
disabled={disabled || isSending}
1472+
disabled={disabled || isSendInFlight}
15091473
mode={mode}
15101474
/>
15111475
</div>

‎src/browser/utils/imageHandling.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ImagePart } from "@/common/orpc/types";
12
import type { ImageAttachment } from "@/browser/components/ImageAttachments";
23

34
/**
@@ -24,6 +25,46 @@ function getMimeTypeFromExtension(filename: string): string {
2425
return mimeTypes[ext ?? ""] ?? "image/png";
2526
}
2627

28+
/**
29+
* Convert ImageAttachment[] → ImagePart[] for API calls.
30+
*
31+
* Kept in imageHandling to ensure creation + send flows stay aligned.
32+
*/
33+
export function imageAttachmentsToImageParts(
34+
attachments: ImageAttachment[],
35+
options?: { validate?: boolean }
36+
): ImagePart[] {
37+
const validate = options?.validate ?? false;
38+
39+
return attachments.map((img, index) => {
40+
if (validate) {
41+
// Validate before sending to help with debugging
42+
if (!img.url || typeof img.url !== "string") {
43+
console.error(
44+
`Image attachment [${index}] has invalid url:`,
45+
typeof img.url,
46+
(img as { url?: unknown }).url
47+
);
48+
}
49+
if (typeof img.url === "string" && !img.url.startsWith("data:")) {
50+
console.error(`Image attachment [${index}] url is not a data URL:`, img.url.slice(0, 100));
51+
}
52+
if (!img.mediaType || typeof img.mediaType !== "string") {
53+
console.error(
54+
`Image attachment [${index}] has invalid mediaType:`,
55+
typeof img.mediaType,
56+
(img as { mediaType?: unknown }).mediaType
57+
);
58+
}
59+
}
60+
61+
return {
62+
url: img.url,
63+
mediaType: img.mediaType,
64+
};
65+
});
66+
}
67+
2768
/**
2869
* Converts a File to an ImageAttachment with a base64 data URL
2970
*/

0 commit comments

Comments
 (0)