Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
04b391b
refactor(chat): return slash command results
ibetitsmike Aug 28, 2026
e46f8d9
refactor(chat): unexport internal command handlers, cover /plan open
ibetitsmike Aug 28, 2026
b6660d2
fix(chat): evaluate input disposition against the live draft
ibetitsmike Aug 28, 2026
2c12b5a
fix(chat): drop the terminal consume-path composer clear
ibetitsmike Aug 28, 2026
1703d30
fix(chat): clear the composer when detached commands are accepted
ibetitsmike Aug 28, 2026
2da6553
refactor(runtime): deepen path handling
ibetitsmike Aug 28, 2026
8f23de5
polish: fix stale comments, drop dead env assignments, dedupe test do…
ibetitsmike Aug 28, 2026
acc7a2b
fix: keep Windows absolute paths intact in shell path exports
ibetitsmike Aug 28, 2026
4beb783
fix: propagate aborts through remote file path resolution
ibetitsmike Aug 28, 2026
ccd3f6c
fix: align local pathEnv expansion with file I/O; keep pathEnv author…
ibetitsmike Aug 28, 2026
abcc46e
fix: reuse the resolved stream temp dir for ensureDir
ibetitsmike Aug 28, 2026
1bd7a36
refactor: extract shared exec file I/O; dedupe test runtime boilerpla…
ibetitsmike Aug 28, 2026
ffe21c4
refactor(tools): deepen tool definition catalog
ibetitsmike Aug 28, 2026
c9857f9
refactor(tools): collapse presentation metadata
ibetitsmike Aug 28, 2026
ef30fa2
fix(tools): preserve historical lifecycle rendering
ibetitsmike Aug 28, 2026
3279377
refactor(services): cut task workspace cycle
ibetitsmike Aug 28, 2026
5c9ba0e
refactor(services): dedupe seam types and drop port-optional chaining
ibetitsmike Aug 28, 2026
74c091e
test(services): delete seam-obsoleted scaffolding in workspaceService…
ibetitsmike Aug 28, 2026
c3b0303
test(services): dedupe taskService suite scaffolding against the type…
ibetitsmike Aug 28, 2026
e05dc05
test(services): apply deslop and simplify audit findings
ibetitsmike Aug 28, 2026
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
121 changes: 82 additions & 39 deletions src/browser/features/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ import {
import {
prepareCompactionMessage,
processSlashCommand,
type SlashCommandContext,
type CommandAction,
type SlashCommandEnv,
} from "@/browser/utils/chatCommands";
import {
addWorkflowRunCardMessageForRun,
Expand Down Expand Up @@ -2474,62 +2475,104 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
// Prepare file parts for commands that need to send messages with attachments
const commandFileParts = chatAttachmentsToFileParts(attachments, { validate: true });
const asyncCommandToken = ++asyncCommandTokenRef.current;
const commandContext: SlashCommandContext = {
const commandEnv: SlashCommandEnv = {
api,
variant,
workspaceId: commandWorkspaceId,
projectPath: commandProjectPath,
rawInput: restoreInput,
dynamicWorkflowsEnabled: dynamicWorkflowsExperimentEnabled,
openSettings: open,
currentModel: workspaceSidebarState?.currentModel ?? null,
sendMessageOptions: commandSendMessageOptions,
getInput: () => getDraft().text,
setInput,
setAttachments,
setSendingState: (increment: boolean) => setSendingCount((c) => c + (increment ? 1 : -1)),
setToast,
setPreferredModel,
setVimEnabled,
asyncCommandToken,
isAsyncCommandCurrent: (token, originWorkspaceId) => {
resetContext: variant === "workspace" ? props.onResetContext : undefined,
truncateHistory: variant === "workspace" ? props.onTruncateHistory : undefined,
editMessageId: editingMessageForUi?.id,
reviews: reviewsData,
attachments,
fileParts: commandFileParts.length > 0 ? commandFileParts : undefined,
attachedReviewIds: reviewIdsForCheck,
isCurrent: () => {
const scope = asyncCommandScopeRef.current;
return (
token === asyncCommandTokenRef.current &&
asyncCommandToken === asyncCommandTokenRef.current &&
scope.variant === "workspace" &&
scope.workspaceId === originWorkspaceId
scope.workspaceId === commandWorkspaceId
);
},
onResetContext: variant === "workspace" ? props.onResetContext : undefined,
onTruncateHistory: variant === "workspace" ? props.onTruncateHistory : undefined,
resetInputHeight: () => {
if (inputRef.current) {
inputRef.current.style.height = "";
};

// Command actions stop at the caller's UI boundary; creation mode intentionally has its own applier.
const applyCommandActions = (actions: CommandAction[]) => {
for (const action of actions) {
switch (action.type) {
case "clear-input":
setInput("");
break;
case "reset-input-height":
if (inputRef.current) inputRef.current.style.height = "";
break;
case "show-toast":
setToast(action.toast);
break;
case "set-preferred-model":
setPreferredModel(action.model);
break;
case "toggle-vim":
setVimEnabled((enabled) => !enabled);
break;
case "set-sending":
setSendingCount((count) => count + (action.sending ? 1 : -1));
break;
case "clear-attachments":
setAttachments([]);
break;
case "detach-reviews":
if (variant === "workspace") props.onDetachAllReviews?.();
break;
case "check-reviews":
if (variant === "workspace" && action.reviewIds.length > 0) {
props.onCheckReviews?.(action.reviewIds);
}
break;
case "message-sent":
if (variant === "workspace") props.onMessageSent?.(action.dispatchMode);
break;
case "cancel-edit":
commandOnCancelEdit?.();
break;
}
},
editMessageId: editingMessageForUi?.id,
onCancelEdit: commandOnCancelEdit,
reviews: reviewsData,
attachments,
fileParts: commandFileParts.length > 0 ? commandFileParts : undefined,
onMessageSent: variant === "workspace" ? props.onMessageSent : undefined,
onDetachAllReviews: variant === "workspace" ? props.onDetachAllReviews : undefined,
onCheckReviews: variant === "workspace" ? props.onCheckReviews : undefined,
attachedReviewIds: reviewIdsForCheck,
}
};

const result = await processSlashCommand(parsed, commandContext);
let result = await processSlashCommand(parsed, commandEnv);
while (result.kind === "phase") {
applyCommandActions(result.actions);
result = await result.continue();
}
applyCommandActions(result.actions);
if (result.backgroundTask) {
void result.backgroundTask().then(applyCommandActions);
}

if (!result.clearInput) {
setInput(restoreInput);
} else {
setDraftReviews(null);
if (variant === "workspace" && parsed.type === "compact") {
if (reviewIdsForCheck.length > 0) {
props.onCheckReviews?.(reviewIdsForCheck);
switch (result.inputDisposition) {
case "consume":
// Commands clear the composer through their own clear-input actions;
// clearing again here would wipe a draft typed while phases ran.
setDraftReviews(null);
break;
case "restore":
setInput(restoreInput);
break;
case "restore-if-empty":
// Async phases can outlive the invoking render, so check the live
// persisted draft: the getDraft closure captured here still reports
// this render's input and would refuse to restore over a newer draft.
if (readPersistedState(storageKeys.inputKey, "").trim().length === 0) {
setInput(restoreInput);
} else {
setDraftReviews(null);
}
props.onMessageSent?.(dispatchMode);
}
break;
}

return true;
Expand Down
33 changes: 22 additions & 11 deletions src/browser/features/ChatInput/useCreationWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ import {
} from "@/browser/features/ChatInput/draftAttachmentsStorage";
import type { MuxMessageMetadata } from "@/common/types/message";
import type { ParsedCommand } from "@/browser/utils/slashCommands/types";
import { processSlashCommand, type SlashCommandContext } from "@/browser/utils/chatCommands";
import {
processSlashCommand,
type CommandAction,
type SlashCommandEnv,
} from "@/browser/utils/chatCommands";
import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events";
import {
useWorkspaceName,
Expand Down Expand Up @@ -741,26 +745,33 @@ export function useCreationWorkspace({

if (initialSlashCommand) {
await initialAiSettingsPersisted;
const commandContext: SlashCommandContext = {
const commandEnv: SlashCommandEnv = {
api,
workspaceId: metadata.id,
variant: "workspace",
projectPath: metadata.projectPath,
rawInput: messageText,
dynamicWorkflowsEnabled,
sendMessageOptions,
setInput: () => undefined,
setAttachments: () => undefined,
setSendingState: () => undefined,
setToast,
setPreferredModel: () => undefined,
setVimEnabled: () => undefined,
resetInputHeight: () => undefined,
};
const commandResult = await processSlashCommand(initialSlashCommand, commandContext);
// Creation owns only toast state; composer actions intentionally remain local to ChatInput.
const applyCommandActions = (actions: CommandAction[]) => {
for (const action of actions) {
if (action.type === "show-toast") setToast(action.toast);
}
};
let commandResult = await processSlashCommand(initialSlashCommand, commandEnv);
while (commandResult.kind === "phase") {
applyCommandActions(commandResult.actions);
commandResult = await commandResult.continue();
}
applyCommandActions(commandResult.actions);
if (commandResult.backgroundTask) {
void commandResult.backgroundTask().then(applyCommandActions);
}
setIsSending(false);

if (!commandResult.clearInput) {
if (commandResult.inputDisposition !== "consume") {
workspaceStore.clearPendingInitialSendState(metadata.id);
return { success: false };
}
Expand Down
Loading
Loading