gloomberb 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -112,38 +112,6 @@ async function pressKey(key: string) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
describe("BrokersPane", () => {
|
|
115
|
-
test("shows empty state and opens add broker flow", async () => {
|
|
116
|
-
const calls: string[] = [];
|
|
117
|
-
testSetup = await testRender(<Harness calls={calls} />, { width: 92, height: 24 });
|
|
118
|
-
await act(async () => {
|
|
119
|
-
await testSetup!.renderOnce();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const frame = testSetup.captureCharFrame();
|
|
123
|
-
expect(frame).toContain("No broker profiles.");
|
|
124
|
-
expect(frame).not.toContain("Press a or click Add Broker.");
|
|
125
|
-
await act(async () => {
|
|
126
|
-
testSetup!.mockInput.pressKey("a");
|
|
127
|
-
await testSetup!.renderOnce();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
expect(calls).toEqual(["command:Add Broker Account"]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("hides unavailable footer actions with no broker profile", async () => {
|
|
134
|
-
const calls: string[] = [];
|
|
135
|
-
testSetup = await testRender(<FooterHarness calls={calls} />, { width: 92, height: 25 });
|
|
136
|
-
await act(async () => {
|
|
137
|
-
await testSetup!.renderOnce();
|
|
138
|
-
await testSetup!.renderOnce();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const frame = testSetup.captureCharFrame();
|
|
142
|
-
expect(frame).toContain("[a]dd");
|
|
143
|
-
expect(frame).not.toContain("[c]onnect");
|
|
144
|
-
expect(frame).not.toContain("[d]isconnect");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
115
|
test("renders IBKR row and invokes broker actions", async () => {
|
|
148
116
|
const calls: string[] = [];
|
|
149
117
|
testSetup = await testRender(<FooterHarness calls={calls} instance={createGatewayInstance()} height={35} />, { width: 92, height: 35 });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TextAttributes } from "../../../ui";
|
|
2
2
|
import type { DataTableCell } from "../../../components";
|
|
3
3
|
import { colors } from "../../../theme/colors";
|
|
4
|
+
import { formatRelativeTime } from "../../../utils/datetime-format";
|
|
4
5
|
import {
|
|
5
6
|
CompanyCell,
|
|
6
7
|
FavoriteCell,
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
11
12
|
activityColor,
|
|
12
13
|
activityLabel,
|
|
13
14
|
criticalityColor,
|
|
14
|
-
formatRelativeTime,
|
|
15
15
|
metricColor,
|
|
16
16
|
text,
|
|
17
17
|
tickerSymbol,
|
|
@@ -2,7 +2,7 @@ import { Box, Text } from "../../../../ui";
|
|
|
2
2
|
import { RemoteImage } from "../../../../components/ui";
|
|
3
3
|
import { colors } from "../../../../theme/colors";
|
|
4
4
|
import type { BuildoutObservation, BuildoutSite } from "../model/types";
|
|
5
|
-
import { dateShort
|
|
5
|
+
import { dateShort } from "../format";
|
|
6
6
|
|
|
7
7
|
function observationImageUrl(observation: BuildoutObservation) {
|
|
8
8
|
return observation.upscaledImageUrl
|
|
@@ -6,7 +6,6 @@ import { colors } from "../../../../theme/colors";
|
|
|
6
6
|
import type { InlineTickerCatalogEntry } from "../../../../state/hooks/inline-tickers";
|
|
7
7
|
import type { BuildoutCompany, BuildoutRelatedCompany, BuildoutSource } from "../model/types";
|
|
8
8
|
import { domainFromUrl, text, textOrNull, tickerSymbol, truncate } from "../format";
|
|
9
|
-
import { booleanText } from "./values";
|
|
10
9
|
|
|
11
10
|
export type InlineTickerCatalog = Record<string, InlineTickerCatalogEntry>;
|
|
12
11
|
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { colors, priceColor } from "../../../theme/colors";
|
|
2
|
-
import { formatDetailDate as sharedFormatDetailDate,
|
|
2
|
+
import { formatDetailDate as sharedFormatDetailDate, parseDisplayDate } from "../../../utils/datetime-format";
|
|
3
3
|
import type { BuildoutSource, BuildoutUpdate } from "./model/types";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
if (width <= 0) return "";
|
|
7
|
-
if (text.length <= width) return text;
|
|
8
|
-
if (width <= 3) return text.slice(0, width);
|
|
9
|
-
return `${text.slice(0, width - 3)}...`;
|
|
10
|
-
}
|
|
5
|
+
export { truncateWithEllipsis as truncate } from "../../../utils/text-wrap";
|
|
11
6
|
|
|
12
7
|
export function text(value: unknown, fallback = "-") {
|
|
13
8
|
if (value == null) return fallback;
|
|
@@ -62,10 +57,6 @@ export function dateDetail(value: string | null | undefined) {
|
|
|
62
57
|
return formatted || null;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
export function formatRelativeTime(value: string | null | undefined): string {
|
|
66
|
-
return sharedFormatRelativeTime(value);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
60
|
export function activityLabel(value: number | null | undefined) {
|
|
70
61
|
if (value == null) return "-";
|
|
71
62
|
if (value >= 2) return "High";
|
|
@@ -9,7 +9,8 @@ import type { ChatMessage } from "../../../api-client";
|
|
|
9
9
|
import { apiClient } from "../../../api-client";
|
|
10
10
|
import { PluginRenderProvider } from "../../runtime";
|
|
11
11
|
import { setSharedRegistryForTests } from "../../registry";
|
|
12
|
-
import { ChatContent
|
|
12
|
+
import { ChatContent } from "./content";
|
|
13
|
+
import { ChatStatusWidget } from "./status-widget";
|
|
13
14
|
import {
|
|
14
15
|
cleanupChatTest,
|
|
15
16
|
createChatTestControls,
|
|
@@ -45,6 +46,55 @@ function makeOwnMessage(content = "typo", createdAt = recentChatTimestamp()): Ch
|
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
const makeNamedMessage = (index: number, username: string): ChatMessage => ({
|
|
50
|
+
id: `named-${index}`,
|
|
51
|
+
channelId: "everyone",
|
|
52
|
+
content: `message from ${username}`,
|
|
53
|
+
replyToId: null,
|
|
54
|
+
createdAt: `2026-03-30T00:01:${String(index).padStart(2, "0")}.000Z`,
|
|
55
|
+
user: { id: `u-${username}`, username, displayName: username },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function renderFocusedComposerWithDraft(
|
|
59
|
+
controller: ReturnType<typeof createController>,
|
|
60
|
+
draft: string,
|
|
61
|
+
options: Parameters<typeof createHarness>[1] = {},
|
|
62
|
+
) {
|
|
63
|
+
const harnessOptions = {
|
|
64
|
+
width: 72,
|
|
65
|
+
height: 14,
|
|
66
|
+
...options,
|
|
67
|
+
};
|
|
68
|
+
await act(async () => {
|
|
69
|
+
testSetup = await testRender(createHarness(controller, harnessOptions), {
|
|
70
|
+
width: harnessOptions.width ?? 72,
|
|
71
|
+
height: harnessOptions.height ?? 14,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await flushFrame();
|
|
76
|
+
|
|
77
|
+
const frameBeforeClick = setup().captureCharFrame().split("\n");
|
|
78
|
+
const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
|
|
79
|
+
const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
|
|
80
|
+
|
|
81
|
+
expect(inputRow).toBeGreaterThanOrEqual(0);
|
|
82
|
+
expect(inputCol).toBeGreaterThanOrEqual(0);
|
|
83
|
+
|
|
84
|
+
await act(async () => {
|
|
85
|
+
await setup().mockMouse.click(inputCol + 1, inputRow);
|
|
86
|
+
await setup().renderOnce();
|
|
87
|
+
await setup().renderOnce();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await act(async () => {
|
|
91
|
+
await setup().mockInput.typeText(draft);
|
|
92
|
+
await setup().renderOnce();
|
|
93
|
+
await setup().renderOnce();
|
|
94
|
+
});
|
|
95
|
+
await flushFrame();
|
|
96
|
+
}
|
|
97
|
+
|
|
48
98
|
beforeEach(() => {
|
|
49
99
|
installChatApiTestDefaults();
|
|
50
100
|
});
|
|
@@ -177,6 +227,82 @@ describe("ChatContent", () => {
|
|
|
177
227
|
expect(frameAfterType).not.toContain("> betaalpha");
|
|
178
228
|
});
|
|
179
229
|
|
|
230
|
+
test("autocompletes recent user mentions from the focused composer", async () => {
|
|
231
|
+
const controller = createController({
|
|
232
|
+
messages: [
|
|
233
|
+
makeNamedMessage(1, "alpha"),
|
|
234
|
+
makeNamedMessage(2, "bravo"),
|
|
235
|
+
makeNamedMessage(3, "charlie"),
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await renderFocusedComposerWithDraft(controller, "@");
|
|
240
|
+
|
|
241
|
+
let frame = setup().captureCharFrame();
|
|
242
|
+
expect(frame).toContain("@charlie");
|
|
243
|
+
expect(frame).toContain("@bravo");
|
|
244
|
+
|
|
245
|
+
await emitKeypress({ name: "down", sequence: "\u001b[B" });
|
|
246
|
+
|
|
247
|
+
await act(async () => {
|
|
248
|
+
setup().mockInput.pressEnter();
|
|
249
|
+
await setup().renderOnce();
|
|
250
|
+
await setup().renderOnce();
|
|
251
|
+
});
|
|
252
|
+
await flushFrame();
|
|
253
|
+
|
|
254
|
+
frame = setup().captureCharFrame();
|
|
255
|
+
expect(frame).toContain("> @bravo");
|
|
256
|
+
expect(frame).not.toContain("@charlie");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("tab accepts the current mention suggestion without pane cycling", async () => {
|
|
260
|
+
const controller = createController({
|
|
261
|
+
messages: [
|
|
262
|
+
makeNamedMessage(1, "alpha"),
|
|
263
|
+
makeNamedMessage(2, "bravo"),
|
|
264
|
+
makeNamedMessage(3, "charlie"),
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await renderFocusedComposerWithDraft(controller, "@");
|
|
269
|
+
|
|
270
|
+
const event = await emitKeypress({ name: "tab", sequence: "\t" });
|
|
271
|
+
await flushFrame();
|
|
272
|
+
|
|
273
|
+
const frame = setup().captureCharFrame();
|
|
274
|
+
expect(frame).toContain("> @charlie");
|
|
275
|
+
expect(frame).not.toContain("@bravo");
|
|
276
|
+
expect(event.defaultPrevented).toBe(true);
|
|
277
|
+
expect(event.propagationStopped).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("leaves tab for the command bar when mention suggestions are open behind it", async () => {
|
|
281
|
+
const controller = createController({
|
|
282
|
+
messages: [
|
|
283
|
+
makeNamedMessage(1, "alpha"),
|
|
284
|
+
makeNamedMessage(2, "bravo"),
|
|
285
|
+
makeNamedMessage(3, "charlie"),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await renderFocusedComposerWithDraft(controller, "@", {
|
|
290
|
+
configureState: (state) => {
|
|
291
|
+
state.commandBarOpen = true;
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const event = await emitKeypress({ name: "tab", sequence: "\t" });
|
|
296
|
+
await flushFrame();
|
|
297
|
+
|
|
298
|
+
const frame = setup().captureCharFrame();
|
|
299
|
+
expect(frame).toContain("@charlie");
|
|
300
|
+
expect(frame).toContain("> @");
|
|
301
|
+
expect(frame).not.toContain("> @charlie");
|
|
302
|
+
expect(event.defaultPrevented).toBe(false);
|
|
303
|
+
expect(event.propagationStopped).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
|
|
180
306
|
test("up arrow selects the newest message first when nothing is selected", async () => {
|
|
181
307
|
const controller = createController({
|
|
182
308
|
messages: [makeMessage(1), makeMessage(2)],
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
|
3
|
-
import type { ChatChannel
|
|
3
|
+
import type { ChatChannel } from "../../../../api-client";
|
|
4
4
|
import { useThrottledCommitValue } from "../../../../react/use-throttled-commit-value";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_CHAT_CHANNEL_ID,
|
|
7
7
|
normalizeChannelId,
|
|
8
8
|
} from "../channels";
|
|
9
|
-
import type { ChatContentController } from "./types";
|
|
10
9
|
|
|
11
10
|
const CHANNEL_NAVIGATION_COMMIT_DELAY_MS = 150;
|
|
12
11
|
|
|
@@ -20,8 +19,6 @@ export function useChatChannelNavigation({
|
|
|
20
19
|
channelIdRef,
|
|
21
20
|
channels,
|
|
22
21
|
channelsLoading,
|
|
23
|
-
closeProfilePopover,
|
|
24
|
-
controller,
|
|
25
22
|
focused,
|
|
26
23
|
inputFocused,
|
|
27
24
|
onChannelChange,
|
|
@@ -33,8 +30,6 @@ export function useChatChannelNavigation({
|
|
|
33
30
|
channelIdRef: MutableRefObject<string>;
|
|
34
31
|
channels: ChatChannel[];
|
|
35
32
|
channelsLoading: boolean;
|
|
36
|
-
closeProfilePopover: () => void;
|
|
37
|
-
controller: ChatContentController;
|
|
38
33
|
focused: boolean;
|
|
39
34
|
inputFocused: boolean;
|
|
40
35
|
onChannelChange?: (channelId: string) => void;
|
|
@@ -46,7 +41,6 @@ export function useChatChannelNavigation({
|
|
|
46
41
|
focusChannelSidebar: () => boolean;
|
|
47
42
|
focusChatContent: () => boolean;
|
|
48
43
|
moveSidebarChannelSelection: (direction: "up" | "down") => boolean;
|
|
49
|
-
openDirectMessage: (target: ChatUserSummary) => Promise<void>;
|
|
50
44
|
selectSidebarChannel: (channelId: string) => void;
|
|
51
45
|
setDirectExpanded: Dispatch<SetStateAction<boolean>>;
|
|
52
46
|
setSidebarFocused: (nextFocused: boolean) => void;
|
|
@@ -82,19 +76,6 @@ export function useChatChannelNavigation({
|
|
|
82
76
|
replaceValue: replaceSidebarCursorChannelId,
|
|
83
77
|
} = useThrottledCommitValue(channelId, changeChannel, CHANNEL_NAVIGATION_COMMIT_DELAY_MS);
|
|
84
78
|
|
|
85
|
-
const openDirectMessage = useCallback(async (target: ChatUserSummary) => {
|
|
86
|
-
if (!target.id && !target.username) return;
|
|
87
|
-
try {
|
|
88
|
-
const channel = await controller.openDirectChannel({
|
|
89
|
-
userId: target.id,
|
|
90
|
-
username: target.username ?? undefined,
|
|
91
|
-
});
|
|
92
|
-
setDirectExpanded(true);
|
|
93
|
-
setSidebarCursorChannelId(channel.id, { immediate: true });
|
|
94
|
-
closeProfilePopover();
|
|
95
|
-
} catch {}
|
|
96
|
-
}, [closeProfilePopover, controller, setSidebarCursorChannelId]);
|
|
97
|
-
|
|
98
79
|
useEffect(() => {
|
|
99
80
|
if (!onChannelChange || channelsLoading || channels.length === 0) return;
|
|
100
81
|
if (channels.some((channel) => channel.id === channelId)) return;
|
|
@@ -185,7 +166,6 @@ export function useChatChannelNavigation({
|
|
|
185
166
|
focusChannelSidebar,
|
|
186
167
|
focusChatContent,
|
|
187
168
|
moveSidebarChannelSelection,
|
|
188
|
-
openDirectMessage,
|
|
189
169
|
selectSidebarChannel,
|
|
190
170
|
setDirectExpanded,
|
|
191
171
|
setSidebarFocused,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TextareaRenderable } from "../../../../ui";
|
|
2
|
+
|
|
3
|
+
const clampCursorOffset = (offset: number, draft: string) => Math.max(0, Math.min(offset, draft.length));
|
|
4
|
+
|
|
5
|
+
export function getComposerCursorOffset(
|
|
6
|
+
textarea: TextareaRenderable | null | undefined,
|
|
7
|
+
draft: string,
|
|
8
|
+
): number {
|
|
9
|
+
const offset = textarea?.visualCursor?.offset ?? textarea?.cursorOffset ?? draft.length;
|
|
10
|
+
return clampCursorOffset(offset, draft);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function moveComposerCursorToOffset(
|
|
14
|
+
textarea: TextareaRenderable,
|
|
15
|
+
draft: string,
|
|
16
|
+
offset: number,
|
|
17
|
+
): void {
|
|
18
|
+
const nextOffset = clampCursorOffset(offset, draft);
|
|
19
|
+
const editBuffer = textarea.editBuffer as typeof textarea.editBuffer & {
|
|
20
|
+
setCursorByOffset?: (offset: number) => void;
|
|
21
|
+
};
|
|
22
|
+
if (typeof editBuffer.setCursorByOffset === "function") return editBuffer.setCursorByOffset(nextOffset);
|
|
23
|
+
if (typeof textarea.setCursorOffset === "function") return textarea.setCursorOffset(nextOffset);
|
|
24
|
+
try {
|
|
25
|
+
textarea.cursorOffset = nextOffset;
|
|
26
|
+
} catch {
|
|
27
|
+
// Some host renderers expose cursorOffset as read-only.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -7,31 +7,12 @@ import {
|
|
|
7
7
|
} from "../layout";
|
|
8
8
|
import { parseChatComposerCommand } from "../composer-commands";
|
|
9
9
|
import type { ChatContentController } from "./types";
|
|
10
|
+
import { getComposerCursorOffset, moveComposerCursorToOffset } from "./composer-cursor";
|
|
10
11
|
|
|
11
12
|
interface MutableRef<T> {
|
|
12
13
|
current: T;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function moveComposerCursorToEnd(textarea: TextareaRenderable, draft: string) {
|
|
16
|
-
const offset = draft.length;
|
|
17
|
-
const editBuffer = textarea.editBuffer as typeof textarea.editBuffer & {
|
|
18
|
-
setCursorByOffset?: (offset: number) => void;
|
|
19
|
-
};
|
|
20
|
-
if (typeof editBuffer.setCursorByOffset === "function") {
|
|
21
|
-
editBuffer.setCursorByOffset(offset);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (typeof textarea.setCursorOffset === "function") {
|
|
25
|
-
textarea.setCursorOffset(offset);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
textarea.cursorOffset = offset;
|
|
30
|
-
} catch {
|
|
31
|
-
// Some host renderers expose cursorOffset as read-only.
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
16
|
export function useChatComposerRuntime({
|
|
36
17
|
applyingExternalDraftRef,
|
|
37
18
|
blurInput,
|
|
@@ -46,6 +27,7 @@ export function useChatComposerRuntime({
|
|
|
46
27
|
inputRef,
|
|
47
28
|
inputValueRef,
|
|
48
29
|
messages,
|
|
30
|
+
onComposerStateChange,
|
|
49
31
|
onChannelChange,
|
|
50
32
|
editingMessage,
|
|
51
33
|
latestEditableMessageId,
|
|
@@ -71,6 +53,7 @@ export function useChatComposerRuntime({
|
|
|
71
53
|
inputRef: MutableRef<TextareaRenderable | null>;
|
|
72
54
|
inputValueRef: MutableRef<string>;
|
|
73
55
|
messages: ChatMessage[];
|
|
56
|
+
onComposerStateChange?: (draft: string, cursorOffset: number) => void;
|
|
74
57
|
onChannelChange?: (channelId: string) => void;
|
|
75
58
|
editingMessage: ChatMessage | null;
|
|
76
59
|
latestEditableMessageId: string | null;
|
|
@@ -112,7 +95,7 @@ export function useChatComposerRuntime({
|
|
|
112
95
|
}
|
|
113
96
|
}, [channelId, controller, useDefaultControllerChannel]);
|
|
114
97
|
|
|
115
|
-
const replaceLocalComposer = useCallback((draft: string) => {
|
|
98
|
+
const replaceLocalComposer = useCallback((draft: string, cursorOffset = draft.length) => {
|
|
116
99
|
inputValueRef.current = draft;
|
|
117
100
|
updateComposerRows(draft);
|
|
118
101
|
const textarea = inputRef.current;
|
|
@@ -120,14 +103,20 @@ export function useChatComposerRuntime({
|
|
|
120
103
|
applyingExternalDraftRef.current = true;
|
|
121
104
|
try {
|
|
122
105
|
textarea.setText(draft);
|
|
123
|
-
|
|
106
|
+
moveComposerCursorToOffset(textarea, draft, cursorOffset);
|
|
124
107
|
} finally {
|
|
125
108
|
applyingExternalDraftRef.current = false;
|
|
126
109
|
}
|
|
127
110
|
} else if (textarea) {
|
|
128
|
-
|
|
111
|
+
moveComposerCursorToOffset(textarea, draft, cursorOffset);
|
|
129
112
|
}
|
|
130
|
-
|
|
113
|
+
onComposerStateChange?.(draft, cursorOffset);
|
|
114
|
+
}, [applyingExternalDraftRef, inputRef, inputValueRef, onComposerStateChange, updateComposerRows]);
|
|
115
|
+
|
|
116
|
+
const replaceComposerDraft = useCallback((draft: string, cursorOffset = draft.length) => {
|
|
117
|
+
replaceLocalComposer(draft, cursorOffset);
|
|
118
|
+
persistDraft(draft);
|
|
119
|
+
}, [persistDraft, replaceLocalComposer]);
|
|
131
120
|
|
|
132
121
|
const beginReplyTo = useCallback((index: number, options?: { deferFocus?: boolean }) => {
|
|
133
122
|
if (!canSend || index < 0 || index >= messages.length) return;
|
|
@@ -295,14 +284,22 @@ export function useChatComposerRuntime({
|
|
|
295
284
|
|
|
296
285
|
const commitLocalDraft = useCallback((draft: string) => {
|
|
297
286
|
if (applyingExternalDraftRef.current) return;
|
|
287
|
+
const previousDraft = inputValueRef.current;
|
|
298
288
|
inputValueRef.current = draft;
|
|
299
289
|
updateComposerRows(draft);
|
|
300
290
|
persistDraft(draft);
|
|
291
|
+
const rawCursorOffset = getComposerCursorOffset(inputRef.current, draft);
|
|
292
|
+
const cursorOffset = rawCursorOffset === 0 && draft.length > previousDraft.length && draft.startsWith(previousDraft)
|
|
293
|
+
? draft.length
|
|
294
|
+
: rawCursorOffset;
|
|
295
|
+
onComposerStateChange?.(draft, cursorOffset);
|
|
301
296
|
}, [
|
|
302
297
|
applyingExternalDraftRef,
|
|
303
298
|
channelId,
|
|
304
299
|
controller,
|
|
300
|
+
inputRef,
|
|
305
301
|
inputValueRef,
|
|
302
|
+
onComposerStateChange,
|
|
306
303
|
persistDraft,
|
|
307
304
|
updateComposerRows,
|
|
308
305
|
useDefaultControllerChannel,
|
|
@@ -376,6 +373,7 @@ export function useChatComposerRuntime({
|
|
|
376
373
|
focusComposer,
|
|
377
374
|
inputPlaceholder,
|
|
378
375
|
replyPreview,
|
|
376
|
+
replaceComposerDraft,
|
|
379
377
|
returnToComposer,
|
|
380
378
|
sendMessage,
|
|
381
379
|
};
|
|
@@ -5,6 +5,7 @@ import type { ChatMessage } from "../../../../api-client";
|
|
|
5
5
|
import { InlineAuthActions } from "../../cloud/auth-actions";
|
|
6
6
|
import { ChatActionChip } from "../message/action-chip";
|
|
7
7
|
import { COMPOSER_ACTION_WIDTH } from "../layout";
|
|
8
|
+
import type { ChatMentionSuggestion } from "./mentions";
|
|
8
9
|
|
|
9
10
|
const DESKTOP_CHAT_INPUT_TOP_MARGIN_PX = 6;
|
|
10
11
|
|
|
@@ -33,9 +34,73 @@ interface ChatComposerAreaProps {
|
|
|
33
34
|
replyPreview: string;
|
|
34
35
|
replyTo: ChatMessage | null;
|
|
35
36
|
sendMessage: () => void;
|
|
37
|
+
mentionSuggestions: ChatMentionSuggestion[];
|
|
38
|
+
mentionSelectedIndex: number;
|
|
39
|
+
onMentionCursorChange: () => void;
|
|
40
|
+
onMentionSelect: (index?: number) => boolean;
|
|
36
41
|
user: { id: string; username: string; emailVerified: boolean } | null;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function ChatMentionSuggestions({
|
|
45
|
+
contentWidth,
|
|
46
|
+
nativePaneChrome,
|
|
47
|
+
onSelect,
|
|
48
|
+
selectedIndex,
|
|
49
|
+
suggestions,
|
|
50
|
+
}: {
|
|
51
|
+
contentWidth: number;
|
|
52
|
+
nativePaneChrome: boolean | undefined;
|
|
53
|
+
onSelect: (index?: number) => boolean;
|
|
54
|
+
selectedIndex: number;
|
|
55
|
+
suggestions: ChatMentionSuggestion[];
|
|
56
|
+
}) {
|
|
57
|
+
if (suggestions.length === 0) return null;
|
|
58
|
+
const width = Math.max(18, Math.min(contentWidth, 44));
|
|
59
|
+
const menuStyle = nativePaneChrome
|
|
60
|
+
? {
|
|
61
|
+
marginBottom: 4,
|
|
62
|
+
border: `1px solid ${colors.border}`,
|
|
63
|
+
borderRadius: 6,
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
}
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Box
|
|
70
|
+
width={width}
|
|
71
|
+
height={suggestions.length}
|
|
72
|
+
flexDirection="column"
|
|
73
|
+
backgroundColor={nativePaneChrome ? colors.panel : colors.bg}
|
|
74
|
+
style={menuStyle}
|
|
75
|
+
>
|
|
76
|
+
{suggestions.map((suggestion, index) => {
|
|
77
|
+
const selected = index === selectedIndex;
|
|
78
|
+
return (
|
|
79
|
+
<Box
|
|
80
|
+
key={suggestion.username}
|
|
81
|
+
height={1}
|
|
82
|
+
width={width}
|
|
83
|
+
flexDirection="row"
|
|
84
|
+
backgroundColor={selected ? colors.selected : "transparent"}
|
|
85
|
+
onMouseDown={() => { onSelect(index); }}
|
|
86
|
+
style={{
|
|
87
|
+
cursor: "pointer",
|
|
88
|
+
paddingInline: nativePaneChrome ? 8 : undefined,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<Text
|
|
92
|
+
fg={selected ? colors.selectedText : colors.positive}
|
|
93
|
+
attributes={selected ? TextAttributes.BOLD : 0}
|
|
94
|
+
>
|
|
95
|
+
{`@${suggestion.username}`}
|
|
96
|
+
</Text>
|
|
97
|
+
</Box>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</Box>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
39
104
|
export function ChatComposerArea({
|
|
40
105
|
canSend,
|
|
41
106
|
cancelEditMessage,
|
|
@@ -57,6 +122,10 @@ export function ChatComposerArea({
|
|
|
57
122
|
replyPreview,
|
|
58
123
|
replyTo,
|
|
59
124
|
sendMessage,
|
|
125
|
+
mentionSuggestions,
|
|
126
|
+
mentionSelectedIndex,
|
|
127
|
+
onMentionCursorChange,
|
|
128
|
+
onMentionSelect,
|
|
60
129
|
user,
|
|
61
130
|
}: ChatComposerAreaProps) {
|
|
62
131
|
if (!canSend) {
|
|
@@ -122,6 +191,14 @@ export function ChatComposerArea({
|
|
|
122
191
|
</Box>
|
|
123
192
|
)}
|
|
124
193
|
|
|
194
|
+
<ChatMentionSuggestions
|
|
195
|
+
contentWidth={contentWidth}
|
|
196
|
+
nativePaneChrome={nativePaneChrome}
|
|
197
|
+
onSelect={onMentionSelect}
|
|
198
|
+
selectedIndex={mentionSelectedIndex}
|
|
199
|
+
suggestions={mentionSuggestions}
|
|
200
|
+
/>
|
|
201
|
+
|
|
125
202
|
<MessageComposer
|
|
126
203
|
inputRef={inputRef}
|
|
127
204
|
initialValue={inputValueRef.current}
|
|
@@ -132,6 +209,7 @@ export function ChatComposerArea({
|
|
|
132
209
|
height={composerHeight}
|
|
133
210
|
onFocusRequest={focusComposer}
|
|
134
211
|
onInput={commitLocalDraft}
|
|
212
|
+
onCursorChange={onMentionCursorChange}
|
|
135
213
|
keyBindings={[
|
|
136
214
|
{ name: "return", action: "submit" },
|
|
137
215
|
{ name: "linefeed", action: "submit" },
|
|
@@ -141,6 +219,7 @@ export function ChatComposerArea({
|
|
|
141
219
|
{ name: "linefeed", meta: true, action: "submit" },
|
|
142
220
|
]}
|
|
143
221
|
onSubmit={() => {
|
|
222
|
+
if (onMentionSelect()) return;
|
|
144
223
|
if (inputValueRef.current.trim()) {
|
|
145
224
|
sendMessage();
|
|
146
225
|
}
|
|
@@ -157,13 +236,17 @@ export function getChatComposerAreaHeight({
|
|
|
157
236
|
editingMessage,
|
|
158
237
|
nativePaneChrome,
|
|
159
238
|
replyTo,
|
|
239
|
+
mentionSuggestionCount,
|
|
160
240
|
}: {
|
|
161
241
|
canSend: boolean;
|
|
162
242
|
composerHeight: number;
|
|
163
243
|
editingMessage: ChatMessage | null;
|
|
164
244
|
nativePaneChrome: boolean | undefined;
|
|
165
245
|
replyTo: ChatMessage | null;
|
|
246
|
+
mentionSuggestionCount?: number;
|
|
166
247
|
}) {
|
|
167
248
|
if (!canSend) return 2;
|
|
168
|
-
return getMessageComposerBlockHeight({ height: composerHeight, nativePaneChrome })
|
|
249
|
+
return getMessageComposerBlockHeight({ height: composerHeight, nativePaneChrome })
|
|
250
|
+
+ (editingMessage || replyTo ? 1 : 0)
|
|
251
|
+
+ Math.max(0, mentionSuggestionCount ?? 0);
|
|
169
252
|
}
|