返回 DeepSeek-Reasonix
useProjectTopicCommands.ts
根目录 / desktop / frontend / src / app-runtime / useProjectTopicCommands.ts
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
64 lines TYPESCRIPT