gloomberb 0.4.1 β 0.5.1
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/README.md +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/pane-sizing.ts +10 -0
- package/src/components/layout/pane.tsx +9 -2
- package/src/components/layout/shell.test.tsx +282 -28
- package/src/components/layout/shell.tsx +10 -7
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +83 -18
- package/src/plugins/builtin/chat-controller.ts +26 -14
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +424 -120
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/api-client.test.ts +90 -0
- package/src/utils/api-client.ts +56 -16
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
package/src/theme/themes.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Theme {
|
|
|
17
17
|
positive: string;
|
|
18
18
|
negative: string;
|
|
19
19
|
neutral: string;
|
|
20
|
+
warning: string;
|
|
20
21
|
|
|
21
22
|
header: string;
|
|
22
23
|
headerText: string;
|
|
@@ -79,6 +80,12 @@ function normalizeTheme(theme: Theme): Theme {
|
|
|
79
80
|
highestMinimumContrast(bodySurfaces, [textDim, text, "#c0c7d1"]),
|
|
80
81
|
SUBTLE_TEXT_MIN,
|
|
81
82
|
);
|
|
83
|
+
const warning = blendForContrastOnSurfaces(
|
|
84
|
+
theme.warning,
|
|
85
|
+
bodySurfaces,
|
|
86
|
+
blendHex(theme.warning, theme.textBright, 0.5),
|
|
87
|
+
SUBTLE_TEXT_MIN,
|
|
88
|
+
);
|
|
82
89
|
const selected = blendForContrastOnSurfaces(
|
|
83
90
|
theme.selected,
|
|
84
91
|
bodySurfaces,
|
|
@@ -106,6 +113,7 @@ function normalizeTheme(theme: Theme): Theme {
|
|
|
106
113
|
positive,
|
|
107
114
|
negative,
|
|
108
115
|
neutral,
|
|
116
|
+
warning,
|
|
109
117
|
headerText,
|
|
110
118
|
selected,
|
|
111
119
|
selectedText,
|
|
@@ -127,6 +135,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
127
135
|
positive: "#00cc66",
|
|
128
136
|
negative: "#ff3333",
|
|
129
137
|
neutral: "#888888",
|
|
138
|
+
warning: "#ffaa00",
|
|
130
139
|
header: "#0044aa",
|
|
131
140
|
headerText: "#ffffff",
|
|
132
141
|
selected: "#1a3a5c",
|
|
@@ -149,6 +158,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
149
158
|
positive: "#00ff66",
|
|
150
159
|
negative: "#ff4444",
|
|
151
160
|
neutral: "#666666",
|
|
161
|
+
warning: "#cccc00",
|
|
152
162
|
header: "#003300",
|
|
153
163
|
headerText: "#00ff00",
|
|
154
164
|
selected: "#003300",
|
|
@@ -171,6 +181,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
171
181
|
positive: "#00ff88",
|
|
172
182
|
negative: "#ff4444",
|
|
173
183
|
neutral: "#666666",
|
|
184
|
+
warning: "#ddcc00",
|
|
174
185
|
header: "#002222",
|
|
175
186
|
headerText: "#00ffff",
|
|
176
187
|
selected: "#002a2a",
|
|
@@ -193,6 +204,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
193
204
|
positive: "#44cc44",
|
|
194
205
|
negative: "#ff6666",
|
|
195
206
|
neutral: "#666666",
|
|
207
|
+
warning: "#ddaa00",
|
|
196
208
|
header: "#220000",
|
|
197
209
|
headerText: "#ff3333",
|
|
198
210
|
selected: "#2a0000",
|
|
@@ -215,6 +227,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
215
227
|
positive: "#00cc66",
|
|
216
228
|
negative: "#ff4444",
|
|
217
229
|
neutral: "#666666",
|
|
230
|
+
warning: "#ddaa00",
|
|
218
231
|
header: "#000033",
|
|
219
232
|
headerText: "#4488ff",
|
|
220
233
|
selected: "#001144",
|
|
@@ -237,6 +250,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
237
250
|
positive: "#44dd66",
|
|
238
251
|
negative: "#ff4444",
|
|
239
252
|
neutral: "#666666",
|
|
253
|
+
warning: "#ddaa00",
|
|
240
254
|
header: "#1a0028",
|
|
241
255
|
headerText: "#bb55ff",
|
|
242
256
|
selected: "#220033",
|
|
@@ -259,6 +273,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
259
273
|
positive: "#44dd66",
|
|
260
274
|
negative: "#ff5555",
|
|
261
275
|
neutral: "#666666",
|
|
276
|
+
warning: "#ddaa00",
|
|
262
277
|
header: "#22000f",
|
|
263
278
|
headerText: "#ff44aa",
|
|
264
279
|
selected: "#2a0018",
|
|
@@ -281,6 +296,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
281
296
|
positive: "#00cc66",
|
|
282
297
|
negative: "#ff3333",
|
|
283
298
|
neutral: "#888888",
|
|
299
|
+
warning: "#ddaa00",
|
|
284
300
|
header: "#1a1a1a",
|
|
285
301
|
headerText: "#ffffff",
|
|
286
302
|
selected: "#222222",
|
|
@@ -303,6 +319,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
303
319
|
positive: "#9ece6a",
|
|
304
320
|
negative: "#f7768e",
|
|
305
321
|
neutral: "#565f89",
|
|
322
|
+
warning: "#e0af68",
|
|
306
323
|
header: "#24283b",
|
|
307
324
|
headerText: "#7aa2f7",
|
|
308
325
|
selected: "#283457",
|
|
@@ -325,6 +342,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
325
342
|
positive: "#859900",
|
|
326
343
|
negative: "#dc322f",
|
|
327
344
|
neutral: "#657b83",
|
|
345
|
+
warning: "#b58900",
|
|
328
346
|
header: "#073642",
|
|
329
347
|
headerText: "#268bd2",
|
|
330
348
|
selected: "#0a4a5c",
|
|
@@ -347,6 +365,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
347
365
|
positive: "#50fa7b",
|
|
348
366
|
negative: "#ff5555",
|
|
349
367
|
neutral: "#6272a4",
|
|
368
|
+
warning: "#f1fa8c",
|
|
350
369
|
header: "#44475a",
|
|
351
370
|
headerText: "#bd93f9",
|
|
352
371
|
selected: "#44475a",
|
|
@@ -369,6 +388,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
369
388
|
positive: "#a3be8c",
|
|
370
389
|
negative: "#bf616a",
|
|
371
390
|
neutral: "#4c566a",
|
|
391
|
+
warning: "#ebcb8b",
|
|
372
392
|
header: "#3b4252",
|
|
373
393
|
headerText: "#88c0d0",
|
|
374
394
|
selected: "#4c566a",
|
|
@@ -391,6 +411,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
391
411
|
positive: "#a6e22e",
|
|
392
412
|
negative: "#f92672",
|
|
393
413
|
neutral: "#75715e",
|
|
414
|
+
warning: "#e6db74",
|
|
394
415
|
header: "#49483e",
|
|
395
416
|
headerText: "#e6db74",
|
|
396
417
|
selected: "#49483e",
|
|
@@ -413,6 +434,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
413
434
|
positive: "#a6e3a1",
|
|
414
435
|
negative: "#f38ba8",
|
|
415
436
|
neutral: "#6c7086",
|
|
437
|
+
warning: "#f9e2af",
|
|
416
438
|
header: "#313244",
|
|
417
439
|
headerText: "#cba6f7",
|
|
418
440
|
selected: "#3e3f56",
|
|
@@ -435,6 +457,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
435
457
|
positive: "#b8bb26",
|
|
436
458
|
negative: "#fb4934",
|
|
437
459
|
neutral: "#928374",
|
|
460
|
+
warning: "#fabd2f",
|
|
438
461
|
header: "#3c3836",
|
|
439
462
|
headerText: "#fabd2f",
|
|
440
463
|
selected: "#3c3836",
|
|
@@ -457,6 +480,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
457
480
|
positive: "#31748f",
|
|
458
481
|
negative: "#eb6f92",
|
|
459
482
|
neutral: "#6e6a86",
|
|
483
|
+
warning: "#f6c177",
|
|
460
484
|
header: "#26233a",
|
|
461
485
|
headerText: "#c4a7e7",
|
|
462
486
|
selected: "#332f4a",
|
|
@@ -479,6 +503,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
479
503
|
positive: "#00cc66",
|
|
480
504
|
negative: "#ff3333",
|
|
481
505
|
neutral: "#6688bb",
|
|
506
|
+
warning: "#ff9900",
|
|
482
507
|
header: "#001144",
|
|
483
508
|
headerText: "#ff6600",
|
|
484
509
|
selected: "#002255",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createPaneInstance,
|
|
4
|
+
findPrimaryPaneInstance,
|
|
5
|
+
resolveFollowBindingInstance,
|
|
6
|
+
resolvePaneInstance,
|
|
7
|
+
type LayoutConfig,
|
|
8
|
+
type PaneInstanceConfig,
|
|
9
|
+
} from "./config";
|
|
10
|
+
|
|
11
|
+
function createLayout(instances: PaneInstanceConfig[]): LayoutConfig {
|
|
12
|
+
return {
|
|
13
|
+
dockRoot: null,
|
|
14
|
+
instances,
|
|
15
|
+
floating: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("findPrimaryPaneInstance", () => {
|
|
20
|
+
test("prefers the main non-fixed ticker pane", () => {
|
|
21
|
+
const layout = createLayout([
|
|
22
|
+
createPaneInstance("portfolio-list", {
|
|
23
|
+
instanceId: "portfolio-list:main",
|
|
24
|
+
binding: { kind: "none" },
|
|
25
|
+
}),
|
|
26
|
+
createPaneInstance("ticker-detail", {
|
|
27
|
+
instanceId: "ticker-detail:pinned",
|
|
28
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
29
|
+
}),
|
|
30
|
+
createPaneInstance("ticker-detail", {
|
|
31
|
+
instanceId: "ticker-detail:main",
|
|
32
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
expect(findPrimaryPaneInstance(layout, "ticker-detail")?.instanceId).toBe("ticker-detail:main");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("does not treat fixed ticker panes as the primary pane", () => {
|
|
40
|
+
const layout = createLayout([
|
|
41
|
+
createPaneInstance("ticker-detail", {
|
|
42
|
+
instanceId: "ticker-detail:aapl",
|
|
43
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
44
|
+
}),
|
|
45
|
+
createPaneInstance("ticker-detail", {
|
|
46
|
+
instanceId: "ticker-detail:msft",
|
|
47
|
+
binding: { kind: "fixed", symbol: "MSFT" },
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
expect(findPrimaryPaneInstance(layout, "ticker-detail")).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("resolvePaneInstance", () => {
|
|
56
|
+
test("accepts either an instance id or a pane id", () => {
|
|
57
|
+
const layout = createLayout([
|
|
58
|
+
createPaneInstance("portfolio-list", {
|
|
59
|
+
instanceId: "portfolio-list:main",
|
|
60
|
+
binding: { kind: "none" },
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
expect(resolvePaneInstance(layout, "portfolio-list:main")?.instanceId).toBe("portfolio-list:main");
|
|
65
|
+
expect(resolvePaneInstance(layout, "portfolio-list")?.instanceId).toBe("portfolio-list:main");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("resolveFollowBindingInstance", () => {
|
|
70
|
+
test("walks follow bindings until it finds a matching pane", () => {
|
|
71
|
+
const layout = createLayout([
|
|
72
|
+
createPaneInstance("portfolio-list", {
|
|
73
|
+
instanceId: "portfolio-list:main",
|
|
74
|
+
binding: { kind: "none" },
|
|
75
|
+
}),
|
|
76
|
+
createPaneInstance("ticker-detail", {
|
|
77
|
+
instanceId: "ticker-detail:main",
|
|
78
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
79
|
+
}),
|
|
80
|
+
createPaneInstance("quote-monitor", {
|
|
81
|
+
instanceId: "quote-monitor:main",
|
|
82
|
+
binding: { kind: "follow", sourceInstanceId: "ticker-detail:main" },
|
|
83
|
+
}),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
resolveFollowBindingInstance(layout, "quote-monitor:main", (instance) => instance.paneId === "portfolio-list")?.instanceId,
|
|
88
|
+
).toBe("portfolio-list:main");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("stops on follow cycles", () => {
|
|
92
|
+
const layout = createLayout([
|
|
93
|
+
createPaneInstance("ticker-detail", {
|
|
94
|
+
instanceId: "ticker-detail:first",
|
|
95
|
+
binding: { kind: "follow", sourceInstanceId: "quote-monitor:first" },
|
|
96
|
+
}),
|
|
97
|
+
createPaneInstance("quote-monitor", {
|
|
98
|
+
instanceId: "quote-monitor:first",
|
|
99
|
+
binding: { kind: "follow", sourceInstanceId: "ticker-detail:first" },
|
|
100
|
+
}),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
resolveFollowBindingInstance(layout, "ticker-detail:first", (instance) => instance.paneId === "portfolio-list"),
|
|
105
|
+
).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/types/config.ts
CHANGED
|
@@ -179,7 +179,7 @@ export const DEFAULT_LAYOUT: LayoutConfig = {
|
|
|
179
179
|
hideTabs: false,
|
|
180
180
|
lockedTabId: "overview",
|
|
181
181
|
chartRangePreset: "5Y",
|
|
182
|
-
chartResolution: "
|
|
182
|
+
chartResolution: "auto",
|
|
183
183
|
},
|
|
184
184
|
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
185
185
|
},
|
|
@@ -317,6 +317,35 @@ export function isFixedTickerPane(instance: PaneInstanceConfig): boolean {
|
|
|
317
317
|
return isTickerPaneInstance(instance) && instance.binding?.kind === "fixed";
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
export function findPrimaryPaneInstance(layout: LayoutConfig, paneId: string): PaneInstanceConfig | undefined {
|
|
321
|
+
const instances = layout.instances.filter((instance) => instance.paneId === paneId);
|
|
322
|
+
if (instances.length === 0) return undefined;
|
|
323
|
+
if (!isTickerPaneId(paneId)) return instances[0];
|
|
324
|
+
return instances.find((instance) => instance.instanceId === `${paneId}:main` && instance.binding?.kind !== "fixed")
|
|
325
|
+
?? instances.find((instance) => instance.binding?.kind !== "fixed");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function resolvePaneInstance(layout: LayoutConfig, paneIdOrInstanceId: string): PaneInstanceConfig | undefined {
|
|
329
|
+
return findPaneInstance(layout, paneIdOrInstanceId) ?? findPrimaryPaneInstance(layout, paneIdOrInstanceId);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function resolveFollowBindingInstance(
|
|
333
|
+
layout: LayoutConfig,
|
|
334
|
+
paneIdOrInstanceId: string | null | undefined,
|
|
335
|
+
matcher: (instance: PaneInstanceConfig) => boolean,
|
|
336
|
+
seen = new Set<string>(),
|
|
337
|
+
): PaneInstanceConfig | undefined {
|
|
338
|
+
if (!paneIdOrInstanceId) return undefined;
|
|
339
|
+
const instance = resolvePaneInstance(layout, paneIdOrInstanceId);
|
|
340
|
+
if (!instance || seen.has(instance.instanceId)) return undefined;
|
|
341
|
+
seen.add(instance.instanceId);
|
|
342
|
+
if (matcher(instance)) return instance;
|
|
343
|
+
if (instance.binding?.kind === "follow") {
|
|
344
|
+
return resolveFollowBindingInstance(layout, instance.binding.sourceInstanceId, matcher, seen);
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
320
349
|
export function createPaneInstance(
|
|
321
350
|
paneId: string,
|
|
322
351
|
options: Partial<PaneInstanceConfig> = {},
|
|
@@ -26,6 +26,18 @@ export interface SecFilingItem {
|
|
|
26
26
|
primaryDocumentUrl?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface EarningsEvent {
|
|
30
|
+
symbol: string;
|
|
31
|
+
name: string;
|
|
32
|
+
earningsDate: Date;
|
|
33
|
+
epsEstimate: number | null;
|
|
34
|
+
epsActual: number | null;
|
|
35
|
+
revenueEstimate: number | null;
|
|
36
|
+
revenueActual: number | null;
|
|
37
|
+
surprise: number | null;
|
|
38
|
+
timing: "BMO" | "AMC" | "TNS" | "";
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
export interface MarketDataRequestContext {
|
|
30
42
|
brokerId?: string;
|
|
31
43
|
brokerInstanceId?: string;
|
|
@@ -68,6 +80,7 @@ export interface DataProvider {
|
|
|
68
80
|
search(query: string, context?: SearchRequestContext): Promise<InstrumentSearchResult[]>;
|
|
69
81
|
getNews(ticker: string, count?: number, exchange?: string, context?: MarketDataRequestContext): Promise<NewsItem[]>;
|
|
70
82
|
getSecFilings?(ticker: string, count?: number, exchange?: string, context?: MarketDataRequestContext): Promise<SecFilingItem[]>;
|
|
83
|
+
getEarningsCalendar?(symbols: string[], context?: MarketDataRequestContext): Promise<EarningsEvent[]>;
|
|
71
84
|
getSecFilingContent?(filing: SecFilingItem): Promise<string | null>;
|
|
72
85
|
/** Fetch article summary/description by URL (lazy-loaded on selection) */
|
|
73
86
|
getArticleSummary(url: string): Promise<string | null>;
|
package/src/types/plugin.ts
CHANGED
|
@@ -169,6 +169,19 @@ export interface WizardStep {
|
|
|
169
169
|
body?: string[];
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
export interface CommandShortcutArgContext {
|
|
173
|
+
activeTicker: string | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface CommandShortcutArgDef {
|
|
177
|
+
placeholder?: string;
|
|
178
|
+
kind?: "text" | "ticker" | "ticker-list";
|
|
179
|
+
parse?: (
|
|
180
|
+
arg: string,
|
|
181
|
+
context: CommandShortcutArgContext,
|
|
182
|
+
) => Record<string, string>;
|
|
183
|
+
}
|
|
184
|
+
|
|
172
185
|
export interface CliHelpColumn {
|
|
173
186
|
header: string;
|
|
174
187
|
align?: "left" | "right" | "center";
|
|
@@ -243,6 +256,7 @@ export interface CommandDef {
|
|
|
243
256
|
label: string;
|
|
244
257
|
keywords: string[];
|
|
245
258
|
shortcut?: string;
|
|
259
|
+
shortcutArg?: CommandShortcutArgDef;
|
|
246
260
|
execute: (values?: Record<string, string>) => void | Promise<void>;
|
|
247
261
|
category: "navigation" | "data" | "portfolio" | "config";
|
|
248
262
|
description?: string;
|
|
@@ -355,6 +369,25 @@ export interface PluginPaneSettingsState {
|
|
|
355
369
|
delete(paneId: string, key: string): Promise<void>;
|
|
356
370
|
}
|
|
357
371
|
|
|
372
|
+
export type AppNotificationType = "info" | "success" | "error";
|
|
373
|
+
export type AppDesktopNotificationMode = "never" | "when-inactive" | "always";
|
|
374
|
+
|
|
375
|
+
export interface AppNotificationRequest {
|
|
376
|
+
title?: string;
|
|
377
|
+
body: string;
|
|
378
|
+
subtitle?: string;
|
|
379
|
+
duration?: number;
|
|
380
|
+
type?: AppNotificationType;
|
|
381
|
+
toast?: boolean;
|
|
382
|
+
desktop?: AppDesktopNotificationMode;
|
|
383
|
+
persistent?: boolean;
|
|
384
|
+
sound?: string;
|
|
385
|
+
action?: {
|
|
386
|
+
label: string;
|
|
387
|
+
onClick: () => void;
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
358
391
|
export interface GloomPluginContext {
|
|
359
392
|
registerPane(pane: PaneDef): void;
|
|
360
393
|
registerPaneTemplate(template: PaneTemplateDef): void;
|
|
@@ -392,6 +425,7 @@ export interface GloomPluginContext {
|
|
|
392
425
|
hidePane(paneId: string): void;
|
|
393
426
|
focusPane(paneId: string): void;
|
|
394
427
|
pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
|
|
428
|
+
navigateTicker(symbol: string): void;
|
|
395
429
|
openPaneSettings(paneId?: string): void;
|
|
396
430
|
|
|
397
431
|
on<K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void): () => void;
|
|
@@ -399,7 +433,7 @@ export interface GloomPluginContext {
|
|
|
399
433
|
|
|
400
434
|
showWidget(widgetId: string): void;
|
|
401
435
|
hideWidget(widgetId: string): void;
|
|
402
|
-
|
|
436
|
+
notify(notification: AppNotificationRequest): void;
|
|
403
437
|
}
|
|
404
438
|
|
|
405
439
|
export interface GloomPlugin {
|
|
@@ -3,6 +3,7 @@ import type { AuthUser } from "./api-client";
|
|
|
3
3
|
import { apiClient } from "./api-client";
|
|
4
4
|
|
|
5
5
|
const originalFetch = globalThis.fetch;
|
|
6
|
+
const originalWebSocket = globalThis.WebSocket;
|
|
6
7
|
|
|
7
8
|
const verifiedUser: AuthUser = {
|
|
8
9
|
id: "user-1",
|
|
@@ -33,7 +34,9 @@ function createResponse(body: unknown, options: { status?: number; cookies?: str
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
afterEach(() => {
|
|
37
|
+
apiClient.dispose();
|
|
36
38
|
globalThis.fetch = originalFetch;
|
|
39
|
+
globalThis.WebSocket = originalWebSocket;
|
|
37
40
|
apiClient.setSessionToken(null);
|
|
38
41
|
apiClient.setWebSocketToken(null);
|
|
39
42
|
});
|
|
@@ -83,6 +86,93 @@ describe("apiClient auth cookies", () => {
|
|
|
83
86
|
"__Secure-gloomberb.session_token=persisted-token.value; gloomberb.session_token=persisted-token.value",
|
|
84
87
|
]);
|
|
85
88
|
});
|
|
89
|
+
|
|
90
|
+
test("keeps cached identity when session refresh is rejected without a hard account-missing response", async () => {
|
|
91
|
+
apiClient.setSessionToken("persisted-token.value");
|
|
92
|
+
apiClient.restoreCachedUser(verifiedUser);
|
|
93
|
+
|
|
94
|
+
globalThis.fetch = (async () => createResponse({ message: "Unauthorized" }, { status: 401 })) as typeof fetch;
|
|
95
|
+
|
|
96
|
+
await expect(apiClient.getSession()).rejects.toThrow("Unauthorized");
|
|
97
|
+
expect(apiClient.getSessionToken()).toBe("persisted-token.value");
|
|
98
|
+
expect(apiClient.getCurrentUser()).toMatchObject({
|
|
99
|
+
id: verifiedUser.id,
|
|
100
|
+
username: verifiedUser.username,
|
|
101
|
+
emailVerified: true,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("clears cached identity when session refresh says the account no longer exists", async () => {
|
|
106
|
+
apiClient.setSessionToken("persisted-token.value");
|
|
107
|
+
apiClient.setWebSocketToken("ws-token");
|
|
108
|
+
apiClient.restoreCachedUser(verifiedUser);
|
|
109
|
+
|
|
110
|
+
globalThis.fetch = (async () => createResponse({ code: "USER_NOT_FOUND" }, { status: 403 })) as typeof fetch;
|
|
111
|
+
|
|
112
|
+
await expect(apiClient.getSession()).resolves.toBeNull();
|
|
113
|
+
expect(apiClient.getSessionToken()).toBeNull();
|
|
114
|
+
expect(apiClient.getWebSocketToken()).toBeNull();
|
|
115
|
+
expect(apiClient.getCurrentUser()).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("clears local session on explicit sign out even if the server request fails", async () => {
|
|
119
|
+
apiClient.setSessionToken("persisted-token.value");
|
|
120
|
+
apiClient.setWebSocketToken("ws-token");
|
|
121
|
+
apiClient.restoreCachedUser(verifiedUser);
|
|
122
|
+
|
|
123
|
+
globalThis.fetch = (async () => createResponse({ message: "server unavailable" }, { status: 503 })) as typeof fetch;
|
|
124
|
+
|
|
125
|
+
await expect(apiClient.signOut()).rejects.toThrow("server unavailable");
|
|
126
|
+
expect(apiClient.getSessionToken()).toBeNull();
|
|
127
|
+
expect(apiClient.getWebSocketToken()).toBeNull();
|
|
128
|
+
expect(apiClient.getCurrentUser()).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("drops a stale websocket token after socket close so reconnect can use the session token", () => {
|
|
132
|
+
const sockets: Array<{
|
|
133
|
+
url: string;
|
|
134
|
+
readyState: number;
|
|
135
|
+
onclose: ((event: unknown) => void) | null;
|
|
136
|
+
close: () => void;
|
|
137
|
+
}> = [];
|
|
138
|
+
|
|
139
|
+
class FakeWebSocket {
|
|
140
|
+
static OPEN = 1;
|
|
141
|
+
readyState = 0;
|
|
142
|
+
onopen: ((event: unknown) => void) | null = null;
|
|
143
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
144
|
+
onclose: ((event: unknown) => void) | null = null;
|
|
145
|
+
onerror: ((event: unknown) => void) | null = null;
|
|
146
|
+
|
|
147
|
+
constructor(readonly url: string) {
|
|
148
|
+
sockets.push(this);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
send(): void {}
|
|
152
|
+
|
|
153
|
+
close(): void {
|
|
154
|
+
this.readyState = 3;
|
|
155
|
+
this.onclose?.({ code: 1000, reason: "closed" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
globalThis.WebSocket = FakeWebSocket as any;
|
|
160
|
+
apiClient.setSessionToken("session-token");
|
|
161
|
+
apiClient.setWebSocketToken("stale-ws-token");
|
|
162
|
+
apiClient.restoreCachedUser(verifiedUser);
|
|
163
|
+
|
|
164
|
+
const unsubscribe = apiClient.subscribeQuotes([{ symbol: "AAPL" }], () => {});
|
|
165
|
+
|
|
166
|
+
expect(sockets).toHaveLength(1);
|
|
167
|
+
expect(sockets[0]!.url).toContain("token=stale-ws-token");
|
|
168
|
+
|
|
169
|
+
sockets[0]!.onclose?.({ code: 1008, reason: "Unauthorized" });
|
|
170
|
+
|
|
171
|
+
expect(apiClient.getWebSocketToken()).toBeNull();
|
|
172
|
+
expect(apiClient.getSessionToken()).toBe("session-token");
|
|
173
|
+
|
|
174
|
+
unsubscribe();
|
|
175
|
+
});
|
|
86
176
|
});
|
|
87
177
|
|
|
88
178
|
describe("apiClient chat timestamps", () => {
|
package/src/utils/api-client.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { normalizeTimestamp } from "./timestamp";
|
|
|
6
6
|
const DEFAULT_API_URL = "https://api.gloom.sh";
|
|
7
7
|
const SESSION_COOKIE_NAMES = ["__Secure-gloomberb.session_token", "gloomberb.session_token"] as const;
|
|
8
8
|
const cloudApiLog = debugLog.createLogger("cloud-api");
|
|
9
|
+
const HARD_SESSION_INVALID_PATTERNS = [
|
|
10
|
+
/\b(user|account)\b.*\b(not found|deleted|removed|disabled|deactivated|suspended)\b/i,
|
|
11
|
+
/\b(user|account)\b.*\bdoes(?:\s+not|n't)\s+exist\b/i,
|
|
12
|
+
/\b(no|unknown|missing)\s+(user|account)\b/i,
|
|
13
|
+
];
|
|
9
14
|
|
|
10
15
|
export interface ChatMessage {
|
|
11
16
|
id: string;
|
|
@@ -131,6 +136,22 @@ class ApiRequestError extends Error {
|
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
function parseApiErrorMessage(body: string): string {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
142
|
+
const parts = [parsed.message, parsed.error, parsed.code, parsed.reason]
|
|
143
|
+
.filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
|
144
|
+
return parts.join(" ") || body;
|
|
145
|
+
} catch {
|
|
146
|
+
return body;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isHardSessionInvalidMessage(message: string): boolean {
|
|
151
|
+
const normalized = message.replace(/[_-]+/g, " ");
|
|
152
|
+
return HARD_SESSION_INVALID_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
class GloomApiClient {
|
|
135
156
|
private sessionToken: string | null = null;
|
|
136
157
|
private sessionCookieName: SessionCookieName | null = null;
|
|
@@ -262,12 +283,7 @@ class GloomApiClient {
|
|
|
262
283
|
|
|
263
284
|
if (!res.ok) {
|
|
264
285
|
const body = await res.text();
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
msg = JSON.parse(body).message ?? body;
|
|
268
|
-
} catch {
|
|
269
|
-
msg = body;
|
|
270
|
-
}
|
|
286
|
+
const msg = parseApiErrorMessage(body);
|
|
271
287
|
throw new ApiRequestError(msg, res.status);
|
|
272
288
|
}
|
|
273
289
|
|
|
@@ -300,10 +316,11 @@ class GloomApiClient {
|
|
|
300
316
|
|
|
301
317
|
const socketToken = this.getSocketAuthToken();
|
|
302
318
|
if (!socketToken) return;
|
|
319
|
+
const usingWebSocketToken = !!this.websocketToken;
|
|
303
320
|
const url = `${this.getWebSocketBaseUrl()}/cloud/ws?token=${encodeURIComponent(socketToken)}`;
|
|
304
321
|
cloudApiLog.info("open websocket", {
|
|
305
322
|
hasToken: !!socketToken,
|
|
306
|
-
tokenSource:
|
|
323
|
+
tokenSource: usingWebSocketToken ? "websocket" : "session",
|
|
307
324
|
quoteTargets: this.quoteTargets.size,
|
|
308
325
|
channelTargets: this.channelListeners.size,
|
|
309
326
|
});
|
|
@@ -321,11 +338,24 @@ class GloomApiClient {
|
|
|
321
338
|
void this.handleSocketMessage(String(event.data));
|
|
322
339
|
};
|
|
323
340
|
|
|
324
|
-
ws.onclose = () => {
|
|
341
|
+
ws.onclose = (event) => {
|
|
342
|
+
const activeSocket = this.ws === ws;
|
|
325
343
|
if (this.ws === ws) {
|
|
326
344
|
this.ws = null;
|
|
327
345
|
}
|
|
328
|
-
|
|
346
|
+
const closeEvent = event as CloseEvent | undefined;
|
|
347
|
+
cloudApiLog.warn("websocket closed", {
|
|
348
|
+
quoteTargets: this.quoteTargets.size,
|
|
349
|
+
channelTargets: this.channelListeners.size,
|
|
350
|
+
code: closeEvent?.code,
|
|
351
|
+
reason: closeEvent?.reason,
|
|
352
|
+
tokenSource: usingWebSocketToken ? "websocket" : "session",
|
|
353
|
+
});
|
|
354
|
+
if (activeSocket && usingWebSocketToken && this.sessionToken) {
|
|
355
|
+
this.websocketToken = null;
|
|
356
|
+
this.reconnectDelayMs = 1000;
|
|
357
|
+
cloudApiLog.warn("cleared websocket token after socket close; falling back to session token");
|
|
358
|
+
}
|
|
329
359
|
if (!this.shouldKeepSocketOpen()) return;
|
|
330
360
|
this.scheduleReconnect();
|
|
331
361
|
};
|
|
@@ -410,6 +440,14 @@ class GloomApiClient {
|
|
|
410
440
|
|
|
411
441
|
if (parsed?.type === "auth.unverified") {
|
|
412
442
|
cloudApiLog.warn("websocket marked unverified");
|
|
443
|
+
if (this.websocketToken && this.sessionToken) {
|
|
444
|
+
this.websocketToken = null;
|
|
445
|
+
this.reconnectDelayMs = 1000;
|
|
446
|
+
cloudApiLog.warn("cleared websocket token after auth rejection; falling back to session token");
|
|
447
|
+
this.teardownSocket();
|
|
448
|
+
this.scheduleReconnect();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
413
451
|
if (this.currentUser) {
|
|
414
452
|
this.currentUser = { ...this.currentUser, emailVerified: false };
|
|
415
453
|
}
|
|
@@ -464,9 +502,11 @@ class GloomApiClient {
|
|
|
464
502
|
}
|
|
465
503
|
|
|
466
504
|
async signOut(): Promise<void> {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
505
|
+
try {
|
|
506
|
+
await this.request("/api/auth/api/auth/sign-out", { method: "POST" });
|
|
507
|
+
} finally {
|
|
508
|
+
this.setSessionToken(null);
|
|
509
|
+
}
|
|
470
510
|
}
|
|
471
511
|
|
|
472
512
|
async getSession(): Promise<AuthUser | null> {
|
|
@@ -478,11 +518,11 @@ class GloomApiClient {
|
|
|
478
518
|
this.setCurrentUser(user);
|
|
479
519
|
return user;
|
|
480
520
|
} catch (error) {
|
|
481
|
-
if (
|
|
482
|
-
|
|
521
|
+
if (error instanceof ApiRequestError && isHardSessionInvalidMessage(error.message)) {
|
|
522
|
+
this.setSessionToken(null);
|
|
523
|
+
return null;
|
|
483
524
|
}
|
|
484
|
-
|
|
485
|
-
return null;
|
|
525
|
+
throw error;
|
|
486
526
|
}
|
|
487
527
|
}
|
|
488
528
|
|
package/src/utils/format.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { formatTimeAgo } from "./format";
|
|
2
|
+
import { displayWidth, formatTimeAgo, padTo } from "./format";
|
|
3
3
|
|
|
4
4
|
describe("formatTimeAgo", () => {
|
|
5
5
|
test("handles UTC ISO timestamps with explicit offsets", () => {
|
|
@@ -12,3 +12,13 @@ describe("formatTimeAgo", () => {
|
|
|
12
12
|
expect(formatTimeAgo(fiveMinutesAgo)).toBe("5m ago");
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
describe("padTo", () => {
|
|
17
|
+
test("pads and truncates by display width instead of UTF-16 length", () => {
|
|
18
|
+
expect(displayWidth("πΊπΈ")).toBe(2);
|
|
19
|
+
expect(padTo("πΊπΈ", 2)).toBe("πΊπΈ");
|
|
20
|
+
expect(padTo("πΊπΈ", 3)).toBe("πΊπΈ ");
|
|
21
|
+
expect(padTo("πΊπΈ", 1)).toBe(" ");
|
|
22
|
+
expect(padTo("πΊπΈ CPI", 6)).toBe("πΊπΈ CPI");
|
|
23
|
+
});
|
|
24
|
+
});
|