| 1 | import { useLayoutEffect, useRef, useState } from "react"; |
| 2 | import { useCommittedCommand } from "../lib/useCommittedCommand"; |
| 3 | import { useResourceOperations, type SessionResource } from "./useResourceOperations"; |
| 4 | import { refreshProjectTopics, renameProjectTopic, type ProjectTopicPorts, type TopicRenameTarget } from "./projectTopicOwner"; |
| 5 | |
| 6 | type Input = { |
| 7 | visible: SessionResource; |
| 8 | topic?: { id: string; title: string; target: TopicRenameTarget }; |
| 9 | ports: ProjectTopicPorts; |
| 10 | navigation: { |
| 11 | openBlank: (scope: string, workspace: string) => Promise<void>; |
| 12 | enqueue: (request: { kind: "isolated-worktree"; workspaceRoot: string }) => Promise<void>; |
| 13 | switchFolder: (path?: string) => Promise<unknown>; |
| 14 | }; |
| 15 | reportError: (error: unknown) => void; |
| 16 | }; |
| 17 | type RenameDraft = { id: string; target: TopicRenameTarget; title: string }; |
| 18 | |
| 19 | /** Project commands retain only committed ports and one explicitly targeted draft. */ |
| 20 | export function useProjectTopicCommands(input: Input) { |
| 21 | const operations = useResourceOperations({ visible: { tabId: input.visible.tabId || "application", sessionKey: input.visible.sessionKey } }); |
| 22 | const [draft, setDraft] = useState<RenameDraft | null>(null); |
| 23 | const handled = useRef(false); |
| 24 | const activeIdentity = input.topic ? JSON.stringify([input.topic.id, input.topic.target]) : ""; |
| 25 | const draftIdentity = draft ? JSON.stringify([draft.id, draft.target]) : ""; |
| 26 | useLayoutEffect(() => { |
| 27 | if (draftIdentity && draftIdentity !== activeIdentity) { handled.current = true; setDraft(null); } |
| 28 | }, [activeIdentity, draftIdentity]); |
| 29 | const report = useCommittedCommand(input.reportError); |
| 30 | const rename = useCommittedCommand(async (target: TopicRenameTarget, title: string) => { |
| 31 | if (!title.trim()) return; |
| 32 | const outcome = await operations({ kind: "workspace", workspaceKey: JSON.stringify(target) }, "topic-rename", |
| 33 | { target, title: title.trim(), activeTabId: input.visible.tabId, ports: input.ports }, renameProjectTopic); |
| 34 | if (outcome.status === "failed") report(outcome.error); |
| 35 | }); |
| 36 | const renameTopic = useCommittedCommand((topicId: string, title: string) => input.topic?.target.kind === "local" && input.topic.id === topicId |
| 37 | ? rename(input.topic.target, title) : Promise.resolve()); |
| 38 | const refreshProjectsAndTabs = useCommittedCommand(async () => { |
| 39 | const outcome = await operations({ kind: "application" }, "project-refresh", { activeTabId: input.visible.tabId, ports: input.ports }, refreshProjectTopics); |
| 40 | if (outcome.status === "failed") report(outcome.error); |
| 41 | }); |
| 42 | const startActiveTopicRename = useCommittedCommand(() => { |
| 43 | if (!input.topic) return; |
| 44 | handled.current = false; |
| 45 | setDraft({ ...input.topic }); |
| 46 | }); |
| 47 | const cancelActiveTopicRename = useCommittedCommand(() => { handled.current = true; setDraft(null); }); |
| 48 | const commitActiveTopicRename = useCommittedCommand(async () => { |
| 49 | if (!draft || handled.current) return; |
| 50 | handled.current = true; |
| 51 | setDraft(null); |
| 52 | await rename(draft.target, draft.title); |
| 53 | }); |
| 54 | const setTopicTitleDraft = useCommittedCommand((title: string) => setDraft(current => current ? { ...current, title } : current)); |
| 55 | const onCreateTopic = useCommittedCommand((scope: string, workspace: string) => input.navigation.openBlank(scope, workspace)); |
| 56 | const onCreateIsolatedWorktree = useCommittedCommand((workspaceRoot: string) => input.navigation.enqueue({ kind: "isolated-worktree", workspaceRoot })); |
| 57 | const onAddProject = useCommittedCommand(async (path?: string) => { await input.navigation.switchFolder(path); }); |
| 58 | return { |
| 59 | topicTitleDraft: draft?.title ?? "", topicbarEditing: Boolean(draft && draftIdentity === activeIdentity), |
| 60 | setTopicTitleDraft, startActiveTopicRename, cancelActiveTopicRename, commitActiveTopicRename, |
| 61 | renameTopic, refreshProjectsAndTabs, onCreateTopic, onCreateIsolatedWorktree, onAddProject, |
| 62 | }; |
| 63 | } |
| 64 |