| 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 |