basalt-ui 2.4.0 → 2.6.0
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/CHANGELOG.md +29 -0
- package/MIGRATING.md +51 -12
- package/agent/rules/basalt-batteries.md +1 -1
- package/agent/rules/basalt-charts.md +1 -1
- package/agent/rules/basalt-mantine.md +1 -1
- package/agent/rules/basalt-state.md +2 -2
- package/agent/skills/basalt-app/SKILL.md +5 -5
- package/configs/oxlint-plugin.js +6 -3
- package/configs/oxlint-plugin.test.ts +8 -0
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +4 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/run-records.d.ts +12 -3
- package/dist/agent/run-records.d.ts.map +1 -1
- package/dist/agent/run-records.js +16 -3
- package/dist/agent/transport.d.ts +10 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +23 -7
- package/dist/charts/DualPanel.d.ts.map +1 -1
- package/dist/charts/DualPanel.js +14 -5
- package/dist/charts/MirroredBars.d.ts.map +1 -1
- package/dist/charts/MirroredBars.js +6 -4
- package/dist/charts/compile.d.ts +4 -0
- package/dist/charts/compile.d.ts.map +1 -1
- package/dist/charts/compile.js +40 -11
- package/dist/charts/panel.d.ts +6 -0
- package/dist/charts/panel.d.ts.map +1 -1
- package/dist/charts/panel.js +10 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +8 -2
- package/dist/state/fields.d.ts +52 -15
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +49 -3
- package/dist/state.d.ts +2 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +2 -0
- package/dist/telemetry.d.ts +58 -4
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +13 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/agent/ai-sdk-transport.ts +5 -2
- package/src/agent/index.ts +1 -1
- package/src/agent/run-records.ts +27 -3
- package/src/agent/transport.ts +11 -5
- package/src/agent/use-agent-thread-runs.ts +35 -8
- package/src/charts/DualPanel.tsx +13 -4
- package/src/charts/MirroredBars.tsx +5 -3
- package/src/charts/compile.ts +59 -12
- package/src/charts/panel.tsx +16 -1
- package/src/index.ts +1 -1
- package/src/provider/index.tsx +8 -2
- package/src/state/fields.ts +112 -21
- package/src/state.ts +2 -0
- package/src/telemetry.ts +70 -7
package/dist/telemetry.d.ts
CHANGED
|
@@ -57,15 +57,69 @@ export type BasaltEvent = Ok<'route.navigate', RouteAttrs> | Err<'route.error',
|
|
|
57
57
|
}> | Err<'window.error', {
|
|
58
58
|
'basalt.error.source': 'error' | 'unhandledrejection';
|
|
59
59
|
}> | Ok<'chat.turn', ChatAttrs> | Err<'chat.turn', ChatAttrs, Span>;
|
|
60
|
-
|
|
60
|
+
/** W3C trace-context request headers a sink contributes to one chat turn's request. */
|
|
61
|
+
export type TraceHeaders = {
|
|
62
|
+
readonly traceparent: string;
|
|
63
|
+
readonly tracestate?: string | undefined;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* How a traced chat turn ended: its `chat.turn` event fired (`settled`/`failed`), or `stopped` — a
|
|
67
|
+
* `stop()`, an unmount or a newer run taking the thread over — which emits no event. The one
|
|
68
|
+
* lifecycle vocabulary of the run records too.
|
|
69
|
+
*/
|
|
70
|
+
export type ChatTurnEnd = 'settled' | 'failed' | 'stopped';
|
|
71
|
+
/** The turn a sink is asked to trace. */
|
|
72
|
+
type ChatTurnInit = {
|
|
73
|
+
readonly threadId: string;
|
|
74
|
+
readonly messageId: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* What a sink returns from `startChatTurn`. `settle` runs once, synchronously, right after the
|
|
78
|
+
* turn's `chat.turn` event (`settled`/`failed`), or when the turn is cut short (`stopped`) — the
|
|
79
|
+
* point to end a span opened in `startChatTurn`. It runs even when `headers` is absent or
|
|
80
|
+
* malformed (dropped, the request goes out untraced), so the span never leaks.
|
|
81
|
+
*/
|
|
82
|
+
export type ChatTurnTrace = {
|
|
83
|
+
readonly headers?: TraceHeaders | undefined;
|
|
84
|
+
readonly settle?: ((end: ChatTurnEnd) => void) | undefined;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The event sink. It may carry one optional hook, `startChatTurn`, called when a new chat turn
|
|
88
|
+
* begins (not on a resume): open your span there and return its `traceparent` (and `tracestate`)
|
|
89
|
+
* and `aiSdkTransport` sends them with that turn's request to the chat endpoint — never anywhere
|
|
90
|
+
* else. A sink without the hook, or no sink, changes nothing on the wire. basalt has no OTel
|
|
91
|
+
* dependency; the hook is where the consumer's SDK does the work.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* export const telemetry: Telemetry = Object.assign(onEvent, {
|
|
95
|
+
* startChatTurn({ threadId }) {
|
|
96
|
+
* const span = tracer.startSpan('chat.turn', {
|
|
97
|
+
* attributes: { 'basalt.chat.thread_id': threadId },
|
|
98
|
+
* })
|
|
99
|
+
* const carrier: Record<string, string> = {}
|
|
100
|
+
* propagation.inject(trace.setSpan(context.active(), span), carrier)
|
|
101
|
+
* return {
|
|
102
|
+
* headers: { traceparent: carrier.traceparent ?? '', tracestate: carrier.tracestate },
|
|
103
|
+
* settle(end) {
|
|
104
|
+
* if (end === 'failed') span.setStatus({ code: SpanStatusCode.ERROR })
|
|
105
|
+
* span.end()
|
|
106
|
+
* },
|
|
107
|
+
* }
|
|
108
|
+
* },
|
|
109
|
+
* })
|
|
110
|
+
*/
|
|
111
|
+
export type Telemetry = ((event: BasaltEvent) => void) & {
|
|
112
|
+
readonly startChatTurn?: ((turn: ChatTurnInit) => ChatTurnTrace | undefined) | undefined;
|
|
113
|
+
};
|
|
61
114
|
export declare const now: () => number;
|
|
62
115
|
/** `error.name`, `'Error'` for a non-`Error` throw. */
|
|
63
116
|
export declare const errorName: (error: unknown) => string;
|
|
64
117
|
/**
|
|
65
|
-
*
|
|
66
|
-
* events.
|
|
118
|
+
* The wrapped emitter over a sink read at call time (a ref), so a swapped sink is never stale. It is
|
|
119
|
+
* called synchronously and a throwing sink never breaks a render; no sink, no events. Its
|
|
120
|
+
* `startChatTurn` forwards the sink's hook; the hook's consumer validates the headers.
|
|
67
121
|
*/
|
|
68
|
-
export declare function
|
|
122
|
+
export declare function createLiveEmitter(read: () => Telemetry | undefined): Telemetry;
|
|
69
123
|
/**
|
|
70
124
|
* The provider's wrapped emitter, for hooks that emit their own events (`useAgentThreadRuns`'
|
|
71
125
|
* `chat.turn`). `undefined` outside a `BasaltProvider`, where such a hook emits nothing.
|
package/dist/telemetry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,+DAA+D;AAC/D,KAAK,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAC3B,qCAAqC;AACrC,KAAK,IAAI,GAAG,EAAE,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAErC,+DAA+D;AAC/D,KAAK,SAAS,GACV,OAAO,GACP,OAAO,GACP,UAAU,GACV,QAAQ,GACR,QAAQ,GACR,oBAAoB,GACpB,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,SAAS,CAAA;AAEb,KAAK,OAAO,GAAG;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,SAAS,CAAA;CAAE,CAAA;AAC3E,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,CAAC,CAAA;CAAE,CAAA;AAChE,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,CAAC,GAAG,OAAO,CAAA;CAAE,CAAA;AAE/E,KAAK,UAAU,GAAG;IAChB,iBAAiB,EAAE,MAAM,CAAA;IACzB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,KAAK,SAAS,GAAG;IACf,uBAAuB,EAAE,MAAM,CAAA;IAC/B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,oBAAoB,EAAE,YAAY,CAAA;CACnC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GACnB,EAAE,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAChC,GAAG,CAAC,aAAa,EAAE;IAAE,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,GACjD,GAAG,CAAC,aAAa,EAAE;IAAE,mBAAmB,EAAE,MAAM,CAAC;IAAC,4BAA4B,EAAE,MAAM,CAAA;CAAE,CAAC,GACzF,GAAG,CAAC,gBAAgB,EAAE;IAAE,qBAAqB,EAAE,MAAM,CAAA;CAAE,CAAC,GACxD,GAAG,CAAC,cAAc,EAAE;IAAE,uBAAuB,EAAE,MAAM,CAAA;CAAE,CAAC,GACxD,GAAG,CAAC,cAAc,EAAE;IAAE,qBAAqB,EAAE,OAAO,GAAG,oBAAoB,CAAA;CAAE,CAAC,GAC9E,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,GAC1B,GAAG,CAAC,WAAW,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;AAErC,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
package/dist/telemetry.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
2
|
const now = () => performance.timeOrigin + performance.now();
|
|
3
3
|
const errorName = (error) => error instanceof Error ? error.name : "Error";
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
return (event) => {
|
|
4
|
+
function createLiveEmitter(read) {
|
|
5
|
+
const emit = (event) => {
|
|
7
6
|
try {
|
|
8
|
-
|
|
7
|
+
read()?.(event);
|
|
9
8
|
} catch {
|
|
10
9
|
}
|
|
11
10
|
};
|
|
11
|
+
return Object.assign(emit, {
|
|
12
|
+
startChatTurn(turn) {
|
|
13
|
+
try {
|
|
14
|
+
return read()?.startChatTurn?.(turn);
|
|
15
|
+
} catch {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
12
20
|
}
|
|
13
21
|
function noop() {
|
|
14
22
|
}
|
|
@@ -26,7 +34,7 @@ function routerTelemetry(router) {
|
|
|
26
34
|
export {
|
|
27
35
|
TelemetryContext,
|
|
28
36
|
bindRouterTelemetry,
|
|
29
|
-
|
|
37
|
+
createLiveEmitter,
|
|
30
38
|
errorName,
|
|
31
39
|
now,
|
|
32
40
|
routerTelemetry
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v2.
|
|
1
|
+
# basalt-ui v2.6.0
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, TanStack Charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# Entry points: use the exact import specifier below; do not guess.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# A subpath's `Install:` line is its required peers; `OptionalPeers:` are import()-only, install on use.
|
|
14
14
|
|
|
15
15
|
Package: basalt-ui
|
|
16
|
-
Version: 2.
|
|
16
|
+
Version: 2.6.0
|
|
17
17
|
|
|
18
18
|
## basalt-ui
|
|
19
19
|
Import: import { ... } from 'basalt-ui'
|
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ import { assertNever } from '../register'
|
|
|
49
49
|
import { mintMessageId, mintThreadId } from './id'
|
|
50
50
|
import { TERMINAL_TOOL_STATES } from './parts'
|
|
51
51
|
import type { AgentPart, AgentPartDraft } from './parts'
|
|
52
|
-
import type { ResumableAgentTransport } from './transport'
|
|
52
|
+
import type { AgentStreamContext, ResumableAgentTransport } from './transport'
|
|
53
53
|
|
|
54
54
|
// ── ai (optional peer) — TYPE-ONLY imports, erased at runtime ────────────────
|
|
55
55
|
// verbatimModuleSyntax-safe: these never trigger a runtime resolution of 'ai'. The actual runtime
|
|
@@ -415,7 +415,7 @@ export function aiSdkTransport<TPart = AgentPart>(
|
|
|
415
415
|
async *stream(
|
|
416
416
|
input: string,
|
|
417
417
|
signal?: AbortSignal,
|
|
418
|
-
ctx?:
|
|
418
|
+
ctx?: AgentStreamContext,
|
|
419
419
|
): AsyncGenerator<TPart> {
|
|
420
420
|
// Synthesized locally, first — we already know the chat id client-side, so there is no
|
|
421
421
|
// need to wait on the server to hand us a run id (unlike a river-style protocol).
|
|
@@ -439,6 +439,9 @@ export function aiSdkTransport<TPart = AgentPart>(
|
|
|
439
439
|
trigger: 'submit-message',
|
|
440
440
|
messageId: undefined,
|
|
441
441
|
abortSignal: signal,
|
|
442
|
+
// Per-turn trace context goes to the chat endpoint only (merged over the static headers by
|
|
443
|
+
// AI SDK's HTTP transport); absent, the request is exactly what it was.
|
|
444
|
+
...(ctx?.headers !== undefined ? { headers: ctx.headers } : {}),
|
|
442
445
|
})
|
|
443
446
|
yield* diffChunkStream<TPart>(chatId, chunkStream, readStream, signal)
|
|
444
447
|
},
|
package/src/agent/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export { toAgentError } from './error'
|
|
|
24
24
|
export type { ChatMessage } from './history'
|
|
25
25
|
export type { AgentThread, ThreadStatus, ThreadsStore } from './thread'
|
|
26
26
|
export type { AgentOutcome, OutcomeResolver } from './outcome'
|
|
27
|
-
export type { AgentTransport, ResumableAgentTransport } from './transport'
|
|
27
|
+
export type { AgentStreamContext, AgentTransport, ResumableAgentTransport } from './transport'
|
|
28
28
|
|
|
29
29
|
// ── Chat UI ───────────────────────────────────────────────────────────────────
|
|
30
30
|
export { ThreadFeed } from './chat/thread-feed'
|
package/src/agent/run-records.ts
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* covers: it outlives the run only until a render shows the outcome (`sweepFlushed`).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { ChatTurnEnd, ChatTurnTrace } from '../telemetry'
|
|
16
|
+
|
|
15
17
|
/** The part of a rendered `runs` entry this module reads. */
|
|
16
18
|
type RenderedRun<TPart> = { readonly status: string; readonly parts: TPart[] }
|
|
17
19
|
|
|
18
|
-
export type RunEnd =
|
|
20
|
+
export type RunEnd = ChatTurnEnd
|
|
19
21
|
|
|
20
22
|
export type RunRecord<TPart> = {
|
|
21
23
|
readonly controller: AbortController
|
|
@@ -28,15 +30,22 @@ export type RunRecord<TPart> = {
|
|
|
28
30
|
ended: RunEnd | undefined
|
|
29
31
|
/** A newer run began on the same thread (set by `beginRun` on the record it replaces). */
|
|
30
32
|
superseded: boolean
|
|
33
|
+
/** The turn's telemetry `settle`, run once by `settleTrace`. */
|
|
34
|
+
settle: ChatTurnTrace['settle']
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export type RunRecords<TPart> = Map<string, RunRecord<TPart>>
|
|
34
38
|
|
|
35
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Registers a live run for `threadId`, replacing whatever record the previous run left. A previous
|
|
41
|
+
* run still being driven is cut short: its trace settles as `stopped`. `settle` is the new turn's
|
|
42
|
+
* telemetry hook (absent for a resume).
|
|
43
|
+
*/
|
|
36
44
|
export function beginRun<TPart>(
|
|
37
45
|
records: RunRecords<TPart>,
|
|
38
46
|
threadId: string,
|
|
39
47
|
controller: AbortController,
|
|
48
|
+
settle?: ChatTurnTrace['settle'],
|
|
40
49
|
): RunRecord<TPart> {
|
|
41
50
|
const record: RunRecord<TPart> = {
|
|
42
51
|
controller,
|
|
@@ -45,13 +54,28 @@ export function beginRun<TPart>(
|
|
|
45
54
|
resolving: false,
|
|
46
55
|
ended: undefined,
|
|
47
56
|
superseded: false,
|
|
57
|
+
settle,
|
|
48
58
|
}
|
|
49
59
|
const previous = records.get(threadId)
|
|
50
|
-
if (previous !== undefined)
|
|
60
|
+
if (previous !== undefined) {
|
|
61
|
+
previous.superseded = true
|
|
62
|
+
settleTrace(previous, 'stopped')
|
|
63
|
+
}
|
|
51
64
|
records.set(threadId, record)
|
|
52
65
|
return record
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
/** Runs the turn's telemetry `settle` once; a throwing consumer hook never breaks the run. */
|
|
69
|
+
export function settleTrace<TPart>(record: RunRecord<TPart>, end: RunEnd): void {
|
|
70
|
+
const { settle } = record
|
|
71
|
+
record.settle = undefined
|
|
72
|
+
try {
|
|
73
|
+
settle?.(end)
|
|
74
|
+
} catch {
|
|
75
|
+
// a throwing sink must never break the app (§ 7 delivery rules)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
/** Marks a run as no longer driven and drops the content it held. */
|
|
56
80
|
export function endRun<TPart>(record: RunRecord<TPart>, reason: RunEnd): void {
|
|
57
81
|
record.ended = reason
|
package/src/agent/transport.ts
CHANGED
|
@@ -16,6 +16,16 @@ import type { AgentPartDraft } from './parts'
|
|
|
16
16
|
|
|
17
17
|
// ── AgentTransport ────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The third `stream` argument. `headers` are extra request headers for THIS turn's request to the
|
|
21
|
+
* transport's own endpoint — today the W3C trace context a telemetry sink's `startChatTurn`
|
|
22
|
+
* returned. Absent unless a sink contributed; a transport that ignores them keeps working.
|
|
23
|
+
*/
|
|
24
|
+
export type AgentStreamContext = {
|
|
25
|
+
readonly messageId: string
|
|
26
|
+
readonly headers?: Readonly<Record<string, string>> | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
/**
|
|
20
30
|
* The injected transport seam. `stream(input, signal, ctx)` returns an async generator of parts.
|
|
21
31
|
* Implement this interface for any backend — fetch-based SSE, AI SDK, or a mock.
|
|
@@ -43,11 +53,7 @@ import type { AgentPartDraft } from './parts'
|
|
|
43
53
|
* }
|
|
44
54
|
*/
|
|
45
55
|
export type AgentTransport<TPart = AgentPartDraft, TInput = string> = {
|
|
46
|
-
stream: (
|
|
47
|
-
input: TInput,
|
|
48
|
-
signal?: AbortSignal,
|
|
49
|
-
ctx?: { readonly messageId: string },
|
|
50
|
-
) => AsyncGenerator<TPart>
|
|
56
|
+
stream: (input: TInput, signal?: AbortSignal, ctx?: AgentStreamContext) => AsyncGenerator<TPart>
|
|
51
57
|
/** Optional: resume a previously-started run from its resumeToken (see StartPart). Transports that don't support resumption simply omit this. */
|
|
52
58
|
resume?: ((resumeToken: string, signal?: AbortSignal) => AsyncGenerator<TPart>) | undefined
|
|
53
59
|
}
|
|
@@ -50,6 +50,7 @@ import { createRunCommitter, isContentPart } from './run-committer'
|
|
|
50
50
|
import {
|
|
51
51
|
beginRun,
|
|
52
52
|
endRun,
|
|
53
|
+
settleTrace,
|
|
53
54
|
failedThreadIds,
|
|
54
55
|
dismissRun,
|
|
55
56
|
hasLiveRun,
|
|
@@ -62,8 +63,8 @@ import type { RunRecord, RunRecords } from './run-records'
|
|
|
62
63
|
import type { AgentThread, LiveRunStatus, ThreadStatus, ThreadsStore } from './thread'
|
|
63
64
|
import { isResumable } from './transport'
|
|
64
65
|
import type { AgentTransport } from './transport'
|
|
65
|
-
import { TelemetryContext,
|
|
66
|
-
import type { BasaltEvent, Telemetry } from '../telemetry'
|
|
66
|
+
import { TelemetryContext, createLiveEmitter, errorName, now } from '../telemetry'
|
|
67
|
+
import type { BasaltEvent, Telemetry, TraceHeaders } from '../telemetry'
|
|
67
68
|
|
|
68
69
|
// ── ThreadRunState ────────────────────────────────────────────────────────────
|
|
69
70
|
|
|
@@ -388,6 +389,7 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
|
|
|
388
389
|
status: outcome.status,
|
|
389
390
|
}),
|
|
390
391
|
)
|
|
392
|
+
settleTrace(record, 'settled')
|
|
391
393
|
} catch (err) {
|
|
392
394
|
// Guard: the run ended (stop(), unmount — which abort the controller, so their AbortError lands
|
|
393
395
|
// here) or a newer stream took the thread over. Only an abort from somewhere else gets past.
|
|
@@ -421,6 +423,7 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
|
|
|
421
423
|
failure: { error: err, agentError },
|
|
422
424
|
}),
|
|
423
425
|
)
|
|
426
|
+
settleTrace(record, 'failed')
|
|
424
427
|
onError?.({ threadId, error: err })
|
|
425
428
|
} finally {
|
|
426
429
|
committer.cancel()
|
|
@@ -555,6 +558,24 @@ async function finalizeStop<TPart extends PartLike>(args: {
|
|
|
555
558
|
}
|
|
556
559
|
}
|
|
557
560
|
|
|
561
|
+
const TRACEPARENT = /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/
|
|
562
|
+
const TRACESTATE_MEMBER = /^[\da-z][\w*/@-]{0,255}=[^\s,=]([^,=]{0,254}[^\s,=])?$/
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The wire form of a turn's trace context: a well-formed `traceparent` (else none — the request
|
|
566
|
+
* goes out untraced, the sink's `settle` still runs) and a `tracestate` only if valid.
|
|
567
|
+
*/
|
|
568
|
+
function traceRequestHeaders(headers: TraceHeaders): Record<string, string> | undefined {
|
|
569
|
+
const { traceparent, tracestate } = headers
|
|
570
|
+
if (typeof traceparent !== 'string' || !TRACEPARENT.test(traceparent)) return undefined
|
|
571
|
+
const members = typeof tracestate === 'string' ? tracestate.split(',') : []
|
|
572
|
+
const valid =
|
|
573
|
+
members.length > 0 &&
|
|
574
|
+
members.length <= 32 &&
|
|
575
|
+
members.every((member) => TRACESTATE_MEMBER.test(member.trim()))
|
|
576
|
+
return valid ? { traceparent, tracestate: tracestate as string } : { traceparent }
|
|
577
|
+
}
|
|
578
|
+
|
|
558
579
|
/** The `chat.turn` event for a turn that completed (`failure` absent) or failed. */
|
|
559
580
|
function buildTurnEvent(args: {
|
|
560
581
|
threadId: string
|
|
@@ -654,10 +675,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
654
675
|
toUserPartsRef.current = resolvedToUserParts
|
|
655
676
|
|
|
656
677
|
// Stable over the sink ref, so an inline `telemetry` function never changes what `start` closes over.
|
|
657
|
-
const emitTelemetry = useMemo<Telemetry>(
|
|
658
|
-
() => createEmitter((event) => telemetryRef.current?.(event)),
|
|
659
|
-
[],
|
|
660
|
-
)
|
|
678
|
+
const emitTelemetry = useMemo<Telemetry>(() => createLiveEmitter(() => telemetryRef.current), [])
|
|
661
679
|
|
|
662
680
|
// Resolves the transport for `threadId`: the shared object as-is for the plain-object form
|
|
663
681
|
// (unchanged behavior), or the factory's result — resolved once per thread id and cached in
|
|
@@ -707,6 +725,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
707
725
|
if (record.ended !== undefined) continue
|
|
708
726
|
abortedThreadIds.push(threadId)
|
|
709
727
|
record.controller.abort()
|
|
728
|
+
settleTrace(record, 'stopped')
|
|
710
729
|
}
|
|
711
730
|
recordsRef.current.clear()
|
|
712
731
|
if (abortedThreadIds.length === 0) return
|
|
@@ -841,8 +860,12 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
841
860
|
})
|
|
842
861
|
|
|
843
862
|
const controller = new AbortController()
|
|
844
|
-
|
|
863
|
+
// Opened before the request so its context rides on it; closed by `settleTrace` (a resume is
|
|
864
|
+
// the same turn's reconnect, not a new one, and carries none).
|
|
865
|
+
const trace = emitTelemetry.startChatTurn?.({ threadId, messageId: userMessage.id })
|
|
866
|
+
const record = beginRun(recordsRef.current, threadId, controller, trace?.settle)
|
|
845
867
|
const resolvedTransport = resolveTransport(threadId)
|
|
868
|
+
const headers = trace?.headers === undefined ? undefined : traceRequestHeaders(trace.headers)
|
|
846
869
|
|
|
847
870
|
// Non-awaited: this consumer runs in the background so start() returns immediately and
|
|
848
871
|
// multiple threads can stream at once.
|
|
@@ -855,7 +878,10 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
855
878
|
// id instead of a second, unrelated one (R1 — see AgentTransport.stream's doc).
|
|
856
879
|
generator: withPartIds(
|
|
857
880
|
threadId,
|
|
858
|
-
resolvedTransport.stream(input, controller.signal, {
|
|
881
|
+
resolvedTransport.stream(input, controller.signal, {
|
|
882
|
+
messageId: userMessage.id,
|
|
883
|
+
...(headers !== undefined ? { headers } : {}),
|
|
884
|
+
}),
|
|
859
885
|
),
|
|
860
886
|
userMessage,
|
|
861
887
|
recordsRef,
|
|
@@ -913,6 +939,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
913
939
|
const { appended, resolving } = record
|
|
914
940
|
endRun(record, 'stopped')
|
|
915
941
|
record.controller.abort()
|
|
942
|
+
settleTrace(record, 'stopped')
|
|
916
943
|
void finalizeStop({
|
|
917
944
|
threadId,
|
|
918
945
|
parts,
|
package/src/charts/DualPanel.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import { VX, alpha } from '../tokens'
|
|
|
16
16
|
import { resolveContainerClass } from '../tokens/size-classes'
|
|
17
17
|
import { defaultFormatX, defaultNumberFormat, engineRow, seriesRow, visibleSeries } from './compile'
|
|
18
18
|
import { barMark, emphasis, lineMark, markersOf, thinFill } from './marks'
|
|
19
|
-
import { PanelRung, paneX, paneY, rowsOf } from './panel'
|
|
19
|
+
import { PanelRung, chartHeightOf, paneX, paneY, rowsOf } from './panel'
|
|
20
20
|
import type { PaneBuilder, PanelInput, PanelPlan } from './panel'
|
|
21
21
|
import { xScaleFor } from './scales'
|
|
22
22
|
import { rowX } from './types'
|
|
@@ -269,13 +269,21 @@ export function DualPanel<T, X extends Date | number = Date | number>(
|
|
|
269
269
|
],
|
|
270
270
|
scales: {
|
|
271
271
|
x: { scale: xScale(), axis: false },
|
|
272
|
-
y: paneY({
|
|
272
|
+
y: paneY({
|
|
273
|
+
scale: topY,
|
|
274
|
+
domain: topDomain,
|
|
275
|
+
format: topFormat,
|
|
276
|
+
ticks: TOP_TICKS,
|
|
277
|
+
cls,
|
|
278
|
+
height: chartHeightOf(h, topFraction, PANE_GAP),
|
|
279
|
+
}),
|
|
273
280
|
},
|
|
274
281
|
}
|
|
275
282
|
}
|
|
276
283
|
|
|
277
|
-
const barPane: PaneBuilder = ({ width }) => {
|
|
284
|
+
const barPane: PaneBuilder = ({ width, height: h }) => {
|
|
278
285
|
const cls = resolveContainerClass(width)
|
|
286
|
+
const whole = chartHeightOf(h, 1 - topFraction, PANE_GAP)
|
|
279
287
|
const barRows = barOn ? rowsOf(data, x, barSeries) : []
|
|
280
288
|
const dim = emphasis(highlighted, { key: BAR_KEY })
|
|
281
289
|
return {
|
|
@@ -290,13 +298,14 @@ export function DualPanel<T, X extends Date | number = Date | number>(
|
|
|
290
298
|
}),
|
|
291
299
|
],
|
|
292
300
|
scales: {
|
|
293
|
-
x: paneX({ scale: xScale(), model, format, cls }),
|
|
301
|
+
x: paneX({ scale: xScale(), model, format, cls, height: whole }),
|
|
294
302
|
y: paneY({
|
|
295
303
|
scale: barScale,
|
|
296
304
|
domain: barDomain,
|
|
297
305
|
format: bar.format,
|
|
298
306
|
ticks: BAR_TICKS,
|
|
299
307
|
cls,
|
|
308
|
+
height: whole,
|
|
300
309
|
}),
|
|
301
310
|
},
|
|
302
311
|
}
|
|
@@ -23,7 +23,7 @@ import { VX, alpha } from '../tokens'
|
|
|
23
23
|
import { resolveContainerClass } from '../tokens/size-classes'
|
|
24
24
|
import { defaultFormatX, defaultNumberFormat, seriesRow, visibleSeries } from './compile'
|
|
25
25
|
import { barMark, emphasis } from './marks'
|
|
26
|
-
import { PanelRung, paneX, paneY, rowsOf } from './panel'
|
|
26
|
+
import { PanelRung, chartHeightOf, paneX, paneY, rowsOf } from './panel'
|
|
27
27
|
import type { PaneBuilder, PanelInput, PanelPlan } from './panel'
|
|
28
28
|
import { xScaleFor } from './scales'
|
|
29
29
|
import { rowX } from './types'
|
|
@@ -174,8 +174,9 @@ export function MirroredBars<T, X extends ChartX = ChartX>(
|
|
|
174
174
|
on: boolean,
|
|
175
175
|
max: number,
|
|
176
176
|
): PaneBuilder =>
|
|
177
|
-
({ width: w }) => {
|
|
177
|
+
({ width: w, height: h }) => {
|
|
178
178
|
const cls = resolveContainerClass(w)
|
|
179
|
+
const whole = chartHeightOf(h, side === 'up' ? upFraction : 1 - upFraction, 0)
|
|
179
180
|
// The down pane's zero is at its top.
|
|
180
181
|
const domain: [number, number] = side === 'up' ? [0, max] : [max, 0]
|
|
181
182
|
const rows: PlotRow<T, X>[] = on ? rowsOf(drawn, x, s) : []
|
|
@@ -249,7 +250,7 @@ export function MirroredBars<T, X extends ChartX = ChartX>(
|
|
|
249
250
|
scales: {
|
|
250
251
|
x:
|
|
251
252
|
side === 'down'
|
|
252
|
-
? paneX({ scale: bandScale(), model, format, cls })
|
|
253
|
+
? paneX({ scale: bandScale(), model, format, cls, height: whole })
|
|
253
254
|
: { scale: bandScale(), axis: false },
|
|
254
255
|
y: paneY({
|
|
255
256
|
scale: scaleLinear().domain(domain),
|
|
@@ -257,6 +258,7 @@ export function MirroredBars<T, X extends ChartX = ChartX>(
|
|
|
257
258
|
format: cfg.format,
|
|
258
259
|
ticks: cfg.ticks ?? DEFAULT_PANE_TICKS,
|
|
259
260
|
cls,
|
|
261
|
+
height: whole,
|
|
260
262
|
}),
|
|
261
263
|
zx: { channel: 'x', scale: scaleLinear().domain([0, 1]), axis: false },
|
|
262
264
|
},
|
package/src/charts/compile.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
ResponsiveChartDefinition,
|
|
24
24
|
StoredChartSpec,
|
|
25
25
|
} from '@tanstack/charts/types/core'
|
|
26
|
+
import { SHORT_CHART_HEIGHT } from './box'
|
|
26
27
|
import { fmtCompact } from '../format/number'
|
|
27
28
|
import { VX } from '../tokens'
|
|
28
29
|
import type { ContainerClass } from '../tokens/size-classes'
|
|
@@ -97,6 +98,14 @@ const LADDER = {
|
|
|
97
98
|
const INSIDE_LABEL_DX = 4
|
|
98
99
|
const INSIDE_LABEL_DY = -6
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Y labels go inside the plot only for the compact class under `SHORT_CHART_HEIGHT` (the box's own
|
|
103
|
+
* short signal); otherwise they sit in a left gutter. `height` is the whole chart's, `0` before it
|
|
104
|
+
* is measured, which keeps the gutter rather than flipping a first paint inside.
|
|
105
|
+
*/
|
|
106
|
+
const labelsInside = (cls: ContainerClass, height: number): boolean =>
|
|
107
|
+
cls === 'compact' && height > 0 && height < SHORT_CHART_HEIGHT
|
|
108
|
+
|
|
100
109
|
export function defaultNumberFormat(v: number): string {
|
|
101
110
|
return fmtCompact(v, { digits: Math.abs(v) >= 1000 ? 1 : 2 })
|
|
102
111
|
}
|
|
@@ -180,6 +189,22 @@ const REF_KEY_PREFIX = '__ref-'
|
|
|
180
189
|
/** The legend key of a reference line that names itself (`RefLine.legend`). */
|
|
181
190
|
const refLegendKey = (index: number): string => `${REF_KEY_PREFIX}${index}`
|
|
182
191
|
|
|
192
|
+
/**
|
|
193
|
+
* A reference line is drawn translucent (`*-ref`, 20-30% alpha) so it recedes behind the data, but
|
|
194
|
+
* a 16x8 swatch at that alpha vanishes on the dark surface. The legend takes the status solid,
|
|
195
|
+
* which the color gate holds to 3:1 on the panel in both schemes, like a series swatch.
|
|
196
|
+
*/
|
|
197
|
+
const SWATCH_SOLID: ReadonlyMap<string, string> = new Map([
|
|
198
|
+
[VX.goodRef, VX.goodSolid],
|
|
199
|
+
[VX.badRef, VX.badSolid],
|
|
200
|
+
[VX.warnRef, VX.warnSolid],
|
|
201
|
+
// The tinted status fills are as translucent, and a consumer may pass either as a line color.
|
|
202
|
+
[VX.good, VX.goodSolid],
|
|
203
|
+
[VX.goodSoft, VX.goodSolid],
|
|
204
|
+
[VX.bad, VX.badSolid],
|
|
205
|
+
[VX.warn, VX.warnSolid],
|
|
206
|
+
])
|
|
207
|
+
|
|
183
208
|
/** Legend-only entries for the reference lines that name themselves, in line order. */
|
|
184
209
|
export function refLegendSeries<T>(refLines: readonly RefLine[] | undefined): ChartSeries<T>[] {
|
|
185
210
|
return (refLines ?? []).flatMap((r, i) =>
|
|
@@ -189,7 +214,7 @@ export function refLegendSeries<T>(refLines: readonly RefLine[] | undefined): Ch
|
|
|
189
214
|
{
|
|
190
215
|
key: refLegendKey(i),
|
|
191
216
|
label: r.legend.label,
|
|
192
|
-
color: r.color,
|
|
217
|
+
color: SWATCH_SOLID.get(r.color) ?? r.color,
|
|
193
218
|
role: 'reference' as const,
|
|
194
219
|
tooltip: false,
|
|
195
220
|
getValue: () => null,
|
|
@@ -218,8 +243,10 @@ export function yGuide<T>(opts: {
|
|
|
218
243
|
cls: ContainerClass
|
|
219
244
|
side: 'left' | 'right'
|
|
220
245
|
grid: boolean
|
|
246
|
+
/** The whole chart's height: decides inside or gutter labels. */
|
|
247
|
+
height: number
|
|
221
248
|
}): Omit<ChartPositionScaleOptions<number>, 'scale'> {
|
|
222
|
-
const { cfg, axis, cls, side, grid } = opts
|
|
249
|
+
const { cfg, axis, cls, side, grid, height } = opts
|
|
223
250
|
const ladder = LADDER[cls]
|
|
224
251
|
const ticks =
|
|
225
252
|
typeof cfg?.ticks === 'object'
|
|
@@ -227,8 +254,8 @@ export function yGuide<T>(opts: {
|
|
|
227
254
|
: { count: typeof cfg?.ticks === 'number' ? cfg.ticks : ladder.yTicks }
|
|
228
255
|
const gridLine = grid ? { grid: GRID_LINE } : {}
|
|
229
256
|
if (cls === 'micro') return { ...gridLine, axis: false }
|
|
230
|
-
//
|
|
231
|
-
const inside = cls
|
|
257
|
+
// Labels sit INSIDE the plot, above their grid line, so the plot keeps the full width.
|
|
258
|
+
const inside = labelsInside(cls, height)
|
|
232
259
|
return {
|
|
233
260
|
...gridLine,
|
|
234
261
|
...(side === 'right' && { side: 'right' as const }),
|
|
@@ -276,9 +303,12 @@ export function xGuide<X extends ChartX>(opts: {
|
|
|
276
303
|
xTicks: number | readonly X[] | undefined
|
|
277
304
|
format: (v: ChartValue) => string
|
|
278
305
|
cls: ContainerClass
|
|
306
|
+
/** The whole chart's height: decides whether the left margin is locked to zero. */
|
|
307
|
+
height: number
|
|
279
308
|
}): Omit<ChartPositionScaleOptions<ChartValue>, 'scale'> {
|
|
280
|
-
const { model, xTicks, format, cls } = opts
|
|
309
|
+
const { model, xTicks, format, cls, height } = opts
|
|
281
310
|
const ladder = LADDER[cls]
|
|
311
|
+
const inside = labelsInside(cls, height)
|
|
282
312
|
const ticks = xTickPolicy(model, xTicks ?? ladder.xTicks)
|
|
283
313
|
return {
|
|
284
314
|
axis: {
|
|
@@ -288,8 +318,8 @@ export function xGuide<X extends ChartX>(opts: {
|
|
|
288
318
|
fontSize: ladder.font,
|
|
289
319
|
opacity: 1,
|
|
290
320
|
thin: true,
|
|
291
|
-
// A locked-zero left margin (
|
|
292
|
-
...(
|
|
321
|
+
// A locked-zero left margin (inside labels) must not clip the first label.
|
|
322
|
+
...(inside && {
|
|
293
323
|
anchor: ({ index }: { index: number }) => (index === 0 ? ('start' as const) : undefined),
|
|
294
324
|
}),
|
|
295
325
|
},
|
|
@@ -634,18 +664,26 @@ function buildScales<T, X extends ChartX>(opts: {
|
|
|
634
664
|
axes: Axes<T>
|
|
635
665
|
formatX: (v: ChartValue) => string
|
|
636
666
|
cls: ContainerClass
|
|
667
|
+
height: number
|
|
637
668
|
zoneScale: boolean
|
|
638
669
|
}): StoredChartSpec['scales'] {
|
|
639
|
-
const { input, model, axes, formatX, cls, zoneScale } = opts
|
|
670
|
+
const { input, model, axes, formatX, cls, height, zoneScale } = opts
|
|
640
671
|
const { left, right } = axes
|
|
641
672
|
return {
|
|
642
673
|
x: {
|
|
643
674
|
scale: model.scale,
|
|
644
|
-
...xGuide({ model, xTicks: input.xTicks, format: formatX, cls }),
|
|
675
|
+
...xGuide({ model, xTicks: input.xTicks, format: formatX, cls, height }),
|
|
645
676
|
},
|
|
646
677
|
y: {
|
|
647
678
|
scale: yScaleFor(input.y, left.domain),
|
|
648
|
-
...yGuide({
|
|
679
|
+
...yGuide({
|
|
680
|
+
cfg: input.y,
|
|
681
|
+
axis: left,
|
|
682
|
+
cls,
|
|
683
|
+
height,
|
|
684
|
+
side: 'left',
|
|
685
|
+
grid: input.y?.grid ?? true,
|
|
686
|
+
}),
|
|
649
687
|
},
|
|
650
688
|
...(right && {
|
|
651
689
|
y2: {
|
|
@@ -655,6 +693,7 @@ function buildScales<T, X extends ChartX>(opts: {
|
|
|
655
693
|
cfg: input.y2,
|
|
656
694
|
axis: right,
|
|
657
695
|
cls,
|
|
696
|
+
height,
|
|
658
697
|
side: 'right',
|
|
659
698
|
grid: input.y2?.grid ?? false,
|
|
660
699
|
}),
|
|
@@ -816,11 +855,19 @@ export function compileCartesian<T, X extends ChartX>(
|
|
|
816
855
|
})
|
|
817
856
|
return {
|
|
818
857
|
marks: [...under, ...kindMarks, ...dots, ...over, crosshair({ x: CROSSHAIR, y: false })],
|
|
819
|
-
scales: buildScales({
|
|
858
|
+
scales: buildScales({
|
|
859
|
+
input,
|
|
860
|
+
model,
|
|
861
|
+
axes,
|
|
862
|
+
formatX,
|
|
863
|
+
cls,
|
|
864
|
+
height,
|
|
865
|
+
zoneScale: under.length > 0,
|
|
866
|
+
}),
|
|
820
867
|
theme: CHART_THEME,
|
|
821
868
|
...(gradients.length > 0 && { gradients }),
|
|
822
869
|
clip: true,
|
|
823
|
-
...(cls
|
|
870
|
+
...(labelsInside(cls, height) && { margin: { left: 0, ...(axes.right && { right: 0 }) } }),
|
|
824
871
|
}
|
|
825
872
|
},
|
|
826
873
|
// Followers paint the crosshair only, unless the chart opts into following tooltips.
|