Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
3071b1a
πŸ€– refactor(streaming): collapse turn relay
ibetitsmike Aug 28, 2026
0d64704
πŸ€– test(streaming): return turn handles from session mocks
ibetitsmike Aug 28, 2026
26a00ff
πŸ€– refactor(streaming): tidy turn-engine seam declarations
ibetitsmike Aug 28, 2026
856eef2
πŸ€– fix(streaming): deliver handle-less turn failures exactly once
ibetitsmike Aug 28, 2026
a80d4ab
πŸ€– refactor(streaming): collapse facade completion plumbing
ibetitsmike Aug 28, 2026
da9d7ab
πŸ€– tests(streaming): collapse repeated startStream option literals
ibetitsmike Aug 28, 2026
49a9a05
πŸ€– tests(streaming): share turn-handle helpers via test harness
ibetitsmike Aug 28, 2026
9e9cf30
πŸ€– tests(streaming): drop EventEmitter-era scaffolding, share sink helper
ibetitsmike Aug 28, 2026
c85c5f9
πŸ€– refactor(streaming): pass TurnExecutionOptions through stream creation
ibetitsmike Aug 28, 2026
71c5c68
πŸ€– refactor(streaming): drop dead handle token, latch-era tests, mock …
ibetitsmike Aug 28, 2026
73c4839
πŸ€– refactor(streaming): build stream requests from one input object
ibetitsmike Aug 28, 2026
6858f22
πŸ€– chore(streaming): trim repeated capture narration
ibetitsmike Aug 28, 2026
d487ad4
πŸ€– fix(streaming): settle mock playback turns, drop post-dispose faile…
ibetitsmike Aug 28, 2026
d164514
πŸ€– tests(streaming): drop real-API concurrency duplicate, compress com…
ibetitsmike Aug 28, 2026
953b557
πŸ€– tests(streaming): lean on harness default stream mocks
ibetitsmike Aug 28, 2026
ea4de8a
πŸ€– refactor(streaming): collect pre-start errors per call instead of a…
ibetitsmike Aug 28, 2026
c72c9da
πŸ€– refactor(streaming): turn identity lives only on the handle
ibetitsmike Aug 28, 2026
8e3196b
πŸ€– tests(streaming): fold debug settlement into the completion suite, …
ibetitsmike Aug 28, 2026
10268de
πŸ€– tests(streaming): return turn handles from stale streamMessage mock…
ibetitsmike Aug 28, 2026
3eccf33
πŸ€– refactor(streaming): share request options between turn and fallbac…
ibetitsmike Aug 28, 2026
ef4d511
fix(services): skip handle-less startup-failure recovery after disposal
ibetitsmike Aug 28, 2026
769f149
fix(services): contain turn event sink rejections
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