From 87ee013d787bdd4f652b7ed6c4b3ce79c4e3a703 Mon Sep 17 00:00:00 2001 From: otomatty Date: Sun, 5 Apr 2026 09:21:39 +0900 Subject: [PATCH 1/5] feat(ai-chat): add Claude Code multi-step workflow panel (#462) - Workflow tab, templates, save/import JSON, run/pause/resume/stop - streamClaudeQuery + runWorkflowExecution; note updates via editor handler - parseWorkflowDefinitionImport for safe JSON import Made-with: Cursor --- src/components/ai-chat/AIChatPanelContent.tsx | 30 +- .../ai-chat/AIChatViewTabs.test.tsx | 11 +- src/components/ai-chat/AIChatViewTabs.tsx | 11 +- .../ai-chat/AIChatWorkflowPanel.tsx | 16 + src/components/ai-chat/WorkflowPanelForm.tsx | 133 +++++++++ .../ai-chat/WorkflowPanelMetaSection.tsx | 166 +++++++++++ .../ai-chat/WorkflowPanelStepsAndProgress.tsx | 110 +++++++ src/components/ai-chat/workflowPanelTypes.ts | 12 + src/hooks/useWorkflowDraft.ts | 163 ++++++++++ src/hooks/useWorkflowPanelLogic.ts | 21 ++ src/hooks/useWorkflowRunSession.ts | 180 +++++++++++ src/hooks/workflowRunOutcomeHandlers.ts | 71 +++++ src/i18n/locales/en/aiChat.json | 44 +++ src/i18n/locales/ja/aiChat.json | 44 +++ .../claudeCode/runQueryToCompletion.test.ts | 2 + src/lib/claudeCode/runQueryToCompletion.ts | 160 +--------- src/lib/claudeCode/streamClaudeQuery.ts | 205 +++++++++++++ .../workflow/buildWorkflowStepPrompt.test.ts | 44 +++ src/lib/workflow/buildWorkflowStepPrompt.ts | 80 +++++ .../formatWorkflowNoteMarkdown.test.ts | 32 ++ .../workflow/formatWorkflowNoteMarkdown.ts | 57 ++++ src/lib/workflow/newWorkflowId.ts | 15 + .../parseWorkflowDefinitionImport.test.ts | 48 +++ .../workflow/parseWorkflowDefinitionImport.ts | 88 ++++++ src/lib/workflow/runWorkflowExecution.test.ts | 91 ++++++ src/lib/workflow/runWorkflowExecution.ts | 280 ++++++++++++++++++ src/lib/workflow/templates.ts | 148 +++++++++ src/lib/workflow/types.ts | 67 +++++ src/stores/workflowDefinitionsStore.ts | 39 +++ 29 files changed, 2200 insertions(+), 168 deletions(-) create mode 100644 src/components/ai-chat/AIChatWorkflowPanel.tsx create mode 100644 src/components/ai-chat/WorkflowPanelForm.tsx create mode 100644 src/components/ai-chat/WorkflowPanelMetaSection.tsx create mode 100644 src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx create mode 100644 src/components/ai-chat/workflowPanelTypes.ts create mode 100644 src/hooks/useWorkflowDraft.ts create mode 100644 src/hooks/useWorkflowPanelLogic.ts create mode 100644 src/hooks/useWorkflowRunSession.ts create mode 100644 src/hooks/workflowRunOutcomeHandlers.ts create mode 100644 src/lib/claudeCode/streamClaudeQuery.ts create mode 100644 src/lib/workflow/buildWorkflowStepPrompt.test.ts create mode 100644 src/lib/workflow/buildWorkflowStepPrompt.ts create mode 100644 src/lib/workflow/formatWorkflowNoteMarkdown.test.ts create mode 100644 src/lib/workflow/formatWorkflowNoteMarkdown.ts create mode 100644 src/lib/workflow/newWorkflowId.ts create mode 100644 src/lib/workflow/parseWorkflowDefinitionImport.test.ts create mode 100644 src/lib/workflow/parseWorkflowDefinitionImport.ts create mode 100644 src/lib/workflow/runWorkflowExecution.test.ts create mode 100644 src/lib/workflow/runWorkflowExecution.ts create mode 100644 src/lib/workflow/templates.ts create mode 100644 src/lib/workflow/types.ts create mode 100644 src/stores/workflowDefinitionsStore.ts diff --git a/src/components/ai-chat/AIChatPanelContent.tsx b/src/components/ai-chat/AIChatPanelContent.tsx index 2eebbd69..8539bd28 100644 --- a/src/components/ai-chat/AIChatPanelContent.tsx +++ b/src/components/ai-chat/AIChatPanelContent.tsx @@ -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} 向けプロパティ。 @@ -116,7 +120,7 @@ export function AIChatPanelContent({ onSwitchBranch={switchBranch} isStreaming={isStreaming} /> - ) : ( + ) : activeViewTab === "branch" ? ( + ) : ( + + + )} -
- -
+ {activeViewTab !== "workflow" && ( +
+ +
+ )} ); } diff --git a/src/components/ai-chat/AIChatViewTabs.test.tsx b/src/components/ai-chat/AIChatViewTabs.test.tsx index b21ac763..4506671a 100644 --- a/src/components/ai-chat/AIChatViewTabs.test.tsx +++ b/src/components/ai-chat/AIChatViewTabs.test.tsx @@ -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(); 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..bb06471a --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelForm.tsx @@ -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 ( +
+
+ + {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..6cdaca00 --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelMetaSection.tsx @@ -0,0 +1,166 @@ +/** + * 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..a2c0e4a1 --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx @@ -0,0 +1,110 @@ +/** + * 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 })} + /> +