返回 DeepSeek-Reasonix
TopicbarActionsStack.tsx
根目录 / desktop / frontend / src / app-shell / TopicbarActionsStack.tsx
1 import { lazy, Suspense, type ReactNode } from "react";
2 import { TopicbarActionsRegion } from "./TopicbarActionsRegion";
3 import { shouldMountExternalOpener } from "../components/ExternalOpener";
4 import { tabWorkspaceTitle, topicDisplayTitle, topicTitle } from "../lib/sessionTitles";
5 import { sidebarImScopeLabel, type SidebarImTopicSource, type SidebarImConnection } from "../app-runtime/sidebarImProjection";
6 import type { TopicbarView } from "./TopicbarRegion";
7 import type { TabMeta } from "../lib/types";
8 import type { Translator } from "../lib/i18n";
9 import type { SessionExportFormat } from "../app-runtime/useSessionExportCommands";
10
11 const TaskMonitorPanel = lazy(() => import("../components/TaskMonitorPanel").then((module) => ({ default: module.TaskMonitorPanel })));
12
13 /** Topicbar view projection: IM/bot detail identity, workspace label/subtitle,
14 * worktree merge entry and rename gating. Pure function of the committed tab
15 * and preferences; ownership stays in the caller. */
16 export function buildTopicbarView(input: {
17 t: Translator;
18 locale: string;
19 activeTab: TabMeta | undefined;
20 cwd: string | undefined;
21 imDetail: SidebarImConnection | null;
22 imTopicSources: Record<string, SidebarImTopicSource>;
23 chromeHidden: boolean;
24 windowsBrand: boolean;
25 automationReturn: boolean;
26 sidebar: { title: string; blocked: boolean; pressed: boolean; collapsed: boolean };
27 rename: { editing: boolean; draft: string };
28 draft?: { title: string; workspaceLabel: string };
29 }): TopicbarView {
30 const { t, locale, activeTab, imDetail } = input;
31 const topicbarTitle = input.draft?.title ?? (imDetail ? t("botDetail.title", { name: imDetail.title }) : topicDisplayTitle(activeTab));
32 const topicbarWorkspaceLabel = input.draft?.workspaceLabel ?? (imDetail ? t("botDetail.subtitle") : activeTab ? tabWorkspaceTitle(activeTab) : "");
33 const topicbarWorkspacePath = activeTab?.scope === "project" ? activeTab.workspaceRoot || input.cwd : "";
34 const topicbarImSource = activeTab?.scope === "global" && activeTab.topicId ? input.imTopicSources[activeTab.topicId] : undefined;
35 const topicbarImSourceLabel = imDetail
36 ? imDetail.platformLabel
37 : topicbarImSource ? t("msg.fromIm", { source: topicbarImSource.label }) : "";
38 const topicbarImSourcePlatform = imDetail?.platform ?? topicbarImSource?.platform;
39 const topicbarSubtitleVisible = Boolean(activeTab?.isolatedWorktree || topicbarImSourceLabel);
40 const topicbarSubtitleTitle = imDetail
41 ? [topicbarWorkspaceLabel, topicbarImSourceLabel, sidebarImScopeLabel(imDetail, t)].filter(Boolean).join(" · ")
42 : [topicbarWorkspacePath || topicbarWorkspaceLabel, topicbarImSourceLabel].filter(Boolean).join(" · ");
43 const topicbarCanRename = !input.draft && !imDetail && (Boolean(activeTab?.topicId) || Boolean(activeTab?.remote));
44 return {
45 automationReturn: input.automationReturn,
46 automationReturnLabel: locale === "en" ? "Back to automation" : locale === "zh-TW" ? "返回自動化" : "返回自动化",
47 chromeHidden: input.chromeHidden,
48 brand: input.windowsBrand,
49 sidebar: input.sidebar,
50 title: { text: topicbarTitle, hover: !topicbarCanRename && imDetail ? topicbarTitle : topicTitle(activeTab),
51 renameLabel: t("topicBar.renameSession"), editing: input.rename.editing, draft: input.rename.draft,
52 canRename: topicbarCanRename, workspaceLabel: topicbarWorkspaceLabel },
53 subtitle: { visible: topicbarSubtitleVisible, title: topicbarSubtitleTitle, worktreeTabId: activeTab?.isolatedWorktree ? activeTab.id : undefined,
54 mergeLabel: t("worktree.mergeAction"), mergeTooltip: t("worktree.mergeButtonTooltip"),
55 sourcePlatform: topicbarImSourcePlatform, sourceLabel: topicbarImSourceLabel },
56 };
57 }
58
59 /** The topicbar actions stack: per-session actions, the dock toggle and the
60 * task-monitor popover. Pure prop-driven. */
61 export function TopicbarActionsStack(props: {
62 t: Translator;
63 activeTab: TabMeta | undefined;
64 activeTabId: string | undefined;
65 imDetailActive: boolean;
66 dismissSignal: number;
67 sessionHasContent: boolean;
68 exportCommands: {
69 getSessionMarkdown: () => Promise<string>;
70 exportSession: (format: SessionExportFormat) => Promise<void>;
71 };
72 terminal: { toggle: () => void; enabled: boolean; open: boolean; prefetch: () => void };
73 tasksOpen: false | "session" | "all";
74 setTasksOpen: (update: (open: false | "session" | "all") => false | "session" | "all") => void;
75 onCloseTasks: () => void;
76 onOpenTaskSession: (tabID: string, taskID: string) => Promise<boolean>;
77 dockToggle: ReactNode;
78 launcherToggle?: ReactNode;
79 }) {
80 const { t, activeTab, imDetailActive } = props;
81 return (
82 <div className="topicbar__actions">
83 {props.launcherToggle}
84 <TopicbarActionsRegion sessionIdentity={activeTab?.id}
85 external={shouldMountExternalOpener(activeTab, imDetailActive) && activeTab
86 ? { tabId: activeTab.id, dismissSignal: props.dismissSignal } : undefined}
87 session={!imDetailActive ? {
88 sessionHasContent: props.sessionHasContent,
89 getSessionMarkdown: props.exportCommands.getSessionMarkdown,
90 exportSession: (format) => void props.exportCommands.exportSession(format),
91 toggleTerminal: props.terminal.toggle, terminalEnabled: props.terminal.enabled,
92 terminalOpen: props.terminal.open, prefetchTerminal: props.terminal.prefetch,
93 openSessionSummary: () => props.setTasksOpen((open) => open ? false : "session"), tasksOpen: Boolean(props.tasksOpen),
94 } : undefined}
95 />
96 {props.dockToggle}
97 {props.tasksOpen && (
98 <div className="taskmonitor-popover" role="dialog" aria-label={t("summary.session")}>
99 <Suspense fallback={null}>
100 <TaskMonitorPanel
101 key={`${activeTab?.id || props.activeTabId || "none"}:${activeTab?.workspaceRoot || "global"}:${activeTab?.sessionPath || ""}:${props.tasksOpen}`}
102 tabID={activeTab?.id || props.activeTabId || ""}
103 initialOpen initialScope={props.tasksOpen || "session"}
104 popover
105 summaryMode
106 onClose={props.onCloseTasks}
107 onOpenSession={props.onOpenTaskSession}
108 />
109 </Suspense>
110 </div>
111 )}
112 </div>
113 );
114 }
115
115 lines Plain Text