@dbx-tools/ui-mastra 0.9.111 → 0.9.112

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/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/ui-mastra";
6
- export const PACKAGE_VERSION = "0.9.111";
6
+ export const PACKAGE_VERSION = "0.9.112";
7
7
  export * as reactBubbles from "./src/react/bubbles.tsx";
8
8
  export * as reactChatApprovals from "./src/react/chat-approvals.ts";
9
9
  export * as reactChatComposer from "./src/react/chat-composer.tsx";
@@ -35,7 +35,6 @@ export * as supportExport from "./src/support/export.ts";
35
35
  export * as supportMastraClient from "./src/support/mastra-client.ts";
36
36
  export * as supportModelSelection from "./src/support/model-selection.ts";
37
37
  export * as supportRequestContext from "./src/support/request-context.ts";
38
- export * as supportShikiPlugin from "./src/support/shiki-plugin.ts";
39
38
  export * as supportThreadLabels from "./src/support/thread-labels.ts";
40
39
  export * as supportThreadSessions from "./src/support/thread-sessions.ts";
41
40
  export * as supportThreadTabs from "./src/support/thread-tabs.ts";
@@ -52,7 +51,7 @@ export type { DataRow } from "./src/react/data-grid.tsx";
52
51
  export { MarkdownWithEmbeds } from "./src/react/embed-slots.tsx";
53
52
  export { ExportMenu } from "./src/react/export-menu.tsx";
54
53
  export { FeedbackControls } from "./src/react/feedback-controls.tsx";
55
- export { AssistantMarkdown, ToolMarkdown, SqlBlock, JsonBlock, SourceBlock } from "./src/react/markdown.tsx";
54
+ export { AssistantMarkdown, ToolMarkdown, SqlBlock, SourceBlock } from "./src/react/markdown.tsx";
56
55
  export type { MastraAssistantMode, MastraAssistantSide, MastraAssistantLauncherPosition, MastraAssistantResizeOptions, MastraAssistantLauncherOptions, MastraAssistantController, MastraAssistantProps } from "./src/react/mastra-assistant.tsx";
57
56
  export { useMastraChat, MastraChat } from "./src/react/mastra-chat.tsx";
58
57
  export type { UseMastraChatOptions, MastraChatProps } from "./src/react/mastra-chat.tsx";
package/package.json CHANGED
@@ -29,11 +29,11 @@
29
29
  "react-dom": "~19.2.4"
30
30
  },
31
31
  "dependencies": {
32
- "@dbx-tools/shared-core": "^0.9.111",
33
- "@dbx-tools/shared-genie": "^0.9.111",
34
- "@dbx-tools/shared-mastra": "^0.9.111",
35
- "@dbx-tools/shared-model": "^0.9.111",
36
- "@dbx-tools/ui": "^0.9.111",
32
+ "@dbx-tools/shared-core": "^0.9.112",
33
+ "@dbx-tools/shared-genie": "^0.9.112",
34
+ "@dbx-tools/shared-mastra": "^0.9.112",
35
+ "@dbx-tools/shared-model": "^0.9.112",
36
+ "@dbx-tools/ui": "^0.9.112",
37
37
  "@mastra/client-js": "1.50.0",
38
38
  "@mastra/core": "1.71.0",
39
39
  "@tanstack/react-table": "^8.21.3",
@@ -42,7 +42,6 @@
42
42
  "echarts-for-react": "^3.0.2",
43
43
  "lucide-react": "^0.554.0",
44
44
  "marked": "^18.0.5",
45
- "shiki": "^3.0.0",
46
45
  "sql-formatter": "^15.6.9",
47
46
  "streamdown": "^2.5.0"
48
47
  },
@@ -50,7 +49,7 @@
50
49
  "publishConfig": {
51
50
  "access": "public"
52
51
  },
53
- "version": "0.9.111",
52
+ "version": "0.9.112",
54
53
  "dbxToolsConfig": {
55
54
  "tags": [
56
55
  "ui"
@@ -192,7 +192,7 @@ const ToolApprovalCard = ({
192
192
  {asEmailInput(input) ? (
193
193
  <EmailApprovalPreview email={asEmailInput(input)!} />
194
194
  ) : (
195
- <RecordPreview value={input} className="rounded bg-background/40" />
195
+ <RecordPreview value={input} className="max-h-80 rounded bg-background/40" />
196
196
  )}
197
197
  {expired ? (
198
198
  <div className="mt-3 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
@@ -10,6 +10,9 @@ import {
10
10
  Tooltip,
11
11
  TooltipContent,
12
12
  TooltipTrigger,
13
+ createShikiPlugin,
14
+ highlightToHtml,
15
+ useHighlightedHtml,
13
16
  cn,
14
17
  } from "@dbx-tools/ui/react";
15
18
  import { CheckIcon, CopyIcon } from "lucide-react";
@@ -17,7 +20,6 @@ import React, { useEffect, useRef, useState } from "react";
17
20
  import { Streamdown } from "streamdown";
18
21
  import { DataGrid, TABLE_WRAPPER_CLASSES, colorizeDelta, type DataRow } from "./data-grid.tsx";
19
22
  import { copyText } from "../support/clipboard.ts";
20
- import { createShikiPlugin, highlightToHtml } from "../support/shiki-plugin.ts";
21
23
 
22
24
  // Markdown rendering for the chat: the streaming `Streamdown` engine
23
25
  // wired with shiki highlighting and AppKit table primitives, plus the
@@ -181,27 +183,6 @@ const TOOL_MARKDOWN_COMPONENTS = {
181
183
  */
182
184
  const SHIKI_PLUGIN = { code: createShikiPlugin() };
183
185
 
184
- /** Resolve syntax-highlighted HTML while preserving plaintext during lazy load. */
185
- function useHighlightedHtml(source: string, language: string): string | null {
186
- const [highlighted, setHighlighted] = useState<{
187
- source: string;
188
- language: string;
189
- html: string;
190
- } | null>(null);
191
- useEffect(() => {
192
- let active = true;
193
- void highlightToHtml(source, language).then((result) => {
194
- if (active) setHighlighted({ source, language, html: result });
195
- });
196
- return () => {
197
- active = false;
198
- };
199
- }, [language, source]);
200
- return highlighted?.source === source && highlighted.language === language
201
- ? highlighted.html
202
- : null;
203
- }
204
-
205
186
  /**
206
187
  * Per-word fade-in applied while a reply is still streaming. Streamdown
207
188
  * wraps each newly arrived token in a `[data-sd-animate]` span and only
@@ -397,28 +378,6 @@ export const SqlBlock = ({ sql }: { sql: string }) => {
397
378
  );
398
379
  };
399
380
 
400
- /**
401
- * Render formatted JSON with the same lazily loaded Shiki runtime used by
402
- * SQL and fenced Markdown code blocks.
403
- */
404
- export const JsonBlock = ({ json, className }: { json: string; className?: string }) => {
405
- const html = useHighlightedHtml(json, "json");
406
- return (
407
- <pre
408
- className={cn(
409
- "max-w-full overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed",
410
- className,
411
- )}
412
- >
413
- {html === null ? (
414
- <code>{json}</code>
415
- ) : (
416
- <code data-dbx-highlighted-code dangerouslySetInnerHTML={{ __html: html }} />
417
- )}
418
- </pre>
419
- );
420
- };
421
-
422
381
  /** Compact syntax-highlighted source block for tool request previews. */
423
382
  export const SourceBlock = ({
424
383
  source,
@@ -4,11 +4,13 @@ import {
4
4
  Collapsible,
5
5
  CollapsibleContent,
6
6
  CollapsibleTrigger,
7
+ JsonBlock,
8
+ RecordPreview,
7
9
  Spinner,
8
10
  cn,
9
11
  } from "@dbx-tools/ui/react";
10
12
  import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
11
- import { JsonBlock, SourceBlock, SqlBlock, ToolMarkdown } from "./markdown.tsx";
13
+ import { SourceBlock, SqlBlock, ToolMarkdown } from "./markdown.tsx";
12
14
  import type { ToolEvent, ToolProgress } from "./types.ts";
13
15
 
14
16
  // Consolidated tool-session pill and its Genie progress detail view:
@@ -388,20 +390,50 @@ export function formatRawToolPayload(value: unknown): string {
388
390
  return JSON.stringify(value, null, 2) ?? String(value);
389
391
  }
390
392
 
391
- const RawToolPayload = ({ label, value }: { label: "Request" | "Response"; value: unknown }) => (
392
- <Collapsible className="rounded border border-border/60 bg-background/40">
393
- <CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-xs uppercase tracking-wide text-muted-foreground hover:text-foreground">
394
- <ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
395
- <span>{label}</span>
396
- </CollapsibleTrigger>
397
- <CollapsibleContent>
398
- <JsonBlock
399
- json={formatRawToolPayload(value)}
400
- className="max-h-80 border-t border-border/50 px-2 py-2 text-foreground"
401
- />
402
- </CollapsibleContent>
403
- </Collapsible>
404
- );
393
+ /** Return structured JSON from an object value or a complete JSON string. */
394
+ export function structuredToolPayload(value: unknown): unknown | undefined {
395
+ if (typeof value === "object" && value !== null) return value;
396
+ if (typeof value !== "string") return undefined;
397
+ const trimmed = value.trim();
398
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return undefined;
399
+ try {
400
+ const parsed: unknown = JSON.parse(trimmed);
401
+ return typeof parsed === "object" && parsed !== null ? parsed : undefined;
402
+ } catch {
403
+ return undefined;
404
+ }
405
+ }
406
+
407
+ const RawToolPayload = ({
408
+ label,
409
+ value,
410
+ }: {
411
+ label: "Request" | "Response" | "Output";
412
+ value: unknown;
413
+ }) => {
414
+ const structured = structuredToolPayload(value);
415
+ return (
416
+ <Collapsible className="rounded border border-border/60 bg-background/40">
417
+ <CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-xs uppercase tracking-wide text-muted-foreground hover:text-foreground">
418
+ <ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
419
+ <span>{label}</span>
420
+ </CollapsibleTrigger>
421
+ <CollapsibleContent>
422
+ {structured === undefined ? (
423
+ <JsonBlock
424
+ json={formatRawToolPayload(value)}
425
+ className="max-h-80 border-t border-border/50 px-2 py-2 text-foreground"
426
+ />
427
+ ) : (
428
+ <RecordPreview
429
+ value={structured}
430
+ className="max-h-80 border-t border-border/50 bg-transparent text-foreground"
431
+ />
432
+ )}
433
+ </CollapsibleContent>
434
+ </Collapsible>
435
+ );
436
+ };
405
437
 
406
438
  export type ToolInputPresentation = {
407
439
  label: "Code" | "Command" | "Output";
@@ -621,7 +653,9 @@ const ToolCallDetails = ({
621
653
  ) : null}
622
654
  <WebSearchProgressDetails event={event} />
623
655
  <ToolProgressDetails summary={summary} omitQuestion />
624
- {outputPresentation ? (
656
+ {outputPresentation?.language === "json" ? (
657
+ <RawToolPayload label="Output" value={outputPresentation.source} />
658
+ ) : outputPresentation ? (
625
659
  <ToolInputSource presentation={outputPresentation} />
626
660
  ) : "output" in event ? (
627
661
  <RawToolPayload label="Response" value={event.output} />
@@ -1,27 +0,0 @@
1
- /**
2
- * Fine-grained Shiki runtime containing only the chat's supported languages.
3
- *
4
- * This module is itself dynamically imported, so its grammar, theme, and WASM
5
- * dependencies remain off the network until a supported code block appears.
6
- */
7
- import { createHighlighterCore } from "shiki/core";
8
- import { createOnigurumaEngine } from "shiki/engine/oniguruma";
9
- import bash from "shiki/langs/bash.mjs";
10
- import javascript from "shiki/langs/javascript.mjs";
11
- import json from "shiki/langs/json.mjs";
12
- import markdown from "shiki/langs/markdown.mjs";
13
- import python from "shiki/langs/python.mjs";
14
- import sql from "shiki/langs/sql.mjs";
15
- import typescript from "shiki/langs/typescript.mjs";
16
- import yaml from "shiki/langs/yaml.mjs";
17
- import githubDarkHighContrast from "shiki/themes/github-dark-high-contrast.mjs";
18
- import githubLightHighContrast from "shiki/themes/github-light-high-contrast.mjs";
19
-
20
- /** Create the shared highlighter with the exact configured language set. */
21
- export function createHighlighter() {
22
- return createHighlighterCore({
23
- themes: [githubLightHighContrast, githubDarkHighContrast],
24
- langs: [sql, python, typescript, javascript, json, bash, yaml, markdown],
25
- engine: createOnigurumaEngine(import("shiki/wasm")),
26
- });
27
- }
@@ -1,158 +0,0 @@
1
- import { stringUtils } from "@dbx-tools/shared-core";
2
- import type { BundledLanguage, BundledTheme } from "shiki";
3
- import type { HighlighterCore } from "shiki/core";
4
- import type { CodeHighlighterPlugin } from "streamdown";
5
-
6
- // Streamdown 2.x ships syntax highlighting as an opt-in plugin and has
7
- // no shiki dependency of its own; without a `code` plugin every fenced
8
- // block renders as uncolored plaintext. This module provides a small
9
- // shiki-backed highlighter to wire in via `plugins={{ code }}`.
10
-
11
- /**
12
- * Languages we highlight in the chat. SQL is the primary one (Genie
13
- * query previews); the rest cover code the assistant might emit. Kept
14
- * to a curated set so shiki only bundles these grammars.
15
- */
16
- const LANGUAGES = [
17
- "text",
18
- "plaintext",
19
- "txt",
20
- "sql",
21
- "python",
22
- "typescript",
23
- "javascript",
24
- "json",
25
- "bash",
26
- "yaml",
27
- "markdown",
28
- ] as const;
29
-
30
- /** High-contrast theme pair used for every syntax surface. */
31
- const THEMES = [
32
- "github-light-high-contrast",
33
- "github-dark-high-contrast",
34
- ] as const satisfies readonly [BundledTheme, BundledTheme];
35
-
36
- /** Languages we can tokenize, as a set for O(1) support checks. */
37
- const SUPPORTED = new Set<string>(LANGUAGES);
38
-
39
- let _highlighter: HighlighterCore | null = null;
40
- let _loading: Promise<HighlighterCore> | null = null;
41
-
42
- /** Lazily create the shared shiki highlighter (singleton, loaded once). */
43
- function loadHighlighter(): Promise<HighlighterCore> {
44
- _loading ??= import("./_shiki-runtime.ts")
45
- .then(({ createHighlighter }) => createHighlighter())
46
- .then((highlighter) => {
47
- _highlighter = highlighter;
48
- return highlighter;
49
- });
50
- return _loading;
51
- }
52
-
53
- function splitThemeColor(
54
- value: string | undefined,
55
- darkVariable: "--shiki-dark" | "--shiki-dark-bg",
56
- ): { light?: string; dark?: string } {
57
- if (!value) return {};
58
- const marker = `;${darkVariable}:`;
59
- const index = value.indexOf(marker);
60
- if (index < 0) return { light: value };
61
- return {
62
- light: value.slice(0, index),
63
- dark: value.slice(index + marker.length),
64
- };
65
- }
66
-
67
- /** Tokenize `code` with the active themes into Streamdown's result shape. */
68
- function highlightTokens(h: HighlighterCore, code: string, language: string) {
69
- const { tokens, fg, bg, rootStyle } = h.codeToTokens(code, {
70
- lang: language as BundledLanguage,
71
- themes: { light: THEMES[0], dark: THEMES[1] },
72
- });
73
- const foreground = splitThemeColor(fg, "--shiki-dark");
74
- const background = splitThemeColor(bg, "--shiki-dark-bg");
75
- const darkStyles = [
76
- foreground.dark ? `--shiki-dark:${foreground.dark}` : "",
77
- background.dark ? `--shiki-dark-bg:${background.dark}` : "",
78
- ]
79
- .filter(Boolean)
80
- .join(";");
81
- return {
82
- tokens,
83
- fg: foreground.light,
84
- bg: background.light,
85
- rootStyle: [rootStyle, darkStyles].filter(Boolean).join(";"),
86
- };
87
- }
88
-
89
- /** Escape HTML-significant characters (from the shared string utils). */
90
- const escapeHtml = stringUtils.escapeHtml;
91
-
92
- /**
93
- * Highlight `code` into minimal inline HTML: one colored `<span>` per
94
- * token, lines joined by real newlines, with no line-number gutter or
95
- * per-line wrapper elements. Meant to drop straight into a
96
- * `<pre><code>` so the rendered text stays cleanly selectable and
97
- * copyable. Falls back to plain escaped text when the language isn't
98
- * supported or shiki fails to parse the snippet.
99
- */
100
- export async function highlightToHtml(code: string, language: string): Promise<string> {
101
- if (!SUPPORTED.has(language)) return escapeHtml(code);
102
- const h = await loadHighlighter();
103
- try {
104
- const { tokens } = highlightTokens(h, code, language);
105
- return tokens
106
- .map((line) =>
107
- line
108
- .map((token) => {
109
- const light = token.htmlStyle?.color ?? token.color ?? "inherit";
110
- const dark = token.htmlStyle?.["--shiki-dark"] ?? light;
111
- const style = `--sdm-c:${light};--shiki-dark:${dark}`;
112
- return `<span style="${escapeHtml(style)}">${escapeHtml(token.content)}</span>`;
113
- })
114
- .join(""),
115
- )
116
- .join("\n");
117
- } catch {
118
- return escapeHtml(code);
119
- }
120
- }
121
-
122
- /**
123
- * shiki-backed code highlighter plugin for Streamdown. The highlighter
124
- * loads asynchronously: the first call for any block returns `null`
125
- * and resolves through the `callback` once shiki is ready, after which
126
- * results are synchronous. Unsupported languages return `null` so the
127
- * block stays plaintext rather than throwing.
128
- */
129
- export function createShikiPlugin(): CodeHighlighterPlugin {
130
- const isSupported = (language: string): boolean => SUPPORTED.has(language);
131
-
132
- return {
133
- name: "shiki",
134
- type: "code-highlighter",
135
- getSupportedLanguages: () => [...LANGUAGES],
136
- getThemes: () => [...THEMES],
137
- supportsLanguage: (language) => isSupported(language),
138
- highlight: (options, callback) => {
139
- if (!isSupported(options.language)) return null;
140
- const language = options.language;
141
- if (_highlighter) {
142
- try {
143
- return highlightTokens(_highlighter, options.code, language);
144
- } catch {
145
- return null;
146
- }
147
- }
148
- void loadHighlighter().then((h) => {
149
- try {
150
- callback?.(highlightTokens(h, options.code, language));
151
- } catch {
152
- // Parse failure for this block - leave it as plaintext.
153
- }
154
- });
155
- return null;
156
- },
157
- };
158
- }