Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
30 changes: 20 additions & 10 deletions src/components/ai-chat/AIChatPanelContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const AIChatBranchTree = lazy(() =>
import("./AIChatBranchTree").then((m) => ({ default: m.AIChatBranchTree })),
);

const AIChatWorkflowPanel = lazy(() =>
import("./AIChatWorkflowPanel").then((m) => ({ default: m.AIChatWorkflowPanel })),
);

/**
* Props for {@link AIChatPanelContent}.
* {@link AIChatPanelContent} 向けプロパティ。
Expand Down Expand Up @@ -116,7 +120,7 @@ export function AIChatPanelContent({
onSwitchBranch={switchBranch}
isStreaming={isStreaming}
/>
) : (
) : activeViewTab === "branch" ? (
<Suspense fallback={null}>
<AIChatBranchTree
messageMap={messageMap}
Expand All @@ -127,18 +131,24 @@ export function AIChatPanelContent({
onDeleteBranch={handleDeleteBranchFromTree}
/>
</Suspense>
) : (
<Suspense fallback={null}>
<AIChatWorkflowPanel />
</Suspense>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
)}
</div>

<div className="bg-background border-t p-4">
<AIChatInput
onSendMessage={handleSendMessage}
onStopStreaming={stopStreaming}
prefillText={inputPrefill?.text}
prefillNonce={inputPrefill?.nonce}
focusEditorNonce={focusEditorNonce}
/>
</div>
{activeViewTab !== "workflow" && (
<div className="bg-background border-t p-4">
<AIChatInput
onSendMessage={handleSendMessage}
onStopStreaming={stopStreaming}
prefillText={inputPrefill?.text}
prefillNonce={inputPrefill?.nonce}
focusEditorNonce={focusEditorNonce}
/>
</div>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</div>
);
}
11 changes: 10 additions & 1 deletion src/components/ai-chat/AIChatViewTabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ describe("AIChatViewTabs", () => {
vi.clearAllMocks();
});

it("renders Chat and Branch tabs", () => {
it("renders Chat, Branch, and Workflow tabs", () => {
const onTabChange = vi.fn();
render(<AIChatViewTabs activeTab="chat" onTabChange={onTabChange} />);
expect(screen.getByRole("tab", { name: "aiChat.viewTabs.chat" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "aiChat.viewTabs.branch" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "aiChat.viewTabs.workflow" })).toBeInTheDocument();
});

it("Branch tab is always enabled", () => {
Expand All @@ -47,4 +48,12 @@ describe("AIChatViewTabs", () => {
await user.click(screen.getByRole("tab", { name: "aiChat.viewTabs.chat" }));
expect(onTabChange).toHaveBeenCalledWith("chat");
});

it("calls onTabChange with workflow when Workflow tab is clicked", async () => {
const user = userEvent.setup();
const onTabChange = vi.fn();
render(<AIChatViewTabs activeTab="chat" onTabChange={onTabChange} />);
await user.click(screen.getByRole("tab", { name: "aiChat.viewTabs.workflow" }));
expect(onTabChange).toHaveBeenCalledWith("workflow");
});
});
11 changes: 9 additions & 2 deletions src/components/ai-chat/AIChatViewTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useTranslation } from "react-i18next";
import { MessageSquare, GitBranch } from "lucide-react";
import { MessageSquare, GitBranch, ListChecks } from "lucide-react";
import { Tabs, TabsList, TabsTrigger } from "@zedi/ui";

/**
* Active AI chat panel view: threaded messages or branch tree.
* AI チャットパネルの表示:スレッド表示かブランチツリーか。
*/
export type AIChatViewTab = "chat" | "branch";
export type AIChatViewTab = "chat" | "branch" | "workflow";

/**
* Props for {@link AIChatViewTabs}.
Expand Down Expand Up @@ -43,6 +43,13 @@ export function AIChatViewTabs({ activeTab, onTabChange }: AIChatViewTabsProps)
<GitBranch className="h-3.5 w-3.5" />
{t("aiChat.viewTabs.branch")}
</TabsTrigger>
<TabsTrigger
value="workflow"
className="flex h-7 items-center gap-1.5 px-3 text-xs data-[state=active]:shadow-sm"
>
<ListChecks className="h-3.5 w-3.5" />
{t("aiChat.viewTabs.workflow")}
</TabsTrigger>
</TabsList>
</Tabs>
);
Expand Down
16 changes: 16 additions & 0 deletions src/components/ai-chat/AIChatWorkflowPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Multi-step Claude Code workflow UI (Issue #462).
* Claude Code マルチステップワークフロー UI(Issue #462)。
*/

import { useWorkflowPanelLogic } from "@/hooks/useWorkflowPanelLogic";
import { WorkflowPanelForm } from "./WorkflowPanelForm";

/**
* Workflow editor and runner embedded in the AI chat panel.
* AI チャットパネルに埋め込むワークフロー編集・実行。
*/
export function AIChatWorkflowPanel() {
const logic = useWorkflowPanelLogic();
return <WorkflowPanelForm {...logic} />;
}
133 changes: 133 additions & 0 deletions src/components/ai-chat/WorkflowPanelForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Form layout for the workflow panel (Issue #462).
* ワークフローパネルのフォームレイアウト(Issue #462)。
*/

import { Button, ScrollArea } from "@zedi/ui";
import { ListChecks, Pause, Play, Square } from "lucide-react";
import { isTauriDesktop } from "@/lib/platform";
import { WorkflowPanelMetaSection } from "./WorkflowPanelMetaSection";
import { WorkflowPanelStepsAndProgress } from "./WorkflowPanelStepsAndProgress";
import type { WorkflowPanelFormProps } from "./workflowPanelTypes";

export type { WorkflowPanelFormProps } from "./workflowPanelTypes";

/**
* Renders workflow name, templates, steps, progress, and run controls.
* ワークフロー名・テンプレート・ステップ・進捗・実行操作を描画する。
*/
export function WorkflowPanelForm(props: WorkflowPanelFormProps) {
const {
t,
draft,
setDraft,
definitions,
selectedSavedId,
progress,
activeRunSteps,
pausedState,
importInputRef,
isEditor,
running,
runExecution,
handlePause,
handleStop,
addStep,
removeStep,
updateStep,
loadTemplate,
saveCustom,
exportJson,
onImportFile,
loadSaved,
deleteSaved,
} = props;

return (
<div className="flex h-full min-h-0 flex-col gap-3 p-3">
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<ListChecks className="h-4 w-4 shrink-0" />
<span>{t("aiChat.workflow.subtitle")}</span>
</div>

{!isTauriDesktop() && (
<p className="text-destructive text-xs">{t("aiChat.workflow.desktopOnly")}</p>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<ScrollArea className="min-h-0 flex-1 pr-2">
<div className="flex flex-col gap-3 pb-2">
<WorkflowPanelMetaSection
t={t}
draft={draft}
setDraft={setDraft}
definitions={definitions}
selectedSavedId={selectedSavedId}
importInputRef={importInputRef}
running={running}
loadTemplate={loadTemplate}
saveCustom={saveCustom}
exportJson={exportJson}
onImportFile={onImportFile}
loadSaved={loadSaved}
deleteSaved={deleteSaved}
/>
<WorkflowPanelStepsAndProgress
t={t}
draft={draft}
running={running}
progress={progress}
activeRunSteps={activeRunSteps}
addStep={addStep}
removeStep={removeStep}
updateStep={updateStep}
/>
</div>
</ScrollArea>

<div className="border-border flex flex-wrap gap-2 border-t pt-2">
<Button
type="button"
size="sm"
className="h-8 text-xs"
disabled={running || !isEditor}
onClick={() => void runExecution("fresh")}
>
<Play className="mr-1 h-3 w-3" />
{t("aiChat.workflow.run")}
</Button>
<Button
type="button"
variant="secondary"
size="sm"
className="h-8 text-xs"
disabled={!running}
onClick={handlePause}
>
<Pause className="mr-1 h-3 w-3" />
{t("aiChat.workflow.pause")}
</Button>
<Button
type="button"
variant="secondary"
size="sm"
className="h-8 text-xs"
disabled={running || !pausedState}
onClick={() => void runExecution("resume")}
>
{t("aiChat.workflow.resume")}
</Button>
<Button
type="button"
variant="destructive"
size="sm"
className="h-8 text-xs"
disabled={!running}
onClick={handleStop}
>
<Square className="mr-1 h-3 w-3" />
{t("aiChat.workflow.stop")}
</Button>
</div>
</div>
);
}
Loading
Loading