@dbx-tools/ui-mastra 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ /** One chunk from a Mastra agent SSE stream (`data: { type, payload, ... }`). */
2
+ export interface MastraStreamChunk {
3
+ type: string;
4
+ payload?: unknown;
5
+ runId?: string;
6
+ }
7
+
8
+ /** Response from agent streaming endpoints with {@link processMastraStream}. */
9
+ export type MastraStreamResponse = Response & {
10
+ processDataStream: (options: {
11
+ onChunk: (chunk: MastraStreamChunk) => void | Promise<void>;
12
+ }) => Promise<void>;
13
+ };
14
+
15
+ /**
16
+ * Parse Mastra agent SSE (`data: …` lines) and invoke `onChunk` per event.
17
+ * Mirrors `@mastra/client-js`'s internal reader without the
18
+ * `processChatResponse_vNext` side channel that throws when a resumed
19
+ * `approve-tool-call` stream emits `tool-result` without a new `tool-call`.
20
+ */
21
+ export async function processMastraStream(options: {
22
+ stream: ReadableStream<Uint8Array>;
23
+ onChunk: (chunk: MastraStreamChunk) => void | Promise<void>;
24
+ signal?: AbortSignal;
25
+ }): Promise<void> {
26
+ const reader = options.stream.getReader();
27
+ const decoder = new TextDecoder();
28
+ let buffer = "";
29
+ const abort = () => void reader.cancel();
30
+ if (options.signal?.aborted) abort();
31
+ else options.signal?.addEventListener("abort", abort, { once: true });
32
+ try {
33
+ for (;;) {
34
+ const { done, value } = await reader.read();
35
+ if (done) break;
36
+ buffer += decoder.decode(value, { stream: true });
37
+ const lines = buffer.split("\n\n");
38
+ buffer = lines.pop() ?? "";
39
+ for (const line of lines) {
40
+ if (!line.startsWith("data: ")) continue;
41
+ const data = line.slice(6);
42
+ if (data === "[DONE]") return;
43
+ let json: MastraStreamChunk;
44
+ try {
45
+ json = JSON.parse(data) as MastraStreamChunk;
46
+ } catch {
47
+ continue;
48
+ }
49
+ if (json) await options.onChunk(json);
50
+ }
51
+ }
52
+ } finally {
53
+ options.signal?.removeEventListener("abort", abort);
54
+ reader.releaseLock();
55
+ }
56
+ }
57
+
58
+ /** Attach {@link processMastraStream} to a fetch `Response`. */
59
+ export function asMastraStreamResponse(response: Response): MastraStreamResponse {
60
+ const streamResponse = response as MastraStreamResponse;
61
+ streamResponse.processDataStream = async ({ onChunk }) => {
62
+ if (!response.body) throw new Error("No response body");
63
+ await processMastraStream({ stream: response.body, onChunk });
64
+ };
65
+ return streamResponse;
66
+ }
@@ -0,0 +1,134 @@
1
+ import { string } from "@dbx-tools/shared-core";
2
+ import {
3
+ createHighlighter,
4
+ type BundledLanguage,
5
+ type BundledTheme,
6
+ type Highlighter,
7
+ } from "shiki";
8
+ import type { CodeHighlighterPlugin } from "streamdown";
9
+
10
+ // Streamdown 2.x ships syntax highlighting as an opt-in plugin and has
11
+ // no shiki dependency of its own; without a `code` plugin every fenced
12
+ // block renders as uncolored plaintext. This module provides a small
13
+ // shiki-backed highlighter to wire in via `plugins={{ code }}`.
14
+
15
+ /**
16
+ * Languages we highlight in the chat. SQL is the primary one (Genie
17
+ * query previews); the rest cover code the assistant might emit. Kept
18
+ * to a curated set so shiki only bundles these grammars.
19
+ */
20
+ const LANGUAGES: BundledLanguage[] = [
21
+ "sql",
22
+ "python",
23
+ "typescript",
24
+ "javascript",
25
+ "json",
26
+ "bash",
27
+ "yaml",
28
+ "markdown",
29
+ ];
30
+
31
+ /**
32
+ * Single light theme. The demo has no dark-mode toggle, and a single
33
+ * theme guarantees every token carries a concrete `color` (shiki's
34
+ * dual-theme mode emits CSS-variable-only tokens that need extra CSS
35
+ * wiring to paint).
36
+ */
37
+ const THEME: BundledTheme = "github-light";
38
+
39
+ /** Languages we can tokenize, as a set for O(1) support checks. */
40
+ const SUPPORTED = new Set<BundledLanguage>(LANGUAGES);
41
+
42
+ let _highlighter: Highlighter | null = null;
43
+ let _loading: Promise<Highlighter> | null = null;
44
+
45
+ /** Lazily create the shared shiki highlighter (singleton, loaded once). */
46
+ function loadHighlighter(): Promise<Highlighter> {
47
+ _loading ??= createHighlighter({ themes: [THEME], langs: LANGUAGES }).then((h) => {
48
+ _highlighter = h;
49
+ return h;
50
+ });
51
+ return _loading;
52
+ }
53
+
54
+ /** Tokenize `code` with the active theme into Streamdown's result shape. */
55
+ function highlightTokens(h: Highlighter, code: string, language: BundledLanguage) {
56
+ const { tokens, fg, bg, rootStyle } = h.codeToTokens(code, {
57
+ lang: language,
58
+ theme: THEME,
59
+ });
60
+ return { tokens, fg, bg, rootStyle };
61
+ }
62
+
63
+ /** Escape HTML-significant characters (from the shared string utils). */
64
+ const escapeHtml = string.escapeHtml;
65
+
66
+ /**
67
+ * Highlight `code` into minimal inline HTML: one colored `<span>` per
68
+ * token, lines joined by real newlines, with no line-number gutter or
69
+ * per-line wrapper elements. Meant to drop straight into a
70
+ * `<pre><code>` so the rendered text stays cleanly selectable and
71
+ * copyable. Falls back to plain escaped text when the language isn't
72
+ * supported or shiki fails to parse the snippet.
73
+ */
74
+ export async function highlightToHtml(code: string, language: string): Promise<string> {
75
+ if (!SUPPORTED.has(language as BundledLanguage)) return escapeHtml(code);
76
+ const h = await loadHighlighter();
77
+ try {
78
+ const { tokens } = h.codeToTokens(code, {
79
+ lang: language as BundledLanguage,
80
+ theme: THEME,
81
+ });
82
+ return tokens
83
+ .map((line) =>
84
+ line
85
+ .map(
86
+ (token) =>
87
+ `<span style="color:${token.color ?? "inherit"}">${escapeHtml(token.content)}</span>`,
88
+ )
89
+ .join(""),
90
+ )
91
+ .join("\n");
92
+ } catch {
93
+ return escapeHtml(code);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * shiki-backed code highlighter plugin for Streamdown. The highlighter
99
+ * loads asynchronously: the first call for any block returns `null`
100
+ * and resolves through the `callback` once shiki is ready, after which
101
+ * results are synchronous. Unsupported languages return `null` so the
102
+ * block stays plaintext rather than throwing.
103
+ */
104
+ export function createShikiPlugin(): CodeHighlighterPlugin {
105
+ const isSupported = (language: string): language is BundledLanguage =>
106
+ SUPPORTED.has(language as BundledLanguage);
107
+
108
+ return {
109
+ name: "shiki",
110
+ type: "code-highlighter",
111
+ getSupportedLanguages: () => LANGUAGES,
112
+ getThemes: () => [THEME, THEME],
113
+ supportsLanguage: (language) => isSupported(language),
114
+ highlight: (options, callback) => {
115
+ if (!isSupported(options.language)) return null;
116
+ const language = options.language;
117
+ if (_highlighter) {
118
+ try {
119
+ return highlightTokens(_highlighter, options.code, language);
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ void loadHighlighter().then((h) => {
125
+ try {
126
+ callback?.(highlightTokens(h, options.code, language));
127
+ } catch {
128
+ // Parse failure for this block - leave it as plaintext.
129
+ }
130
+ });
131
+ return null;
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,55 @@
1
+ import type { UIMessage } from "ai";
2
+ import type {
3
+ ChatStatus,
4
+ MessageFeedback,
5
+ PendingApproval,
6
+ ToolEvent,
7
+ } from "../react/types";
8
+
9
+ /** Session-scoped transcript + stream state for one conversation thread. */
10
+ export type ThreadSession = {
11
+ messages: UIMessage[];
12
+ status: ChatStatus;
13
+ error: Error | null;
14
+ toolEventsByMessage: Record<string, ToolEvent[]>;
15
+ pendingApprovalsByMessage: Record<string, PendingApproval[]>;
16
+ feedbackByMessage: Record<string, MessageFeedback>;
17
+ abortController: AbortController | null;
18
+ runToken: number;
19
+ assistantId: string | null;
20
+ runId: string | null;
21
+ historyLoaded: boolean;
22
+ hasMoreHistory: boolean;
23
+ historyPage: number;
24
+ lastUserText: string | null;
25
+ };
26
+
27
+ /** Map key for the classic single-thread chat (no explicit thread id). */
28
+ export const DEFAULT_THREAD_SESSION_KEY = "__session__";
29
+
30
+ export function createThreadSession(): ThreadSession {
31
+ return {
32
+ messages: [],
33
+ status: "ready",
34
+ error: null,
35
+ toolEventsByMessage: {},
36
+ pendingApprovalsByMessage: {},
37
+ feedbackByMessage: {},
38
+ abortController: null,
39
+ runToken: 0,
40
+ assistantId: null,
41
+ runId: null,
42
+ historyLoaded: false,
43
+ hasMoreHistory: false,
44
+ historyPage: 0,
45
+ lastUserText: null,
46
+ };
47
+ }
48
+
49
+ export function isSessionRunning(session: ThreadSession): boolean {
50
+ return session.status === "submitted" || session.status === "streaming";
51
+ }
52
+
53
+ export function sessionKey(activeThreadId: string | undefined): string {
54
+ return activeThreadId ?? DEFAULT_THREAD_SESSION_KEY;
55
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,43 @@
1
+ // ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
2
+ {
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "lib",
6
+ "alwaysStrict": true,
7
+ "declaration": true,
8
+ "esModuleInterop": true,
9
+ "experimentalDecorators": true,
10
+ "inlineSourceMap": true,
11
+ "inlineSources": true,
12
+ "lib": [
13
+ "ES2022",
14
+ "DOM",
15
+ "DOM.Iterable"
16
+ ],
17
+ "module": "ESNext",
18
+ "noEmitOnError": false,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noImplicitAny": true,
21
+ "noImplicitReturns": true,
22
+ "noImplicitThis": true,
23
+ "noUnusedLocals": true,
24
+ "noUnusedParameters": true,
25
+ "resolveJsonModule": true,
26
+ "strict": true,
27
+ "strictNullChecks": true,
28
+ "strictPropertyInitialization": true,
29
+ "stripInternal": true,
30
+ "target": "ES2022",
31
+ "types": [],
32
+ "moduleResolution": "bundler",
33
+ "skipLibCheck": true,
34
+ "jsx": "react-jsx"
35
+ },
36
+ "include": [
37
+ "src/**/*.ts",
38
+ "src/**/*.tsx"
39
+ ],
40
+ "exclude": [
41
+ "node_modules"
42
+ ]
43
+ }