Skip to content
Open
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
20 changes: 19 additions & 1 deletion apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vitest";

import { buildExpiredTerminalContextToastCopy, deriveComposerSendState } from "./ChatView.logic";
import {
buildExpiredTerminalContextToastCopy,
buildRunningTurnBlockedMessage,
deriveComposerSendState,
} from "./ChatView.logic";

describe("deriveComposerSendState", () => {
it("treats expired terminal pills as non-sendable content", () => {
Expand Down Expand Up @@ -67,3 +71,17 @@ describe("buildExpiredTerminalContextToastCopy", () => {
});
});
});

describe("buildRunningTurnBlockedMessage", () => {
it("explains when a turn is still running", () => {
expect(buildRunningTurnBlockedMessage(false)).toBe(
"A turn is still running. Stop it before sending another prompt.",
);
});

it("explains when a long-running command is keeping the turn active", () => {
expect(buildRunningTurnBlockedMessage(true)).toBe(
"A long-running command is still active. Stop the current turn before sending another prompt.",
);
});
});
6 changes: 6 additions & 0 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,9 @@ export function buildExpiredTerminalContextToastCopy(
description: "Re-add it if you want that terminal output included.",
};
}

export function buildRunningTurnBlockedMessage(hasRunningSubprocess: boolean): string {
return hasRunningSubprocess
? "A long-running command is still active. Stop the current turn before sending another prompt."
: "A turn is still running. Stop it before sending another prompt.";
}
44 changes: 29 additions & 15 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ import { ProviderHealthBanner } from "./chat/ProviderHealthBanner";
import { ThreadErrorBanner } from "./chat/ThreadErrorBanner";
import {
buildExpiredTerminalContextToastCopy,
buildRunningTurnBlockedMessage,
buildLocalDraftThread,
buildTemporaryWorktreeBranchName,
cloneComposerImageForRetry,
Expand Down Expand Up @@ -661,6 +662,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
const isSendBusy = sendPhase !== "idle";
const isPreparingWorktree = sendPhase === "preparing-worktree";
const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;
const hasRunningTerminalSubprocess = terminalState.runningTerminalIds.length > 0;
const runningTurnBlockedMessage = buildRunningTurnBlockedMessage(hasRunningTerminalSubprocess);
const nowIso = new Date(nowTick).toISOString();
const activeWorkStartedAt = deriveActiveWorkStartedAt(
activeLatestTurn,
Expand Down Expand Up @@ -2346,6 +2349,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
e?.preventDefault();
const api = readNativeApi();
if (!api || !activeThread || isSendBusy || isConnecting || sendInFlightRef.current) return;
if (phase === "running") {
setThreadError(activeThread.id, runningTurnBlockedMessage);
return;
}
if (activePendingProgress) {
onAdvanceActivePendingUserInput();
return;
Expand Down Expand Up @@ -3931,22 +3938,29 @@ export default function ChatView({ threadId }: ChatViewProps) {
</Button>
</div>
) : phase === "running" ? (
<button
type="button"
className="flex size-8 cursor-pointer items-center justify-center rounded-full bg-rose-500/90 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105 sm:h-8 sm:w-8"
onClick={() => void onInterrupt()}
aria-label="Stop generation"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="currentColor"
aria-hidden="true"
<div className="flex items-center gap-2">
<span className="hidden max-w-52 text-right text-xs text-muted-foreground/70 sm:inline">
{runningTurnBlockedMessage}
</span>
<button
type="button"
className="flex h-9 cursor-pointer items-center justify-center gap-2 rounded-full bg-rose-500/90 px-3 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105 sm:h-8"
onClick={() => void onInterrupt()}
aria-label="Stop generation"
title={runningTurnBlockedMessage}
>
<rect x="2" y="2" width="8" height="8" rx="1.5" />
</svg>
</button>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="currentColor"
aria-hidden="true"
>
<rect x="2" y="2" width="8" height="8" rx="1.5" />
</svg>
<span className="hidden sm:inline">Stop</span>
</button>
</div>
) : pendingUserInputs.length === 0 ? (
showPlanFollowUpPrompt ? (
prompt.trim().length > 0 ? (
Expand Down