diff --git a/src/components/ai-chat/AIChatPanelContent.tsx b/src/components/ai-chat/AIChatPanelContent.tsx index 2eebbd69..06ceed75 100644 --- a/src/components/ai-chat/AIChatPanelContent.tsx +++ b/src/components/ai-chat/AIChatPanelContent.tsx @@ -1,6 +1,6 @@ -import { Suspense, lazy, useCallback } from "react"; +import { Suspense, lazy, useCallback, useLayoutEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useToast } from "@zedi/ui"; +import { cn, useToast } from "@zedi/ui"; import { AIChatHeader } from "./AIChatHeader"; import { AIChatViewTabs } from "./AIChatViewTabs"; import { AIChatInput } from "./AIChatInput"; @@ -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} 向けプロパティ。 @@ -87,6 +91,19 @@ export function AIChatPanelContent({ const canInsert = pageContext?.type === "editor"; + /** After the workflow tab is visited once, keep the panel mounted so run state survives tab switches. / ワークフロータブを一度開いたらマウントを維持し、タブ切替で実行状態を失わない */ + const [keepWorkflowMounted, setKeepWorkflowMounted] = useState( + () => activeViewTab === "workflow", + ); + useLayoutEffect(() => { + if (activeViewTab === "workflow") { + // Latch: after first visit to the workflow tab, keep the panel mounted so run/pause state survives tab switches. + // 初回表示後はマウントを維持し、タブ切替で実行状態を失わない。 + // eslint-disable-next-line react-hooks/set-state-in-effect -- one-way latch from tab selection (not external sync) + setKeepWorkflowMounted(true); + } + }, [activeViewTab]); + return (
@@ -116,7 +133,7 @@ export function AIChatPanelContent({ onSwitchBranch={switchBranch} isStreaming={isStreaming} /> - ) : ( + ) : activeViewTab === "branch" ? ( - )} + ) : null} + {keepWorkflowMounted ? ( +
+ + + +
+ ) : null}
-
+ {/* Stay mounted on workflow tab so uncontrolled input draft is not lost. / ワークフロー切替で下書きを失わない */} +
{ vi.clearAllMocks(); }); - it("renders Chat and Branch tabs", () => { + it("renders Chat, Branch, and Workflow tabs", () => { const onTabChange = vi.fn(); render(); 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", () => { @@ -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(); + await user.click(screen.getByRole("tab", { name: "aiChat.viewTabs.workflow" })); + expect(onTabChange).toHaveBeenCalledWith("workflow"); + }); }); diff --git a/src/components/ai-chat/AIChatViewTabs.tsx b/src/components/ai-chat/AIChatViewTabs.tsx index 2512a43d..b158b146 100644 --- a/src/components/ai-chat/AIChatViewTabs.tsx +++ b/src/components/ai-chat/AIChatViewTabs.tsx @@ -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}. @@ -43,6 +43,13 @@ export function AIChatViewTabs({ activeTab, onTabChange }: AIChatViewTabsProps) {t("aiChat.viewTabs.branch")} + + + {t("aiChat.viewTabs.workflow")} + ); diff --git a/src/components/ai-chat/AIChatWorkflowPanel.tsx b/src/components/ai-chat/AIChatWorkflowPanel.tsx new file mode 100644 index 00000000..a474adff --- /dev/null +++ b/src/components/ai-chat/AIChatWorkflowPanel.tsx @@ -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 ; +} diff --git a/src/components/ai-chat/WorkflowPanelForm.tsx b/src/components/ai-chat/WorkflowPanelForm.tsx new file mode 100644 index 00000000..e922fd63 --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelForm.tsx @@ -0,0 +1,135 @@ +/** + * 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; + + const canRunWorkflow = isTauriDesktop() && isEditor; + + return ( +
+
+ + {t("aiChat.workflow.subtitle")} +
+ + {!isTauriDesktop() && ( +

{t("aiChat.workflow.desktopOnly")}

+ )} + + +
+ + +
+
+ +
+ + + + +
+
+ ); +} diff --git a/src/components/ai-chat/WorkflowPanelMetaSection.tsx b/src/components/ai-chat/WorkflowPanelMetaSection.tsx new file mode 100644 index 00000000..ce08566c --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelMetaSection.tsx @@ -0,0 +1,174 @@ +/** + * Workflow name, templates, saved definitions, and import/export (Issue #462). + * ワークフロー名・テンプレート・保存定義・インポート/エクスポート(Issue #462)。 + */ + +import { + Button, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@zedi/ui"; +import { Download, Trash2, Upload } from "lucide-react"; +import { + WORKFLOW_TEMPLATE_IDS, + WORKFLOW_TEMPLATE_NAME_KEYS, + type WorkflowTemplateId, +} from "@/lib/workflow/templates"; +import type { WorkflowPanelFormProps } from "./workflowPanelTypes"; + +type Props = Pick< + WorkflowPanelFormProps, + | "t" + | "draft" + | "setDraft" + | "definitions" + | "selectedSavedId" + | "importInputRef" + | "running" + | "loadTemplate" + | "saveCustom" + | "exportJson" + | "onImportFile" + | "loadSaved" + | "deleteSaved" +>; + +/** + * Name field, template selector, saved workflow picker, and JSON import/export. + * 名前・テンプレート・保存済み選択・JSON インポート/エクスポート。 + */ +export function WorkflowPanelMetaSection(props: Props) { + const { + t, + draft, + setDraft, + definitions, + selectedSavedId, + importInputRef, + running, + loadTemplate, + saveCustom, + exportJson, + onImportFile, + loadSaved, + deleteSaved, + } = props; + + return ( + <> +
+ + setDraft((d) => ({ ...d, name: e.target.value, updatedAt: Date.now() }))} + placeholder={t("aiChat.workflow.workflowNamePlaceholder")} + disabled={running} + /> +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + + + +
+ + ); +} diff --git a/src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx b/src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx new file mode 100644 index 00000000..521b0cc2 --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx @@ -0,0 +1,115 @@ +/** + * Step editor list and run progress summary (Issue #462). + * ステップ編集リストと実行進捗サマリー(Issue #462)。 + */ + +import { Button, Input, Label } from "@zedi/ui"; +import { Plus, Trash2 } from "lucide-react"; +import type { WorkflowPanelFormProps } from "./workflowPanelTypes"; + +type Props = Pick< + WorkflowPanelFormProps, + | "t" + | "draft" + | "running" + | "progress" + | "activeRunSteps" + | "addStep" + | "removeStep" + | "updateStep" +>; + +/** + * Editable steps and optional streaming progress block. + * 編集可能なステップとストリーミング進捗ブロック。 + */ +export function WorkflowPanelStepsAndProgress(props: Props) { + const { t, draft, running, progress, activeRunSteps, addStep, removeStep, updateStep } = props; + + return ( + <> +
+
+ + +
+ + {draft.steps.map((step, index) => ( +
+
+ + {t("aiChat.workflow.stepLabel", { n: index + 1 })} + + +
+ updateStep(index, { title: e.target.value })} + /> +