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
16 changes: 16 additions & 0 deletions gui/src/components/History/HistoryTableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DocumentDuplicateIcon,
CloudIcon,
PencilSquareIcon,
TrashIcon,
Expand All @@ -13,6 +14,7 @@ import { IdeMessengerContext } from "../../context/IdeMessenger";
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
import { exitEdit } from "../../redux/thunks/edit";
import {
copySession,
deleteSession,
getSession,
loadRemoteSession,
Expand Down Expand Up @@ -178,6 +180,20 @@ export function HistoryTableRow({
>
<PencilSquareIcon width="1em" height="1em" />
</HeaderButtonWithToolTip>
<HeaderButtonWithToolTip
text="Copy"
onClick={async (e) => {
e.stopPropagation();
await dispatch(
copySession({
sessionId: sessionMetadata.sessionId,
titlePrefix: "Copy",
}),
);
}}
>
<DocumentDuplicateIcon width="1em" height="1em" />
</HeaderButtonWithToolTip>
<HeaderButtonWithToolTip
text="Delete"
onClick={async (e) => {
Expand Down
21 changes: 21 additions & 0 deletions gui/src/components/StepContainer/ResponseActions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ArrowsPointingInIcon,
ArrowTurnRightDownIcon,
BarsArrowDownIcon,
PencilSquareIcon,
TrashIcon,
Expand Down Expand Up @@ -40,6 +41,7 @@ export default function ResponseActions({
(state) => state.session.contextPercentage,
);
const isPruned = useAppSelector((state) => state.session.isPruned);
const sessionId = useAppSelector((state) => state.session.id);
const ruleGenerationSupported = useMemo(() => {
return selectedModel && modelSupportsNativeTools(selectedModel);
}, [selectedModel]);
Expand All @@ -59,6 +61,16 @@ export default function ResponseActions({
dispatch(setDialogMessage(<GenerateRuleDialog />));
};

const onFork = async () => {
const { forkSession } = await import("../../redux/thunks/session");
void dispatch(
forkSession({
sessionId,
upToMessageIndex: index,
}),
);
};

return (
<div className="text-description-muted mx-2 flex cursor-default items-center justify-end space-x-1 bg-transparent pb-0 text-xs">
<HeaderButtonWithToolTip
Expand Down Expand Up @@ -105,6 +117,15 @@ export default function ResponseActions({
</HeaderButtonWithToolTip>
)}

<HeaderButtonWithToolTip
testId={`fork-button-${index}`}
text="Copy and fork from here"
tabIndex={-1}
onClick={onFork}
>
<ArrowTurnRightDownIcon className="text-description-muted h-3.5 w-3.5" />
</HeaderButtonWithToolTip>

<HeaderButtonWithToolTip
testId={`delete-button-${index}`}
text="Delete"
Expand Down
37 changes: 36 additions & 1 deletion gui/src/components/StepContainer/StepContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ConversationSummary from "./ConversationSummary";
import Reasoning from "./Reasoning";
import ResponseActions from "./ResponseActions";
import ThinkingIndicator from "./ThinkingIndicator";
import UserMessageActions from "./UserMessageActions";

interface StepContainerProps {
item: ChatHistoryItem;
Expand All @@ -34,6 +35,32 @@ export default function StepContainer(props: StepContainerProps) {
const historyItemAfterThis = useAppSelector(
(state) => state.session.history[props.index + 1],
);

const isNextMsgAssistantOrThinking =
historyItemAfterThis?.message.role === "assistant" ||
historyItemAfterThis?.message.role === "thinking" ||
historyItemAfterThis?.message.role === "tool";

const shouldRenderResponseAction = () => {
// Only for assistant messages
if (props.item.message.role !== "assistant") return false;

// Hide when next is assistant/thinking/tool
if (isNextMsgAssistantOrThinking) return false;

// If this is the last message, only show when not streaming and no tool calls
if (props.isLast) {
return !isStreaming && !props.item.toolCallStates;
}

// For non-last messages, only show if the next message is a user message
return historyItemAfterThis?.message.role === "user";
};

const shouldRenderUserMessageActions = () => {
// Show actions for user messages when not streaming
return props.item.message.role === "user";
};
const showResponseActions =
(props.isLast || historyItemAfterThis?.message.role === "user") &&
!(props.isLast && (isStreaming || props.item.toolCallStates));
Expand Down Expand Up @@ -97,7 +124,7 @@ export default function StepContainer(props: StepContainerProps) {
{props.isLast && <ThinkingIndicator historyItem={props.item} />}
</div>

{showResponseActions && (
{shouldRenderResponseAction() && (
<div
className={`mt-2 h-7 transition-opacity duration-300 ease-in-out ${isBeforeLatestSummary || isStreaming ? "opacity-35" : ""} ${isStreaming && "pointer-events-none cursor-not-allowed"}`}
>
Expand All @@ -112,6 +139,14 @@ export default function StepContainer(props: StepContainerProps) {
</div>
)}

{shouldRenderUserMessageActions() && (
<div
className={`mt-2 h-7 transition-opacity duration-300 ease-in-out ${isBeforeLatestSummary ? "opacity-35" : ""}`}
>
<UserMessageActions index={props.index} item={props.item} />
</div>
)}

{/* Show compaction indicator for the latest summary */}
{isLatestSummary && (
<div className="mx-1.5 my-5">
Expand Down
49 changes: 49 additions & 0 deletions gui/src/components/StepContainer/UserMessageActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ArrowTurnRightDownIcon } from "@heroicons/react/24/outline";
import { ChatHistoryItem } from "core";
import { renderChatMessage } from "core/util/messageContent";
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
import { CopyIconButton } from "../gui/CopyIconButton";
import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip";

export interface UserMessageActionsProps {
index: number;
item: ChatHistoryItem;
}

export default function UserMessageActions({
index,
item,
}: UserMessageActionsProps) {
const dispatch = useAppDispatch();
const sessionId = useAppSelector((state) => state.session.id);

const onFork = async () => {
const { forkSession } = await import("../../redux/thunks/session");
void dispatch(
forkSession({
sessionId,
upToMessageIndex: index,
}),
);
};

return (
<div className="text-description-muted mx-2 flex cursor-default items-center justify-end space-x-1 bg-transparent pb-0 text-xs">
<HeaderButtonWithToolTip
testId={`fork-button-${index}`}
text="Copy and fork from here"
tabIndex={-1}
onClick={onFork}
>
<ArrowTurnRightDownIcon className="text-description-muted h-3.5 w-3.5" />
</HeaderButtonWithToolTip>

<CopyIconButton
tabIndex={-1}
text={renderChatMessage(item.message)}
clipboardIconClassName="h-3.5 w-3.5 text-description-muted"
checkIconClassName="h-3.5 w-3.5 text-success"
/>
</div>
);
}
8 changes: 4 additions & 4 deletions gui/src/components/TabBar/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export const TabBar = React.forwardRef<HTMLDivElement>((_, ref) => {

useEffect(() => {
if (!tabs.length) {
handleNewTab();
void handleNewTab();
}
}, [tabs.map((t) => t.id).join(",")]);

Expand Down Expand Up @@ -236,12 +236,12 @@ export const TabBar = React.forwardRef<HTMLDivElement>((_, ref) => {
<Tab
key={tab.id}
isActive={tab.isActive}
onClick={() => handleTabClick(tab.id)}
onClick={() => void handleTabClick(tab.id)}
onAuxClick={(e) => {
// Middle mouse button
if (e.button === 1) {
e.preventDefault();
handleTabClose(tab.id);
void handleTabClose(tab.id);
}
}}
>
Expand All @@ -250,7 +250,7 @@ export const TabBar = React.forwardRef<HTMLDivElement>((_, ref) => {
/* disabled={tabs.length === 1} */
onClick={(e) => {
e.stopPropagation();
handleTabClose(tab.id);
void handleTabClose(tab.id);
}}
>
<XMarkIcon width={12} height={12} />
Expand Down
28 changes: 17 additions & 11 deletions gui/src/pages/gui/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ThinkingBlockPeek from "../../components/mainInput/belowMainInput/Thinkin
import ContinueInputBox from "../../components/mainInput/ContinueInputBox";
import { useOnboardingCard } from "../../components/OnboardingCard";
import StepContainer from "../../components/StepContainer";
import UserMessageActions from "../../components/StepContainer/UserMessageActions";
import { TabBar } from "../../components/TabBar/TabBar";
import { IdeMessengerContext } from "../../context/IdeMessenger";
import { useWebviewListener } from "../../hooks/useWebviewListener";
Expand Down Expand Up @@ -285,17 +286,22 @@ export function Chat() {

if (message.role === "user") {
return (
<ContinueInputBox
onEnter={(editorState, modifiers) =>
sendInput(editorState, modifiers, index)
}
isLastUserInput={isLastUserInput(index)}
isMainInput={false}
editorState={editorState ?? item.message.content}
contextItems={contextItems}
appliedRules={appliedRules}
inputId={message.id}
/>
<div className={isBeforeLatestSummary ? "opacity-50" : ""}>
<ContinueInputBox
onEnter={(editorState, modifiers) =>
sendInput(editorState, modifiers, index)
}
isLastUserInput={isLastUserInput(index)}
isMainInput={false}
editorState={editorState ?? item.message.content}
contextItems={contextItems}
appliedRules={appliedRules}
inputId={message.id}
/>
<div className="mt-2 h-7 transition-opacity duration-300 ease-in-out">
<UserMessageActions index={index} item={item} />
</div>
</div>
);
}

Expand Down
Loading
Loading