gloomberb 0.12.0 → 0.13.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 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.ts +10 -2
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +4 -4
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +193 -23
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/surface/assist.test.tsx +20 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +189 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +274 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +5 -2
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/index.tsx +4 -0
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HeadlessPaneDefinition } from "../../../../types/headless";
|
|
3
|
+
import { marketValuationHeadless } from "../../market-valuation/headless";
|
|
4
|
+
import {
|
|
5
|
+
ASKG_PROTOCOL_VERSION,
|
|
6
|
+
TOOL_NAME_PATTERN,
|
|
7
|
+
type ASKGSessionStartRequest,
|
|
8
|
+
type ASKGSessionStartResponse,
|
|
9
|
+
type ASKGSseEvent,
|
|
10
|
+
type ToolManifest,
|
|
11
|
+
type ToolResultPayload,
|
|
12
|
+
} from "./protocol";
|
|
13
|
+
|
|
14
|
+
function jsonRoundTrip<T>(value: T): T {
|
|
15
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function projectHeadlessDefinition(
|
|
19
|
+
definition: HeadlessPaneDefinition,
|
|
20
|
+
): ToolManifest {
|
|
21
|
+
const options = definition.options.map(({ settingKey: _settingKey, pluginState: _pluginState, ...option }) => option);
|
|
22
|
+
const columns = definition.columns?.map(({ format: _format, ...column }) => column);
|
|
23
|
+
return {
|
|
24
|
+
name: "market.valuation",
|
|
25
|
+
source: "headless",
|
|
26
|
+
title: "Market valuation",
|
|
27
|
+
description: "Compare whole-market valuation ratios with their history.",
|
|
28
|
+
writeTier: "read",
|
|
29
|
+
shape: definition.shape,
|
|
30
|
+
argument: definition.argument,
|
|
31
|
+
options,
|
|
32
|
+
...(columns ? { columns } : {}),
|
|
33
|
+
confirm: "never",
|
|
34
|
+
timeoutMs: 30_000,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("ASKG protocol", () => {
|
|
39
|
+
test("round-trips session negotiation, a manifest, every event, and a tool result", () => {
|
|
40
|
+
const manifest: ToolManifest = {
|
|
41
|
+
name: "layout.place_pane",
|
|
42
|
+
source: "remote-op",
|
|
43
|
+
title: "Place pane",
|
|
44
|
+
description: "Move a pane to a layout region.",
|
|
45
|
+
writeTier: "ui-write",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
paneId: { type: "string" },
|
|
50
|
+
region: { type: "string", enum: ["left", "right", "floating"] },
|
|
51
|
+
},
|
|
52
|
+
required: ["paneId", "region"],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
confirm: "never",
|
|
56
|
+
timeoutMs: 10_000,
|
|
57
|
+
};
|
|
58
|
+
const sessionRequest: ASKGSessionStartRequest = {
|
|
59
|
+
protocolVersion: ASKG_PROTOCOL_VERSION,
|
|
60
|
+
client: { kind: "tui", version: "0.8.0" },
|
|
61
|
+
context: {
|
|
62
|
+
query: "Compare NVDA valuation",
|
|
63
|
+
symbol: "NVDA",
|
|
64
|
+
paneId: "ticker:main",
|
|
65
|
+
layout: { id: "layout-1" },
|
|
66
|
+
},
|
|
67
|
+
tools: [manifest],
|
|
68
|
+
manifestHash: "sha256:manifest-1",
|
|
69
|
+
};
|
|
70
|
+
const sessionResponse: ASKGSessionStartResponse = {
|
|
71
|
+
protocolVersion: ASKG_PROTOCOL_VERSION,
|
|
72
|
+
sessionId: "session-1",
|
|
73
|
+
manifestHash: sessionRequest.manifestHash,
|
|
74
|
+
serverTools: [],
|
|
75
|
+
acceptedTools: [manifest.name],
|
|
76
|
+
rejectedTools: [{ name: "invalid.tool", reason: "Unknown source" }],
|
|
77
|
+
limits: {
|
|
78
|
+
requestsPerMinute: 20,
|
|
79
|
+
turnsPerDay: 300,
|
|
80
|
+
turnsRemainingToday: 299,
|
|
81
|
+
maxToolCallsPerTurn: 25,
|
|
82
|
+
turnWallClockMs: 90_000,
|
|
83
|
+
clientToolTimeoutMs: 10_000,
|
|
84
|
+
},
|
|
85
|
+
tier: "pro",
|
|
86
|
+
model: "gpt-5.6-luna",
|
|
87
|
+
promptVersion: "2026-09-04",
|
|
88
|
+
expiresAt: "2026-09-04T21:30:00.000Z",
|
|
89
|
+
};
|
|
90
|
+
const events: ASKGSseEvent[] = [
|
|
91
|
+
{
|
|
92
|
+
type: "session",
|
|
93
|
+
seq: 1,
|
|
94
|
+
sessionId: "session-1",
|
|
95
|
+
turnId: "turn-1",
|
|
96
|
+
model: "gpt-5.6-luna",
|
|
97
|
+
promptVersion: "2026-09-04",
|
|
98
|
+
},
|
|
99
|
+
{ type: "text-delta", seq: 2, turnId: "turn-1", delta: "NVDA" },
|
|
100
|
+
{
|
|
101
|
+
type: "tool-call",
|
|
102
|
+
seq: 3,
|
|
103
|
+
turnId: "turn-1",
|
|
104
|
+
toolCallId: "call-1",
|
|
105
|
+
name: manifest.name,
|
|
106
|
+
args: { paneId: "chart:main", region: "right" },
|
|
107
|
+
writeTier: "ui-write",
|
|
108
|
+
requiresConfirmation: false,
|
|
109
|
+
preview: { paneId: "chart:main", region: "right", dryRun: true },
|
|
110
|
+
timeoutMs: 10_000,
|
|
111
|
+
expiresAt: "2026-09-04T21:00:10.000Z",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: "tool-executed",
|
|
115
|
+
seq: 4,
|
|
116
|
+
turnId: "turn-1",
|
|
117
|
+
toolCallId: "call-server-1",
|
|
118
|
+
name: "market.quote",
|
|
119
|
+
source: "server",
|
|
120
|
+
status: "ok",
|
|
121
|
+
summary: {
|
|
122
|
+
rowCount: 1,
|
|
123
|
+
elapsedMs: 24,
|
|
124
|
+
truncated: false,
|
|
125
|
+
note: "Fresh quote",
|
|
126
|
+
sample: { symbol: "NVDA", price: 180 },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{ type: "tool-result-ack", seq: 5, turnId: "turn-1", toolCallId: "call-1" },
|
|
130
|
+
{
|
|
131
|
+
type: "error",
|
|
132
|
+
seq: 6,
|
|
133
|
+
turnId: "turn-1",
|
|
134
|
+
code: "rate_limited",
|
|
135
|
+
message: "Try again shortly.",
|
|
136
|
+
retryable: true,
|
|
137
|
+
retryAfterMs: 1_000,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: "usage",
|
|
141
|
+
seq: 7,
|
|
142
|
+
turnId: "turn-1",
|
|
143
|
+
inputTokens: 120,
|
|
144
|
+
outputTokens: 30,
|
|
145
|
+
totalTokens: 150,
|
|
146
|
+
estimated: false,
|
|
147
|
+
},
|
|
148
|
+
{ type: "done", seq: 8, turnId: "turn-1", reason: "complete" },
|
|
149
|
+
];
|
|
150
|
+
const result: ToolResultPayload = {
|
|
151
|
+
turnId: "turn-1",
|
|
152
|
+
toolCallId: "call-1",
|
|
153
|
+
status: "ok",
|
|
154
|
+
result: { moved: true },
|
|
155
|
+
rowCount: 1,
|
|
156
|
+
truncated: false,
|
|
157
|
+
elapsedMs: 12,
|
|
158
|
+
rev: "rev-2",
|
|
159
|
+
undoToken: "undo-1",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
expect(new RegExp(TOOL_NAME_PATTERN).test(manifest.name)).toBe(true);
|
|
163
|
+
expect(jsonRoundTrip(manifest)).toEqual(manifest);
|
|
164
|
+
expect(jsonRoundTrip(sessionRequest)).toEqual(sessionRequest);
|
|
165
|
+
expect(jsonRoundTrip(sessionResponse)).toEqual(sessionResponse);
|
|
166
|
+
expect(events.map(jsonRoundTrip)).toEqual(events);
|
|
167
|
+
expect(jsonRoundTrip(result)).toEqual(result);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("projects a migrated headless definition without executable fields", () => {
|
|
171
|
+
const manifest = projectHeadlessDefinition(marketValuationHeadless);
|
|
172
|
+
const serialized = JSON.stringify(manifest);
|
|
173
|
+
|
|
174
|
+
expect(new RegExp(TOOL_NAME_PATTERN).test(manifest.name)).toBe(true);
|
|
175
|
+
expect(manifest).toMatchObject({
|
|
176
|
+
source: "headless",
|
|
177
|
+
writeTier: "read",
|
|
178
|
+
shape: marketValuationHeadless.shape,
|
|
179
|
+
argument: marketValuationHeadless.argument,
|
|
180
|
+
confirm: "never",
|
|
181
|
+
});
|
|
182
|
+
expect(manifest.options).toHaveLength(marketValuationHeadless.options.length);
|
|
183
|
+
expect(serialized).not.toContain("load");
|
|
184
|
+
expect(serialized).not.toContain("settingKey");
|
|
185
|
+
expect(serialized).not.toContain("pluginState");
|
|
186
|
+
expect(serialized).not.toContain("format");
|
|
187
|
+
expect(jsonRoundTrip(manifest)).toEqual(manifest);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// Wire declarations are mirrored in gloomberb-platform/server/src/services/askg/types.ts.
|
|
2
|
+
import type {
|
|
3
|
+
HeadlessPaneArgumentDef,
|
|
4
|
+
HeadlessPaneColumn,
|
|
5
|
+
HeadlessPaneOptionDef,
|
|
6
|
+
HeadlessPaneOptionValue,
|
|
7
|
+
HeadlessPaneShape,
|
|
8
|
+
} from "../../../../types/headless";
|
|
9
|
+
import type {
|
|
10
|
+
RemoteJsonSchema,
|
|
11
|
+
RemoteJsonSchemaType,
|
|
12
|
+
RemoteWriteTier,
|
|
13
|
+
} from "../../../../remote/types";
|
|
14
|
+
|
|
15
|
+
/** Major version of the ASKG client and server wire contract. */
|
|
16
|
+
export const ASKG_PROTOCOL_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
/** Maximum accepted JSON-encoded client tool result size. */
|
|
19
|
+
export const MAX_TOOL_RESULT_BYTES = 262_144;
|
|
20
|
+
|
|
21
|
+
/** Source form for valid tool names advertised to ASKG. */
|
|
22
|
+
export const TOOL_NAME_PATTERN = "^[a-z][a-z0-9_.]{2,48}$";
|
|
23
|
+
|
|
24
|
+
/** JSON value accepted on the ASKG wire. */
|
|
25
|
+
export type JsonValue =
|
|
26
|
+
| string
|
|
27
|
+
| number
|
|
28
|
+
| boolean
|
|
29
|
+
| null
|
|
30
|
+
| JsonValue[]
|
|
31
|
+
| { [key: string]: JsonValue };
|
|
32
|
+
|
|
33
|
+
/** Safety tier applied before a client tool is executed. */
|
|
34
|
+
export type WriteTier = RemoteWriteTier;
|
|
35
|
+
|
|
36
|
+
/** Runtime that opened an ASKG session. */
|
|
37
|
+
export type ASKGClientKind = "tui" | "desktop" | "web";
|
|
38
|
+
|
|
39
|
+
/** Origin of an advertised client tool. */
|
|
40
|
+
export type ToolManifestSource = "headless" | "remote-op";
|
|
41
|
+
|
|
42
|
+
/** Headless result shapes supported by the tool timeline. */
|
|
43
|
+
export type ToolManifestShape = HeadlessPaneShape;
|
|
44
|
+
|
|
45
|
+
/** Headless argument kinds understood by the manifest compiler. */
|
|
46
|
+
export type ToolManifestArgumentKind = HeadlessPaneArgumentDef["kind"];
|
|
47
|
+
|
|
48
|
+
/** Serializable headless argument declaration. */
|
|
49
|
+
export type ToolManifestArgument = HeadlessPaneArgumentDef;
|
|
50
|
+
|
|
51
|
+
/** Values accepted by a headless enum option. */
|
|
52
|
+
export type ToolManifestOptionValue = HeadlessPaneOptionValue;
|
|
53
|
+
|
|
54
|
+
/** Headless option types understood by the manifest compiler. */
|
|
55
|
+
export type ToolManifestOptionType = HeadlessPaneOptionDef["type"];
|
|
56
|
+
|
|
57
|
+
/** Serializable headless option declaration. */
|
|
58
|
+
export type ToolManifestOption = Omit<HeadlessPaneOptionDef, "settingKey" | "pluginState">;
|
|
59
|
+
|
|
60
|
+
/** Serializable headless column declaration. */
|
|
61
|
+
export type ToolManifestColumn = Omit<HeadlessPaneColumn, "format">;
|
|
62
|
+
|
|
63
|
+
/** JSON Schema primitive types accepted for remote operation inputs. */
|
|
64
|
+
export type ToolInputSchemaType = RemoteJsonSchemaType;
|
|
65
|
+
|
|
66
|
+
/** JSON Schema accepted by a remote operation tool. */
|
|
67
|
+
export type ToolInputSchema = RemoteJsonSchema;
|
|
68
|
+
|
|
69
|
+
/** Client or server tool metadata negotiated when a session starts. */
|
|
70
|
+
export interface ToolManifest {
|
|
71
|
+
/** Lowercase stable identifier matching TOOL_NAME_PATTERN. */
|
|
72
|
+
name: string;
|
|
73
|
+
source: ToolManifestSource;
|
|
74
|
+
title: string;
|
|
75
|
+
description: string;
|
|
76
|
+
writeTier: WriteTier;
|
|
77
|
+
shape?: ToolManifestShape;
|
|
78
|
+
argument?: ToolManifestArgument;
|
|
79
|
+
options?: ToolManifestOption[];
|
|
80
|
+
columns?: ToolManifestColumn[];
|
|
81
|
+
inputSchema?: ToolInputSchema;
|
|
82
|
+
confirm: "never" | "always";
|
|
83
|
+
timeoutMs: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Client identity included in a session negotiation. */
|
|
87
|
+
export interface ASKGClientDescriptor {
|
|
88
|
+
kind: ASKGClientKind;
|
|
89
|
+
version: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Terminal context supplied to the model at session start. */
|
|
93
|
+
export interface ASKGSessionContext {
|
|
94
|
+
query?: string;
|
|
95
|
+
symbol?: string;
|
|
96
|
+
paneId?: string;
|
|
97
|
+
layout?: JsonValue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Request used to negotiate tools and limits for an ASKG session. */
|
|
101
|
+
export interface ASKGSessionStartRequest {
|
|
102
|
+
protocolVersion: typeof ASKG_PROTOCOL_VERSION;
|
|
103
|
+
client: ASKGClientDescriptor;
|
|
104
|
+
context: ASKGSessionContext;
|
|
105
|
+
tools: ToolManifest[];
|
|
106
|
+
manifestHash: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Per-user and per-turn limits returned by the platform. */
|
|
110
|
+
export interface ASKGLimits {
|
|
111
|
+
requestsPerMinute: number;
|
|
112
|
+
turnsPerDay: number;
|
|
113
|
+
turnsRemainingToday: number;
|
|
114
|
+
maxToolCallsPerTurn: number;
|
|
115
|
+
turnWallClockMs: number;
|
|
116
|
+
clientToolTimeoutMs: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Client tool rejected during session negotiation. */
|
|
120
|
+
export interface ASKGRejectedTool {
|
|
121
|
+
name: string;
|
|
122
|
+
reason: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Successful ASKG session negotiation response. */
|
|
126
|
+
export interface ASKGSessionStartResponse {
|
|
127
|
+
protocolVersion: typeof ASKG_PROTOCOL_VERSION;
|
|
128
|
+
sessionId: string;
|
|
129
|
+
manifestHash: string;
|
|
130
|
+
serverTools: ToolManifest[];
|
|
131
|
+
acceptedTools: string[];
|
|
132
|
+
rejectedTools: ASKGRejectedTool[];
|
|
133
|
+
limits: ASKGLimits;
|
|
134
|
+
tier: string;
|
|
135
|
+
model: string;
|
|
136
|
+
promptVersion: string;
|
|
137
|
+
expiresAt: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Shared fields carried by every turn stream event. */
|
|
141
|
+
export interface ASKGEventBase {
|
|
142
|
+
seq: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Announces the session and turn attached to this stream. */
|
|
146
|
+
export interface ASKGSessionEvent extends ASKGEventBase {
|
|
147
|
+
type: "session";
|
|
148
|
+
sessionId: string;
|
|
149
|
+
turnId: string;
|
|
150
|
+
model: string;
|
|
151
|
+
promptVersion: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Appends model text to the visible answer. */
|
|
155
|
+
export interface ASKGTextDeltaEvent extends ASKGEventBase {
|
|
156
|
+
type: "text-delta";
|
|
157
|
+
turnId: string;
|
|
158
|
+
delta: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Delegates one negotiated tool call to the client. */
|
|
162
|
+
export interface ASKGToolCallEvent extends ASKGEventBase {
|
|
163
|
+
type: "tool-call";
|
|
164
|
+
turnId: string;
|
|
165
|
+
toolCallId: string;
|
|
166
|
+
name: string;
|
|
167
|
+
args: Record<string, JsonValue>;
|
|
168
|
+
writeTier: WriteTier;
|
|
169
|
+
requiresConfirmation: boolean;
|
|
170
|
+
preview: JsonValue;
|
|
171
|
+
timeoutMs: number;
|
|
172
|
+
expiresAt: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Compact result information suitable for the tool timeline. */
|
|
176
|
+
export interface ToolExecutionSummary {
|
|
177
|
+
rowCount?: number;
|
|
178
|
+
elapsedMs: number;
|
|
179
|
+
truncated: boolean;
|
|
180
|
+
note?: string;
|
|
181
|
+
sample?: JsonValue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Status returned by either a server or client tool execution. */
|
|
185
|
+
export type ToolResultStatus =
|
|
186
|
+
| "ok"
|
|
187
|
+
| "error"
|
|
188
|
+
| "denied"
|
|
189
|
+
| "timeout"
|
|
190
|
+
| "partial"
|
|
191
|
+
| "cancelled";
|
|
192
|
+
|
|
193
|
+
/** Reports a completed server tool or an accepted client execution. */
|
|
194
|
+
export interface ASKGToolExecutedEvent extends ASKGEventBase {
|
|
195
|
+
type: "tool-executed";
|
|
196
|
+
turnId: string;
|
|
197
|
+
toolCallId: string;
|
|
198
|
+
name: string;
|
|
199
|
+
source: ToolManifestSource | "server";
|
|
200
|
+
status: ToolResultStatus;
|
|
201
|
+
summary: ToolExecutionSummary;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Confirms that one client tool result was accepted by the turn loop. */
|
|
205
|
+
export interface ASKGToolResultAckEvent extends ASKGEventBase {
|
|
206
|
+
type: "tool-result-ack";
|
|
207
|
+
turnId: string;
|
|
208
|
+
toolCallId: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Stable error codes that clients can handle without parsing text. */
|
|
212
|
+
export type ASKGErrorCode =
|
|
213
|
+
| "rate_limited"
|
|
214
|
+
| "daily_turn_cap"
|
|
215
|
+
| "tool_budget_exhausted"
|
|
216
|
+
| "turn_timeout"
|
|
217
|
+
| "model_usage_limit"
|
|
218
|
+
| "model_unavailable"
|
|
219
|
+
| "internal";
|
|
220
|
+
|
|
221
|
+
/** Reports a recoverable or terminal session or turn failure. */
|
|
222
|
+
export interface ASKGErrorEvent extends ASKGEventBase {
|
|
223
|
+
type: "error";
|
|
224
|
+
turnId?: string;
|
|
225
|
+
code: ASKGErrorCode;
|
|
226
|
+
message: string;
|
|
227
|
+
retryable: boolean;
|
|
228
|
+
retryAfterMs?: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Token accounting emitted when the model provider supplies it. */
|
|
232
|
+
export interface ASKGUsageEvent extends ASKGEventBase {
|
|
233
|
+
type: "usage";
|
|
234
|
+
turnId: string;
|
|
235
|
+
inputTokens?: number;
|
|
236
|
+
outputTokens?: number;
|
|
237
|
+
totalTokens?: number;
|
|
238
|
+
estimated: boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Reason a turn stream stopped. */
|
|
242
|
+
export type ASKGDoneReason = "complete" | "cancelled" | "error" | "timeout";
|
|
243
|
+
|
|
244
|
+
/** Terminates a turn stream. */
|
|
245
|
+
export interface ASKGDoneEvent extends ASKGEventBase {
|
|
246
|
+
type: "done";
|
|
247
|
+
turnId: string;
|
|
248
|
+
reason: ASKGDoneReason;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Event payloads emitted over the ASKG turn SSE stream. */
|
|
252
|
+
export type ASKGSseEvent =
|
|
253
|
+
| ASKGSessionEvent
|
|
254
|
+
| ASKGTextDeltaEvent
|
|
255
|
+
| ASKGToolCallEvent
|
|
256
|
+
| ASKGToolExecutedEvent
|
|
257
|
+
| ASKGToolResultAckEvent
|
|
258
|
+
| ASKGErrorEvent
|
|
259
|
+
| ASKGUsageEvent
|
|
260
|
+
| ASKGDoneEvent;
|
|
261
|
+
|
|
262
|
+
/** Client result posted for one delegated tool call. */
|
|
263
|
+
export interface ToolResultPayload {
|
|
264
|
+
turnId: string;
|
|
265
|
+
toolCallId: string;
|
|
266
|
+
status: ToolResultStatus;
|
|
267
|
+
result?: JsonValue;
|
|
268
|
+
rowCount?: number;
|
|
269
|
+
truncated: boolean;
|
|
270
|
+
elapsedMs: number;
|
|
271
|
+
note?: string;
|
|
272
|
+
rev?: string;
|
|
273
|
+
undoToken?: string;
|
|
274
|
+
}
|
|
@@ -12,6 +12,7 @@ import { useAppLanguage } from "../../../i18n/react";
|
|
|
12
12
|
import { useDialog, type PromptContext } from "../../../ui/dialog";
|
|
13
13
|
import { AuthForm, authFormTitle } from "./auth-form";
|
|
14
14
|
import type { AccountMode } from "./auth-model";
|
|
15
|
+
import { CloudVerificationPanel } from "./verification-panel";
|
|
15
16
|
|
|
16
17
|
export function AuthDialog({
|
|
17
18
|
initialMode,
|
|
@@ -20,13 +21,18 @@ export function AuthDialog({
|
|
|
20
21
|
}: PromptContext<AuthUser | undefined> & { initialMode: AccountMode }) {
|
|
21
22
|
useAppLanguage();
|
|
22
23
|
const [mode, setMode] = useState<AccountMode>(initialMode);
|
|
24
|
+
const [verifyingUser, setVerifyingUser] = useState<AuthUser | null>(null);
|
|
25
|
+
|
|
26
|
+
if (verifyingUser) return <DialogFrame title={`Confirm ${verifyingUser.email}`}>
|
|
27
|
+
<CloudVerificationPanel onVerified={resolve} onContinueFree={() => resolve(verifyingUser)} />
|
|
28
|
+
</DialogFrame>;
|
|
23
29
|
|
|
24
30
|
return (
|
|
25
31
|
<DialogFrame title={authFormTitle(mode)}>
|
|
26
32
|
<AuthForm
|
|
27
33
|
initialMode={initialMode}
|
|
28
34
|
onModeChange={setMode}
|
|
29
|
-
onSignedIn={resolve}
|
|
35
|
+
onSignedIn={(user) => user.emailVerified ? resolve(user) : setVerifyingUser(user)}
|
|
30
36
|
onEscape={dismiss}
|
|
31
37
|
/>
|
|
32
38
|
</DialogFrame>
|
|
@@ -8,9 +8,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
8
8
|
import type { AuthUser } from "../../../api-client";
|
|
9
9
|
import { t, tf } from "../../../i18n";
|
|
10
10
|
import { useAppLanguage } from "../../../i18n/react";
|
|
11
|
-
import { useViewport } from "../../../react/input";
|
|
11
|
+
import { useShortcut, useViewport } from "../../../react/input";
|
|
12
12
|
import { colors } from "../../../theme/colors";
|
|
13
|
-
import { Box, Text, TextAttributes, useUiHost } from "../../../ui";
|
|
13
|
+
import { Box, Text, TextAttributes, useUiHost, useRendererHost } from "../../../ui";
|
|
14
|
+
import { Button } from "../../../components/ui/button";
|
|
14
15
|
import { renderAsciiText } from "../../../ui/ascii-font";
|
|
15
16
|
import { useDialog, useDialogKeyboard, type PromptContext } from "../../../ui/dialog";
|
|
16
17
|
import { renderQrLines, renderQrSvgDataUri } from "../../../ui/qr";
|
|
@@ -55,6 +56,7 @@ export function DeviceSignInPanel({
|
|
|
55
56
|
height: number;
|
|
56
57
|
}) {
|
|
57
58
|
useAppLanguage();
|
|
59
|
+
const renderer = useRendererHost();
|
|
58
60
|
const desktop = useUiHost().kind === "desktop-web";
|
|
59
61
|
// Cell lines still drive layout on desktop: they give the code its row/column
|
|
60
62
|
// footprint, but the pixels come from the SVG below.
|
|
@@ -67,14 +69,25 @@ export function DeviceSignInPanel({
|
|
|
67
69
|
[desktop, snapshot.verificationUri],
|
|
68
70
|
);
|
|
69
71
|
const status = deviceSignInStatus(snapshot);
|
|
72
|
+
useShortcut((event) => {
|
|
73
|
+
if (!isPlainKey(event, "b") || !snapshot.verificationUri) return;
|
|
74
|
+
event.preventDefault(); event.stopPropagation();
|
|
75
|
+
void renderer.openExternal(snapshot.verificationUri).catch(() => {});
|
|
76
|
+
}, { scope: "device-signin:browser", phase: "before" });
|
|
70
77
|
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
78
|
+
// Reserve the browser button before fitting the QR, code, and status.
|
|
79
|
+
const contentHeight = height - (snapshot.verificationUri ? 2 : 0);
|
|
80
|
+
const showQr = qrLines.length > 0 && contentHeight >= qrLines.length + 2;
|
|
81
|
+
const spacious = showQr && contentHeight >= qrLines.length + 8;
|
|
82
|
+
const showUrl = !!snapshot.verificationUri && (!showQr || contentHeight >= qrLines.length + 4);
|
|
75
83
|
|
|
76
84
|
return (
|
|
77
85
|
<Box flexDirection="column" alignItems="center">
|
|
86
|
+
{snapshot.verificationUri && <Box height={2}>
|
|
87
|
+
<Button label="Continue in browser" shortcut="b" variant="primary" onPress={() => {
|
|
88
|
+
void renderer.openExternal(snapshot.verificationUri!).catch(() => {});
|
|
89
|
+
}} />
|
|
90
|
+
</Box>}
|
|
78
91
|
{showQr && (qrImage
|
|
79
92
|
? (
|
|
80
93
|
<Box
|
|
@@ -128,7 +141,7 @@ export function DeviceSignInPanel({
|
|
|
128
141
|
{spacious && <Box height={1} />}
|
|
129
142
|
<Box height={1}>
|
|
130
143
|
<Text fg={colors.textMuted}>
|
|
131
|
-
{
|
|
144
|
+
{snapshot.verificationUri ?? ""}
|
|
132
145
|
</Text>
|
|
133
146
|
</Box>
|
|
134
147
|
</>
|
|
@@ -184,7 +197,7 @@ export function DeviceSignInDialog({ resolve, dismiss }: PromptContext<AuthUser
|
|
|
184
197
|
<Box flexDirection="column" alignItems="center">
|
|
185
198
|
<Box height={1}>
|
|
186
199
|
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>
|
|
187
|
-
{t("
|
|
200
|
+
{t("Sign in through your browser or scan the code")}
|
|
188
201
|
</Text>
|
|
189
202
|
</Box>
|
|
190
203
|
<Box height={1} />
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { apiClient, type AuthUser } from "../../../api-client";
|
|
3
|
+
import { Button } from "../../../components/ui/button";
|
|
4
|
+
import { useShortcut } from "../../../react/input";
|
|
5
|
+
import { Box, Text } from "../../../ui";
|
|
6
|
+
import { colors } from "../../../theme/colors";
|
|
7
|
+
|
|
8
|
+
/** Shared continuation after email signup in the terminal and browser. */
|
|
9
|
+
export function CloudVerificationPanel({ onVerified, onContinueFree }: {
|
|
10
|
+
onVerified: (user: AuthUser) => void;
|
|
11
|
+
onContinueFree: () => void;
|
|
12
|
+
}) {
|
|
13
|
+
const [status, setStatus] = useState("Check your inbox and spam folder. This page continues automatically.");
|
|
14
|
+
const [sending, setSending] = useState(false);
|
|
15
|
+
const [cooldown, setCooldown] = useState(false);
|
|
16
|
+
const completed = useRef(false);
|
|
17
|
+
const callback = useRef(onVerified);
|
|
18
|
+
callback.current = onVerified;
|
|
19
|
+
const check = useCallback(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const user = await apiClient.getSession();
|
|
22
|
+
if (user?.emailVerified && !completed.current) {
|
|
23
|
+
completed.current = true;
|
|
24
|
+
callback.current(user);
|
|
25
|
+
}
|
|
26
|
+
} catch { setStatus("Connection interrupted. Check again when you're online."); }
|
|
27
|
+
}, []);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
void check();
|
|
30
|
+
const timer = setInterval(() => { void check(); }, 5000);
|
|
31
|
+
return () => clearInterval(timer);
|
|
32
|
+
}, [check]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!cooldown) return;
|
|
35
|
+
const timer = setTimeout(() => setCooldown(false), 60_000);
|
|
36
|
+
return () => clearTimeout(timer);
|
|
37
|
+
}, [cooldown]);
|
|
38
|
+
useShortcut((event) => {
|
|
39
|
+
if (event.name !== "enter" && event.name !== "return") return;
|
|
40
|
+
event.preventDefault(); event.stopPropagation(); void check();
|
|
41
|
+
}, { scope: "cloud-email-verification" });
|
|
42
|
+
return <Box flexDirection="column" gap={1}>
|
|
43
|
+
<Text fg={colors.textDim} wrapText>{status}</Text>
|
|
44
|
+
<Box flexDirection="row" gap={1}>
|
|
45
|
+
<Button label="I've confirmed" variant="primary" onPress={() => { void check(); }} />
|
|
46
|
+
<Button label={cooldown ? "Email sent" : "Resend email"} disabled={sending || cooldown} onPress={() => {
|
|
47
|
+
setSending(true);
|
|
48
|
+
void apiClient.sendVerification().then(() => {
|
|
49
|
+
setCooldown(true); setStatus("Confirmation link sent. Check your inbox and spam folder.");
|
|
50
|
+
}).catch((error) => setStatus(error instanceof Error ? error.message : "Couldn't send email. Try again."))
|
|
51
|
+
.finally(() => setSending(false));
|
|
52
|
+
}} />
|
|
53
|
+
</Box>
|
|
54
|
+
<Button label="Keep using the terminal" variant="ghost" onPress={onContinueFree} />
|
|
55
|
+
</Box>;
|
|
56
|
+
}
|
|
@@ -19,7 +19,7 @@ export interface CreditConditionsLoadResult {
|
|
|
19
19
|
|
|
20
20
|
const HISTORY_LIMIT = 45;
|
|
21
21
|
|
|
22
|
-
type CreditSeriesLoader = (
|
|
22
|
+
export type CreditSeriesLoader = (
|
|
23
23
|
seriesId: CreditSeriesId,
|
|
24
24
|
options: { limit: number; sortOrder: "desc" },
|
|
25
25
|
) => Promise<CloudFredSeriesPayload>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
3
|
+
import { createCreditConditionsHeadless } from "./headless";
|
|
4
|
+
|
|
5
|
+
const args: HeadlessPaneLoadArgs = { rawArgument: "", argument: null, symbols: [], options: {} };
|
|
6
|
+
|
|
7
|
+
describe("credit conditions headless model", () => {
|
|
8
|
+
test("returns normalized spread rows and partial errors", async () => {
|
|
9
|
+
const headless = createCreditConditionsHeadless({
|
|
10
|
+
load: async () => ({
|
|
11
|
+
rows: [{
|
|
12
|
+
seriesId: "BAMLC0A0CM",
|
|
13
|
+
label: "US IG",
|
|
14
|
+
title: "US Corporate Option-Adjusted Spread",
|
|
15
|
+
units: "Percent",
|
|
16
|
+
frequency: "Daily",
|
|
17
|
+
oasBp: 82.4,
|
|
18
|
+
dailyChangeBp: 1.2,
|
|
19
|
+
date: "2026-09-03",
|
|
20
|
+
stale: false,
|
|
21
|
+
}],
|
|
22
|
+
stale: false,
|
|
23
|
+
errors: ["one series unavailable"],
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
const result = await headless.load(args, {} as HeadlessPaneContext);
|
|
27
|
+
|
|
28
|
+
expect(result.rows).toEqual([expect.objectContaining({ label: "US IG", oasBp: 82.4 })]);
|
|
29
|
+
expect(result.errors).toEqual(["one series unavailable"]);
|
|
30
|
+
});
|
|
31
|
+
});
|