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
93 changes: 89 additions & 4 deletions packages/web/src/components/chat-room/chat-room-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

import {
Activity,
Check,
FileText,
Loader2,
MessageCircle,
PanelLeft,
Pencil,
RotateCcw,
X,
} from "lucide-react";
import type { ReactElement } from "react";
import { type FormEvent, type ReactElement, useState } from "react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Typography } from "@/components/ui/typography";
import { cn } from "@/lib/utils";
import { buildChatRoomHeaderTabs } from "./chat-room-header-tabs";
Expand All @@ -23,17 +27,43 @@ export function ChatRoomHeader({
isRerunDisabled,
isRerunning,
isRerunVisible,
isRenamingTitle,
title,
onOpenAction,
onOpenMessages,
onOpenSidebar,
onOpenTaskDetails,
onRenameTitle,
onRerunWorkflow,
}: ChatRoomHeaderProps): ReactElement {
const [draftTitle, setDraftTitle] = useState(title);
const [isEditingTitle, setIsEditingTitle] = useState(false);
const tabs = buildChatRoomHeaderTabs({
activeTab,
hasTaskDetails: Boolean(activeTaskId),
});
const trimmedDraftTitle = draftTitle.trim();

function startEditingTitle(): void {
setDraftTitle(title);
setIsEditingTitle(true);
}

function cancelEditingTitle(): void {
setDraftTitle(title);
setIsEditingTitle(false);
}

async function saveTitle(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
if (!trimmedDraftTitle || isRenamingTitle) {
return;
}
const didSave = await onRenameTitle(trimmedDraftTitle);
if (didSave !== false) {
setIsEditingTitle(false);
}
}

function selectTab(key: ChatRoomHeaderTabKey): void {
if (key === "taskDetails") {
Expand Down Expand Up @@ -61,9 +91,64 @@ export function ChatRoomHeader({
>
<PanelLeft size={17} />
</Button>
<div className="min-w-0">
<Typography className="truncate text-zinc-300">{title}</Typography>
</div>
{isEditingTitle ? (
<form
className="flex min-w-0 flex-1 items-center gap-2"
onSubmit={(event) => void saveTitle(event)}
>
<Input
aria-label="Session title"
className="h-8 min-w-0"
disabled={isRenamingTitle}
onChange={(event) => setDraftTitle(event.currentTarget.value)}
value={draftTitle}
/>
<Button
aria-label="Save session title"
disabled={!trimmedDraftTitle || isRenamingTitle}
size="icon"
type="submit"
variant="ghost"
>
{isRenamingTitle ? (
<Loader2
aria-hidden="true"
className="animate-spin"
size={15}
/>
) : (
<Check aria-hidden="true" size={15} />
)}
</Button>
<Button
aria-label="Cancel session title edit"
disabled={isRenamingTitle}
onClick={cancelEditingTitle}
size="icon"
type="button"
variant="ghost"
>
<X aria-hidden="true" size={15} />
</Button>
</form>
) : (
<>
<div className="min-w-0">
<Typography className="truncate text-zinc-300">
{title}
</Typography>
</div>
<Button
aria-label="Edit session title"
onClick={startEditingTitle}
size="icon"
type="button"
variant="ghost"
>
<Pencil aria-hidden="true" size={14} />
</Button>
</>
)}
</div>
<div className="flex shrink-0 items-center gap-2">
{isRerunVisible ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function ChatRoomPanelView({
isRerunDisabled,
isRerunning,
isRerunVisible,
isRenamingTitle,
isSending,
isPlanning,
isThinking,
Expand All @@ -44,6 +45,7 @@ export function ChatRoomPanelView({
onOpenMessages,
onOpenSidebar,
onOpenTaskDetails,
onRenameTitle,
onRerunWorkflow,
onSelectCommand,
onSelectOption,
Expand Down Expand Up @@ -93,11 +95,13 @@ export function ChatRoomPanelView({
isRerunDisabled={isRerunDisabled}
isRerunning={isRerunning}
isRerunVisible={isRerunVisible}
isRenamingTitle={isRenamingTitle}
title={selectedSession.title}
onOpenAction={onOpenAction}
onOpenMessages={onOpenMessages}
onOpenSidebar={onOpenSidebar}
onOpenTaskDetails={onOpenTaskDetails}
onRenameTitle={onRenameTitle}
onRerunWorkflow={onRerunWorkflow}
/>
{layout.contentMode === "taskDetails" ? (
Expand Down
58 changes: 21 additions & 37 deletions packages/web/src/components/chat-room/chat-room-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@ import { useRealtimeStore } from "@/lib/realtime";

import { useChatClarificationState } from "./chat-clarification-state";
import { createChatClarificationSubmitters } from "./chat-clarification-submitters";
import {
createStreamLine,
streamLineFromCommandEvent,
} from "./chat-command-stream-lines";
import { parseChatCommand } from "./chat-command-utils";
import { executeChatRoomInput } from "./chat-room-execute-input";
import { useChatRoomMission } from "./chat-room-mission";
import { useChatRoomDraftState } from "./chat-room-panel-draft-state";
import { ChatRoomPanelView } from "./chat-room-panel-view";
import { rerunChatSessionWorkflow } from "./chat-room-rerun-workflow";
import { selectChatSession } from "./chat-room-selection";
import { resolveChatRoomStreamState } from "./chat-room-stream-state";
import { resolveChatSessionRerunState } from "./chat-session-rerun-state";
import { createChatSessionTitleRenameHandler } from "./chat-session-title-rename";
import { useWorkingSectionState } from "./chat-working-section-state";
import type * as CRT from "./types/chat-room.types";
import { useChatRoomContentModeState } from "./use-chat-room-content-mode-state";
Expand Down Expand Up @@ -115,6 +113,11 @@ export function ChatRoomPanel({
selectedSession,
sendMessage: (input) => sendMessage.mutateAsync(input),
});
const renameSessionTitle = createChatSessionTitleRenameHandler({
renameSession: (input) => updateSession.mutateAsync(input),
selectedSession,
showError: (message) => toast.error(message),
});

async function startNewSession(): Promise<void> {
if (!workspaceId) {
Expand Down Expand Up @@ -173,39 +176,18 @@ export function ChatRoomPanel({
toast.error("Workflow cannot be rerun yet.");
return;
}
let finalStatus: "succeeded" | "failed" | "rejected" | null = null;
let finalError: string | undefined;
setIsRerunning(true);
setSubmittedRerunKey(rerunSubmissionKey);
setCommandLines([createStreamLine("system", "Queued workflow rerun.")]);
try {
await apiClient.streamCliCommand(rerunState.command, (event) => {
if (event.type === "complete") {
finalStatus = event.result.status;
finalError = event.result.error;
}
const line = streamLineFromCommandEvent(event);
if (line) setCommandLines((current) => [...current, line]);
});
await Promise.allSettled([
sessionsQuery.refetch(),
messagesQuery.refetch(),
refetchActiveTask(),
]);
if (finalStatus && finalStatus !== "succeeded") {
toast.error(finalError ?? "Workflow rerun failed.");
}
} catch (error) {
const message =
error instanceof Error ? error.message : "Workflow rerun failed.";
setCommandLines((current) => [
...current,
createStreamLine("stderr", message),
]);
toast.error(message);
} finally {
setIsRerunning(false);
}
await rerunChatSessionWorkflow({
command: rerunState.command,
refetchActiveTask,
refetchMessages: messagesQuery.refetch,
refetchSessions: sessionsQuery.refetch,
setCommandLines,
setIsRerunning,
setSubmittedRerunKey,
streamCliCommand: apiClient.streamCliCommand,
submissionKey: rerunSubmissionKey,
showError: (message) => toast.error(message),
});
}

return (
Expand All @@ -218,6 +200,7 @@ export function ChatRoomPanel({
isRerunDisabled={rerunState.isDisabled}
isRerunning={isRerunning}
isRerunVisible={rerunState.isVisible}
isRenamingTitle={updateSession.isPending}
isSending={sendMessage.isPending}
isPlanning={isPlanning}
isThinking={isThinking}
Expand All @@ -237,6 +220,7 @@ export function ChatRoomPanel({
onOpenMessages={contentMode.openMessages}
onOpenSidebar={onOpenSidebar}
onOpenTaskDetails={contentMode.openTaskDetails}
onRenameTitle={renameSessionTitle}
onRerunWorkflow={() => void handleRerunWorkflow()}
onSelectCommand={setDraft}
onSelectOption={(index, value) =>
Expand Down
68 changes: 68 additions & 0 deletions packages/web/src/components/chat-room/chat-room-rerun-workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { CliCommandStreamEvent, CliCommandStreamRequest } from "@/lib/api";
import type { Dispatch, SetStateAction } from "react";
import {
createStreamLine,
streamLineFromCommandEvent,
} from "./chat-command-stream-lines";
import type { ChatStreamLine } from "./types/chat-room.types";

export async function rerunChatSessionWorkflow({
command,
refetchActiveTask,
refetchMessages,
refetchSessions,
setCommandLines,
setIsRerunning,
setSubmittedRerunKey,
streamCliCommand,
submissionKey,
showError,
}: {
command: CliCommandStreamRequest;
refetchActiveTask: () => Promise<unknown>;
refetchMessages: () => Promise<unknown>;
refetchSessions: () => Promise<unknown>;
setCommandLines: Dispatch<SetStateAction<ChatStreamLine[]>>;
setIsRerunning: (value: boolean) => void;
setSubmittedRerunKey: (value: string) => void;
streamCliCommand: (
command: CliCommandStreamRequest,
onEvent: (event: CliCommandStreamEvent) => void,
) => Promise<unknown>;
submissionKey: string;
showError: (message: string) => void;
}): Promise<void> {
let finalStatus: "succeeded" | "failed" | "rejected" | null = null;
let finalError: string | undefined;
setIsRerunning(true);
setSubmittedRerunKey(submissionKey);
setCommandLines([createStreamLine("system", "Queued workflow rerun.")]);
try {
await streamCliCommand(command, (event) => {
if (event.type === "complete") {
finalStatus = event.result.status;
finalError = event.result.error;
}
const line = streamLineFromCommandEvent(event);
if (line) setCommandLines((current) => [...current, line]);
});
await Promise.allSettled([
refetchSessions(),
refetchMessages(),
refetchActiveTask(),
]);
if (finalStatus && finalStatus !== "succeeded") {
showError(finalError ?? "Workflow rerun failed.");
}
} catch (error) {
const message =
error instanceof Error ? error.message : "Workflow rerun failed.";
setCommandLines((current) => [
...current,
createStreamLine("stderr", message),
]);
showError(message);
} finally {
setIsRerunning(false);
}
}
34 changes: 34 additions & 0 deletions packages/web/src/components/chat-room/chat-session-title-rename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ChatSessionRecord, ChatSessionUpdateRequest } from "@/lib/api";

export function createChatSessionTitleRenameHandler({
renameSession,
selectedSession,
showError,
}: {
renameSession: (input: {
sessionId: string;
session: ChatSessionUpdateRequest;
}) => Promise<unknown>;
selectedSession: ChatSessionRecord | null;
showError: (message: string) => void;
}): (title: string) => Promise<boolean> {
return async (title) => {
const nextTitle = title.trim();
if (!selectedSession || !nextTitle) {
return false;
}
if (nextTitle === selectedSession.title.trim()) {
return true;
}
try {
await renameSession({
sessionId: selectedSession.id,
session: { title: nextTitle },
});
return true;
} catch (error) {
showError(error instanceof Error ? error.message : "Rename failed");
return false;
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ export interface ChatRoomHeaderProps {
isRerunDisabled: boolean;
isRerunning: boolean;
isRerunVisible: boolean;
isRenamingTitle: boolean;
title: string;
onOpenAction: () => void;
onOpenMessages: () => void;
onOpenSidebar: () => void;
onOpenTaskDetails: () => void;
onRenameTitle: (title: string) => Promise<boolean> | boolean;
onRerunWorkflow: () => void;
}

Expand Down Expand Up @@ -86,6 +88,7 @@ export interface ChatRoomPanelViewProps {
isRerunDisabled: boolean;
isRerunning: boolean;
isRerunVisible: boolean;
isRenamingTitle: boolean;
isSending: boolean;
isPlanning: boolean;
isThinking: boolean;
Expand All @@ -103,6 +106,7 @@ export interface ChatRoomPanelViewProps {
onOpenMessages: () => void;
onOpenSidebar: () => void;
onOpenTaskDetails: () => void;
onRenameTitle: (title: string) => Promise<boolean> | boolean;
onRerunWorkflow: () => void;
onSelectCommand: (value: string) => void;
onSelectOption: (index: number, value: string) => Promise<void> | void;
Expand Down
Loading
Loading