import type { ComponentType, ReactNode } from "react"; import type { GloomPlugin, PaneProps } from "../../../types/plugin"; import { apiClient } from "../../../api-client"; import { createGloomberbCloudCapabilities, createGloomberbCloudProvider } from "../../../sources/gloomberb-cloud"; import { AccountManagementPane } from "../account-management/pane"; import { chatController } from "../chat/controller"; import { chatSidebarStore } from "../chat/sidebar-store"; import { buildDmCommandResults, formatChatPaneTitle, getPreferredChatOpenChannelId, hasOnlyDmUsernameArgs, normalizeShortcutChannelId, openDmTargetFromCommand, parseDmUsernames, } from "../chat/channels"; import { CONGRESS_TRADES_PANE_ID, CongressPane, } from "../congress-trades/pane"; import { congressHeadless } from "../congress-trades/headless"; import { registerTwitterFeedFeature } from "../cloud-tweets/registration"; import { composeBuiltinPlugin, type PluginModule } from "../plugin-module"; import { askgConversationListStore } from "./askg/conversation-store"; import { ASKG_PANE_ID, ASKGPane } from "./askg/pane"; import { askGloomQuestion } from "./askg/pending-question"; import { registerCloudAuthCommands } from "./auth-commands"; import { registerCloudUpgradeCommand } from "./upgrade-command"; import { CloudUpgradeStatusWidget } from "./upgrade-status-widget"; import { CloudVerificationStatusWidget } from "./verification-status-widget"; import { createPublicPaneShare } from "../shared/public-pane"; import { teamChannelId } from "./team/model"; import { teamModule, teamStore } from "./team/module"; import { thesisModule } from "./thesis/module"; interface GloomberbCloudPluginComponents { ChatPane: (props: PaneProps) => ReactNode; ChatStatusWidget: ComponentType; extraModules?: readonly PluginModule[]; } function createCloudDataModule(): PluginModule { return { capabilities: createGloomberbCloudCapabilities(createGloomberbCloudProvider()), setup(ctx) { ctx.registerSyncTransport({ id: "gloomberb-cloud", isAvailable: () => apiClient.isVerified(), pullSnapshot: () => apiClient.getSyncSnapshot(), pushSnapshot: (snapshot, options) => apiClient.putSyncSnapshot(snapshot, options), }); }, dispose() { apiClient.dispose(); }, }; } function createChatModule( ChatPane: GloomberbCloudPluginComponents["ChatPane"], ChatStatusWidget: GloomberbCloudPluginComponents["ChatStatusWidget"], ): PluginModule { return { panes: [{ id: "chat", name: "Chat", icon: "C", component: ChatPane, defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 80, height: 30 }, portableShare: { private: { title: true, params: true, settings: true, state: true }, }, }], paneTemplates: [{ id: "new-chat-pane", paneId: "chat", label: "New Chat Pane", description: "Open the floating chat window for a channel", keywords: ["new", "chat", "pane", "message"], shortcut: { prefix: "CHAT", argPlaceholder: "channel", argKind: "text" }, createInstance: async (context, options) => { // `CHAT MD` or `CHAT "Macro Desk"` opens that team's #general. A raw // channel id (team ids are mixed case) is kept as typed. const rawArg = options?.arg?.trim() ?? ""; const team = rawArg ? teamStore.findTeam(rawArg) : null; const channelId = team ? teamChannelId(team.id) : rawArg && chatController.getChannels().some((entry) => entry.id === rawArg) ? rawArg : rawArg ? await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(rawArg)) : await chatController.resolvePreferredChannelId( getPreferredChatOpenChannelId(context.config, chatController.getSnapshot()), ); const channel = chatController.getChannels().find((entry) => entry.id === channelId); const targetMessageId = options?.values?.messageId?.trim() || null; return { placement: "floating", // One pane per channel: re-opening the same channel focuses the pane // that already holds it, even though its channelId setting drifts as // the user switches channels inside the pane. A jump to a specific // message stays unkeyed so it never lands on a pane that already // scrolled past the target. ...(targetMessageId ? {} : { instanceId: `chat:${channelId}` }), title: formatChatPaneTitle(channel, channelId), settings: { channelId, ...(targetMessageId ? { targetMessageId } : {}), }, }; }, }], slots: { "status:widget": () => , }, setup(ctx) { chatController.attachPersistence(ctx.persistence, ctx.resume); chatSidebarStore.attach(ctx.persistence); chatController.setNotifier(ctx.notify, (channelId, messageId) => { ctx.createPaneFromTemplate("new-chat-pane", { arg: channelId, values: { messageId } }); }); ctx.registerCommand({ id: "direct-message", label: "DM", description: "Open an existing DM or start a direct/group chat", keywords: ["dm", "direct", "message", "group", "chat"], category: "navigation", shortcut: "DM", shortcutArg: { placeholder: "@username [@username...]", kind: "text", parse: (arg) => ({ participants: arg.trim() }), }, buildResults: (arg) => buildDmCommandResults(ctx, arg), execute: async (values) => { const participants = values?.participants ?? values?.shortcut ?? ""; const usernames = parseDmUsernames(participants); if (participants.trim() && !hasOnlyDmUsernameArgs(participants)) { throw new Error("Use @username, or multiple usernames for a group chat."); } await openDmTargetFromCommand(ctx, usernames); }, }); }, dispose() { chatController.dispose(); }, }; } const accountModule: PluginModule = { panes: [{ id: "account-management", name: "ACM", icon: "A", component: AccountManagementPane, defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 72, height: 36 }, portableShare: { private: { title: true, params: true, settings: true, state: true }, }, }], paneTemplates: [{ id: "account-management-pane", paneId: "account-management", label: "Account Management", description: "Edit your Gloom Cloud profile, password, and public portfolio sharing settings", keywords: ["account", "profile", "cloud", "acm", "password", "settings"], shortcut: { prefix: "ACM" }, createInstance: () => ({ placement: "floating" }), }], slots: { "status:widget": () => ( <> ), }, setup: (ctx) => { registerCloudAuthCommands(ctx); registerCloudUpgradeCommand(ctx); }, }; const askgModule: PluginModule = { setup(ctx) { // Only the dragged sidebar width is per device; the conversations // themselves belong to the account and are read from the cloud. askgConversationListStore.attach(ctx.persistence); }, panes: [{ id: ASKG_PANE_ID, name: "Ask Gloom", icon: "K", component: ASKGPane, defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 96, height: 32 }, portableShare: { // A conversation and the panes it read are personal to the account. private: { title: true, params: true, settings: true, state: true }, }, }], paneTemplates: [{ id: "askg-pane", paneId: ASKG_PANE_ID, label: "Ask Gloom", description: "Ask a question about your panes and watch the tools Gloom runs", keywords: ["ask", "gloom", "assistant", "ai", "question", "askg"], shortcut: { prefix: "ASKG", argPlaceholder: "question", argKind: "text" }, createInstance: (_context, options) => { // The question is handed to the pane directly, so it is never persisted // with the layout and never replayed on the next launch. askGloomQuestion(options?.arg ?? ""); // One assistant: a second question focuses the conversation already open // and asks there instead of stacking another pane. return { placement: "floating", instanceId: "askg:main" }; }, }], }; const congressTradesModule: PluginModule = { panes: [{ id: CONGRESS_TRADES_PANE_ID, name: "Congress", icon: "G", component: CongressPane, defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 112, height: 30 }, tableExport: true, }], paneTemplates: [{ id: "congress-trades-pane", paneId: CONGRESS_TRADES_PANE_ID, label: "Congress Trades", description: "Track newly disclosed House and Senate periodic transaction reports.", keywords: ["congress", "house", "senate", "trades", "ptr", "stock", "disclosures"], shortcut: { prefix: "CG", argPlaceholder: "ticker", argKind: "ticker", argOptional: true }, headless: congressHeadless, createInstance: (_context, options) => { const symbol = (options?.symbol ?? options?.arg)?.trim().toUpperCase(); return symbol ? { instanceId: `${CONGRESS_TRADES_PANE_ID}:${symbol}`, title: `Congress ${symbol}`, placement: "floating", settings: { ticker: symbol } } : { placement: "floating" }; }, publicShare: createPublicPaneShare("Congress Trades"), }], }; const twitterModule: PluginModule = { setup: registerTwitterFeedFeature, }; export function createGloomberbCloudPlugin({ ChatPane, ChatStatusWidget, extraModules = [], }: GloomberbCloudPluginComponents): GloomPlugin { return composeBuiltinPlugin({ id: "gloomberb-cloud", name: "Gloom Cloud", version: "1.0.0", description: "Free market, macro, and chat services. Chat requires signup.", toggleable: true, order: 10, modules: [ createCloudDataModule(), createChatModule(ChatPane, ChatStatusWidget), teamModule, thesisModule, accountModule, askgModule, ...extraModules, congressTradesModule, twitterModule, ], }); }