@djangocfg/ui-tools 2.1.430 → 2.1.432

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.
Files changed (188) hide show
  1. package/dist/tree/index.cjs +43 -8
  2. package/dist/tree/index.cjs.map +1 -1
  3. package/dist/tree/index.d.cts +31 -1
  4. package/dist/tree/index.d.ts +31 -1
  5. package/dist/tree/index.mjs +43 -8
  6. package/dist/tree/index.mjs.map +1 -1
  7. package/package.json +28 -14
  8. package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
  9. package/src/common/CodePanelHeader/index.ts +1 -0
  10. package/src/common/blocks/BlockError.tsx +46 -0
  11. package/src/common/blocks/index.ts +34 -0
  12. package/src/common/blocks/schemas.ts +143 -0
  13. package/src/common/blocks/types.ts +76 -0
  14. package/src/common/chips/README.md +67 -0
  15. package/src/common/chips/UrlChip.tsx +105 -0
  16. package/src/common/chips/index.ts +20 -0
  17. package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
  18. package/src/common/link-preview/index.ts +12 -0
  19. package/src/common/link-preview/types.ts +44 -0
  20. package/src/common/tiptap/index.ts +10 -0
  21. package/src/common/tiptap/useNodeAttrs.ts +128 -0
  22. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
  23. package/src/tools/chat/README.md +109 -2
  24. package/src/tools/chat/composer/Composer.tsx +45 -12
  25. package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
  26. package/src/tools/chat/composer/PastedTextDialog.tsx +88 -0
  27. package/src/tools/chat/composer/index.ts +9 -0
  28. package/src/tools/chat/composer/textToAttachment.ts +69 -0
  29. package/src/tools/chat/composer/types.ts +13 -0
  30. package/src/tools/chat/composer/useComposerAttach.tsx +47 -0
  31. package/src/tools/chat/constants.ts +9 -2
  32. package/src/tools/chat/core/extractFirstUrl.ts +45 -0
  33. package/src/tools/chat/core/reducer.ts +32 -8
  34. package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
  35. package/src/tools/chat/core/transport/mock.ts +4 -1
  36. package/src/tools/chat/hooks/useChat.ts +20 -20
  37. package/src/tools/chat/lazy.tsx +10 -0
  38. package/src/tools/chat/messages/Attachments.tsx +335 -46
  39. package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
  40. package/src/tools/chat/messages/MessageBubble.tsx +175 -31
  41. package/src/tools/chat/messages/MessageList.tsx +107 -21
  42. package/src/tools/chat/messages/ToolCalls.tsx +28 -13
  43. package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
  44. package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
  45. package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
  46. package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
  47. package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
  48. package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
  49. package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
  50. package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
  51. package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
  52. package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
  53. package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
  54. package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
  55. package/src/tools/chat/messages/index.ts +13 -0
  56. package/src/tools/chat/public.ts +5 -1
  57. package/src/tools/chat/shell/ChatRoot.tsx +29 -0
  58. package/src/tools/chat/styles/bubbleTokens.ts +18 -0
  59. package/src/tools/chat/styles/index.ts +1 -0
  60. package/src/tools/chat/types/attachment.ts +38 -3
  61. package/src/tools/chat/types/block.ts +56 -9
  62. package/src/tools/chat/types/blockSchemas.ts +197 -0
  63. package/src/tools/chat/types/config.ts +33 -0
  64. package/src/tools/chat/types/events.ts +14 -7
  65. package/src/tools/chat/types/index.ts +4 -2
  66. package/src/tools/chat/types/message.ts +7 -0
  67. package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
  68. package/src/tools/data/Tree/README.md +30 -0
  69. package/src/tools/data/Tree/components/TreeRow.tsx +11 -2
  70. package/src/tools/data/Tree/context/menu/render.tsx +56 -27
  71. package/src/tools/data/Tree/data/appearance.ts +65 -3
  72. package/src/tools/data/Tree/data/index.ts +2 -0
  73. package/src/tools/data/Tree/index.tsx +2 -0
  74. package/src/tools/dev/Map/README.md +30 -345
  75. package/src/tools/dev/Map/cards/index.ts +6 -0
  76. package/src/tools/dev/Map/cards/types.ts +67 -0
  77. package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
  78. package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
  79. package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
  80. package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
  81. package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
  82. package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
  83. package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
  84. package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
  85. package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
  86. package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
  87. package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
  88. package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
  89. package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
  90. package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
  91. package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
  92. package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
  93. package/src/tools/dev/Map/components/MapControls.tsx +27 -0
  94. package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
  95. package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
  96. package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
  97. package/src/tools/dev/Map/components/index.ts +35 -1
  98. package/src/tools/dev/Map/geocode/index.ts +2 -0
  99. package/src/tools/dev/Map/geocode/photon.ts +265 -0
  100. package/src/tools/dev/Map/hooks/index.ts +37 -0
  101. package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
  102. package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
  103. package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
  104. package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
  105. package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
  106. package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
  107. package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
  108. package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
  109. package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
  110. package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
  111. package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
  112. package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
  113. package/src/tools/dev/Map/index.ts +59 -2
  114. package/src/tools/dev/Map/layers/index.ts +1 -0
  115. package/src/tools/dev/Map/layers/symbol.ts +93 -0
  116. package/src/tools/dev/Map/lazy.tsx +120 -0
  117. package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
  118. package/src/tools/dev/Map/styles/index.ts +62 -0
  119. package/src/tools/dev/Map/styles/terrain.ts +29 -0
  120. package/src/tools/dev/Map/types.ts +31 -3
  121. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
  122. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
  123. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
  124. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
  125. package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
  126. package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
  127. package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
  128. package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
  129. package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
  130. package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
  131. package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
  132. package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
  133. package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
  134. package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
  135. package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
  136. package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
  137. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
  138. package/src/tools/forms/MarkdownEditor/README.md +84 -0
  139. package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
  140. package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
  141. package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
  142. package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
  143. package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
  144. package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
  145. package/src/tools/forms/MarkdownEditor/index.ts +19 -0
  146. package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
  147. package/src/tools/forms/MarkdownEditor/styles.css +134 -2
  148. package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
  149. package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
  150. package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
  151. package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
  152. package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
  153. package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
  154. package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
  155. package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
  156. package/src/tools/forms/NotionEditor/README.md +109 -188
  157. package/src/tools/forms/NotionEditor/extensions.ts +15 -0
  158. package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
  159. package/src/tools/forms/NotionEditor/styles.css +25 -0
  160. package/src/tools/forms/NotionEditor/types.ts +9 -0
  161. package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
  162. package/src/tools/input/SpeechRecognition/types.ts +10 -0
  163. package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
  164. package/src/tools/media/ImageViewer/README.md +9 -0
  165. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
  166. package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
  167. package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
  168. package/src/tools/media/ImageViewer/components/index.ts +2 -0
  169. package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
  170. package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
  171. package/src/tools/media/ImageViewer/types.ts +10 -0
  172. package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
  173. package/src/tools/media/ImageViewer/utils/index.ts +4 -0
  174. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
  175. package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
  176. package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
  177. package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
  178. package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
  179. package/src/tools/visual/charts/SmoothLine/README.md +97 -0
  180. package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
  181. package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
  182. package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
  183. package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
  184. package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
  185. package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
  186. package/src/tools/visual/indicators/Fps/types.ts +1 -1
  187. package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
  188. package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Zod schemas for the chat `MessageBlock` discriminated union (`./block.ts`).
3
+ *
4
+ * These are the runtime VALIDATORS used at the single registry dispatch point
5
+ * (`messages/blocks/MessageBlocks.tsx`): every block flows through
6
+ * `safeParseBlock(BLOCK_SCHEMAS[kind], block)` BEFORE its renderer runs, so a
7
+ * malformed wire / hand-authored payload renders a safe `<BlockError>` card
8
+ * instead of throwing inside a renderer and blanking the transcript.
9
+ *
10
+ * Design rules (mirroring `common/blocks/schemas.ts`):
11
+ * - The TS types in `./block.ts` stay the source of truth; these schemas are
12
+ * kept in sync and `Extends<…>`-checked below so they can't silently drift.
13
+ * - The map / link shapes are COMPOSED from the shared
14
+ * `MapBlockPayloadSchema` / `LinkPreviewDataSchema` in `common/blocks` — we
15
+ * never re-describe a map/link payload here.
16
+ * - LENIENT where the renderer is tolerant: optional fields stay optional,
17
+ * `json.data` is `unknown` (JsonTree is throw-safe), extra keys are stripped.
18
+ * - STRICT on the load-bearing shape: a field whose wrong type would THROW in
19
+ * the renderer is required + typed (gallery `items` must be an array;
20
+ * image/video/audio `src` must be a string; map coords are finite
21
+ * numbers via the shared schema).
22
+ * - `custom` and the prose kinds (`text`/`markdown`) carry no schema in
23
+ * `BLOCK_SCHEMAS` — `custom` is host-defined (don't over-constrain) and the
24
+ * prose kinds can't throw on a wrong-typed field worth guarding. They render
25
+ * as-is. (`text`/`markdown` schemas are still defined for completeness.)
26
+ */
27
+
28
+ import { z } from 'zod';
29
+
30
+ import { MapBlockPayloadSchema, LinkPreviewDataSchema } from '../../../common/blocks';
31
+ import type {
32
+ TextBlock,
33
+ MarkdownBlock,
34
+ AudioBlock,
35
+ VideoBlock,
36
+ ImageBlock,
37
+ GalleryBlock,
38
+ MapBlock,
39
+ JsonBlock,
40
+ MermaidBlock,
41
+ CodeBlock,
42
+ DiffBlock,
43
+ LinkBlock,
44
+ MessageBlock,
45
+ } from './block';
46
+
47
+ /**
48
+ * Base fields shared by every block (`BlockBase` in `./block.ts`). We validate
49
+ * the WHOLE block object (incl. `kind` / `id` / `caption`), so each schema
50
+ * composes this. `id` is required (it is the React key + memo discriminant).
51
+ */
52
+ const BlockBaseSchema = z.object({
53
+ id: z.string(),
54
+ caption: z.string().optional(),
55
+ });
56
+
57
+ const TextBlockSchema = BlockBaseSchema.extend({
58
+ kind: z.literal('text'),
59
+ text: z.string(),
60
+ });
61
+
62
+ const MarkdownBlockSchema = BlockBaseSchema.extend({
63
+ kind: z.literal('markdown'),
64
+ markdown: z.string(),
65
+ });
66
+
67
+ const AudioBlockSchema = BlockBaseSchema.extend({
68
+ kind: z.literal('audio'),
69
+ src: z.string(),
70
+ title: z.string().optional(),
71
+ artist: z.string().optional(),
72
+ cover: z.string().optional(),
73
+ variant: z.enum(['default', 'compact']).optional(),
74
+ });
75
+
76
+ const VideoBlockSchema = BlockBaseSchema.extend({
77
+ kind: z.literal('video'),
78
+ src: z.string(),
79
+ poster: z.string().optional(),
80
+ title: z.string().optional(),
81
+ aspectRatio: z.union([z.number(), z.literal('auto'), z.literal('fill')]).optional(),
82
+ });
83
+
84
+ const ImageBlockSchema = BlockBaseSchema.extend({
85
+ kind: z.literal('image'),
86
+ src: z.string(),
87
+ alt: z.string().optional(),
88
+ interactive: z.boolean().optional(),
89
+ });
90
+
91
+ const GalleryItemSchema = z.object({
92
+ id: z.string(),
93
+ src: z.string(),
94
+ thumbnail: z.string().optional(),
95
+ alt: z.string().optional(),
96
+ });
97
+
98
+ const GalleryBlockSchema = BlockBaseSchema.extend({
99
+ kind: z.literal('gallery'),
100
+ // STRICT: `items` must be an array — the renderer maps over it.
101
+ items: z.array(GalleryItemSchema),
102
+ });
103
+
104
+ /**
105
+ * Map block = the shared serializable map payload (`MapBlockPayloadSchema`,
106
+ * which already enforces finite-number `center.lat/lng` etc.) + the base block
107
+ * fields + the chat-only `userLocation` opt-in flag. Composed, never
108
+ * duplicated.
109
+ */
110
+ const MapBlockSchema = MapBlockPayloadSchema.merge(BlockBaseSchema).extend({
111
+ kind: z.literal('map'),
112
+ userLocation: z.boolean().optional(),
113
+ });
114
+
115
+ const JsonBlockSchema = BlockBaseSchema.extend({
116
+ kind: z.literal('json'),
117
+ // LENIENT: JsonTree renders anything safely — accept any payload.
118
+ data: z.unknown(),
119
+ mode: z.enum(['full', 'compact', 'inline']).optional(),
120
+ });
121
+
122
+ const MermaidBlockSchema = BlockBaseSchema.extend({
123
+ kind: z.literal('mermaid'),
124
+ chart: z.string(),
125
+ });
126
+
127
+ const CodeBlockSchema = BlockBaseSchema.extend({
128
+ kind: z.literal('code'),
129
+ code: z.string(),
130
+ language: z.string(),
131
+ });
132
+
133
+ const DiffBlockSchema = BlockBaseSchema.extend({
134
+ kind: z.literal('diff'),
135
+ oldCode: z.string().optional(),
136
+ newCode: z.string().optional(),
137
+ patch: z.string().optional(),
138
+ language: z.string().optional(),
139
+ layout: z.enum(['split', 'unified']).optional(),
140
+ });
141
+
142
+ /**
143
+ * Link block = base fields + `url` + optional pre-resolved `data` (validated
144
+ * against the shared `LinkPreviewDataSchema`). The renderer also tolerates a
145
+ * resolver-only block (no `data`), so `data` stays optional.
146
+ */
147
+ const LinkBlockSchema = BlockBaseSchema.extend({
148
+ kind: z.literal('link'),
149
+ url: z.string(),
150
+ data: LinkPreviewDataSchema.optional(),
151
+ });
152
+
153
+ // ── Drift guards ──────────────────────────────────────────────────────────
154
+ // Type-level assertion that each schema's inferred output stays assignable to
155
+ // the canonical block type in `./block.ts`. If a schema and the type diverge,
156
+ // the corresponding assignment fails to compile.
157
+ type Extends<A, B> = A extends B ? true : never;
158
+ const _textOk: Extends<z.infer<typeof TextBlockSchema>, TextBlock> = true;
159
+ const _markdownOk: Extends<z.infer<typeof MarkdownBlockSchema>, MarkdownBlock> = true;
160
+ const _audioOk: Extends<z.infer<typeof AudioBlockSchema>, AudioBlock> = true;
161
+ const _videoOk: Extends<z.infer<typeof VideoBlockSchema>, VideoBlock> = true;
162
+ const _imageOk: Extends<z.infer<typeof ImageBlockSchema>, ImageBlock> = true;
163
+ const _galleryOk: Extends<z.infer<typeof GalleryBlockSchema>, GalleryBlock> = true;
164
+ const _mapOk: Extends<z.infer<typeof MapBlockSchema>, MapBlock> = true;
165
+ const _jsonOk: Extends<z.infer<typeof JsonBlockSchema>, JsonBlock> = true;
166
+ const _mermaidOk: Extends<z.infer<typeof MermaidBlockSchema>, MermaidBlock> = true;
167
+ const _codeOk: Extends<z.infer<typeof CodeBlockSchema>, CodeBlock> = true;
168
+ const _diffOk: Extends<z.infer<typeof DiffBlockSchema>, DiffBlock> = true;
169
+ const _linkOk: Extends<z.infer<typeof LinkBlockSchema>, LinkBlock> = true;
170
+ void [
171
+ _textOk, _markdownOk, _audioOk, _videoOk, _imageOk, _galleryOk, _mapOk,
172
+ _jsonOk, _mermaidOk, _codeOk, _diffOk, _linkOk,
173
+ ];
174
+
175
+ /**
176
+ * `kind` → schema. Validated at the registry dispatch point. Kinds WITHOUT an
177
+ * entry are skipped (rendered as-is):
178
+ * - `custom` — host-defined `payload`; over-constraining it would break hosts.
179
+ *
180
+ * Every built-in data block is covered so a malformed payload is caught
181
+ * centrally — and any FUTURE kind is guarded the moment it's added here.
182
+ */
183
+ export const BLOCK_SCHEMAS: Partial<Record<MessageBlock['kind'], z.ZodType>> = {
184
+ text: TextBlockSchema,
185
+ markdown: MarkdownBlockSchema,
186
+ audio: AudioBlockSchema,
187
+ video: VideoBlockSchema,
188
+ image: ImageBlockSchema,
189
+ gallery: GalleryBlockSchema,
190
+ map: MapBlockSchema,
191
+ json: JsonBlockSchema,
192
+ mermaid: MermaidBlockSchema,
193
+ code: CodeBlockSchema,
194
+ diff: DiffBlockSchema,
195
+ link: LinkBlockSchema,
196
+ // `custom` intentionally omitted — host-defined, rendered as-is.
197
+ };
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { ChatAssistantContext, ChatUserContext } from './persona';
6
6
  import type { ChatLabels } from './labels';
7
+ import type { ResolveLinkPreview } from '../../../common/link-preview';
7
8
 
8
9
  export type ChatDisplayMode = 'closed' | 'embedded' | 'floating' | 'sidebar' | 'fullscreen';
9
10
 
@@ -40,4 +41,36 @@ export interface ChatConfig {
40
41
  prefs?: ChatPrefs;
41
42
  /** Visual labels (i18n is the host's job). */
42
43
  labels?: Partial<ChatLabels>;
44
+ /** Link-preview (URL unfurl) wiring — the host owns metadata fetching. */
45
+ linkPreview?: ChatLinkPreviewConfig;
46
+ /**
47
+ * Render bare web URLs inside message bubbles as compact "URL chips"
48
+ * (favicon + domain + middle-ellipsis path, e.g.
49
+ * `github.com/wailsapp/…/README.md`) instead of a plain blue link —
50
+ * matching the composer's URL chip. Only bare `http(s)` autolinks are
51
+ * chipped; a `[labeled](url)` link keeps its author-chosen text as a
52
+ * normal link.
53
+ *
54
+ * Off by default (the host opts in). When on, the chip rule is appended
55
+ * AFTER any host-supplied `linkRules`, so host-specific rules win.
56
+ * @default false
57
+ */
58
+ linkChips?: boolean;
59
+ }
60
+
61
+ export interface ChatLinkPreviewConfig {
62
+ /**
63
+ * Host resolver that turns a URL into preview metadata. Required for any
64
+ * link card to appear (the browser cannot fetch meta tags cross-origin).
65
+ * In Wails/Go this wraps a bound `FetchLinkMetadata` method; on the web,
66
+ * a `/api/unfurl` route.
67
+ */
68
+ resolve?: ResolveLinkPreview;
69
+ /**
70
+ * Auto-render a preview card under a message bubble when its text
71
+ * contains a link (Telegram-style — at most ONE card per message).
72
+ * Off by default; needs `resolve` to be set.
73
+ * @default false
74
+ */
75
+ autoDetect?: boolean;
43
76
  }
@@ -5,12 +5,12 @@
5
5
  * into this union by `mapPydanticAIEvent` and friends.
6
6
  */
7
7
 
8
- import type { ChatSource } from './attachment';
8
+ import type { ChatMessage } from './message';
9
9
 
10
10
  /**
11
- * Per-run metrics block. Emitted once per assistant turn alongside the
12
- * terminal `message_end` event (a separate `message_metrics` event so
13
- * `message_end` keeps its lean `tokensIn`/`tokensOut`/`sources` shape).
11
+ * Per-run metrics block. Emitted once per assistant turn, carried into the
12
+ * message either by a non-terminal `message_metrics` event mid-run or via
13
+ * the terminal `message_end` event's generic `patch` slot.
14
14
  *
15
15
  * All fields optional — the backend omits anything it could not measure.
16
16
  */
@@ -54,9 +54,16 @@ export type ChatStreamEvent =
54
54
  }
55
55
  | {
56
56
  type: 'message_end';
57
- tokensIn?: number;
58
- tokensOut?: number;
59
- sources?: ChatSource[];
57
+ /**
58
+ * Generic finalize patch merged into the message on done — the ONE
59
+ * slot every finalize feature rides (tokens, sources, blocks, OCR
60
+ * text, attachments-finalized, local-preview metadata). `content`
61
+ * here REPLACES the accumulated bubble body (the append-only gap).
62
+ * The reducer applies it as a `Partial<ChatMessage>` merge — the same
63
+ * mechanism `message_metrics`/`resolved_model` already use. Absent →
64
+ * `message_end` is a pure terminal no-op (just clears `isStreaming`).
65
+ */
66
+ patch?: Partial<ChatMessage>;
60
67
  }
61
68
  | {
62
69
  /**
@@ -33,13 +33,15 @@ export type {
33
33
  JsonBlock,
34
34
  MermaidBlock,
35
35
  CodeBlock,
36
- LottieBlock,
36
+ DiffBlock,
37
+ LinkBlock,
37
38
  CustomBlock,
38
39
  } from './block';
40
+ export type { LinkPreviewData, ResolveLinkPreview } from '../../../common/link-preview';
39
41
  export type { ChatMessage } from './message';
40
42
  export { DEFAULT_LABELS } from './labels';
41
43
  export type { ChatLabels } from './labels';
42
- export type { ChatConfig, ChatPrefs, ChatDisplayMode } from './config';
44
+ export type { ChatConfig, ChatPrefs, ChatDisplayMode, ChatLinkPreviewConfig } from './config';
43
45
  export type { ChatStreamEvent, ChatMessageMetrics } from './events';
44
46
  export type {
45
47
  CreateSessionOptions,
@@ -17,6 +17,13 @@ export interface ChatMessage {
17
17
  content: string;
18
18
  /** epoch ms — serializable. */
19
19
  createdAt: number;
20
+ /**
21
+ * Stable per-session transcript position, supplied by hosts whose backend
22
+ * keys messages by an absolute sequence (e.g. a monotonic `chat_history.seq`).
23
+ * The synthetic `id` is a UI key only; `seq` is the durable handle the host
24
+ * passes back to an authoritative edit/truncate API. Optional — engines that
25
+ * don't track a backend seq leave it undefined. */
26
+ seq?: number;
20
27
  isStreaming?: boolean;
21
28
  isError?: boolean;
22
29
  /** Bumps on edit so memo keys invalidate. */
@@ -104,8 +104,8 @@ const TOKEN_CLASS: Record<string, string> = {
104
104
  string: 'text-[#a31515] dark:text-emerald-400',
105
105
  number: 'text-[#098658] dark:text-sky-400',
106
106
  boolean: 'text-[#0550ae] dark:text-amber-400',
107
- null: 'text-neutral-500 dark:text-muted-foreground/60',
108
- punctuation: 'text-neutral-700 dark:text-muted-foreground',
107
+ null: 'text-muted-foreground/70 dark:text-muted-foreground/60',
108
+ punctuation: 'text-muted-foreground dark:text-muted-foreground',
109
109
  };
110
110
 
111
111
  function TokenSpan({
@@ -131,7 +131,7 @@ function HighlightMatch({ text, query }: { text: string; query: string }) {
131
131
  return (
132
132
  <>
133
133
  {text.slice(0, idx)}
134
- <mark className="rounded-sm bg-amber-200/60 px-0.5 text-inherit dark:bg-amber-500/30">
134
+ <mark className="rounded-sm bg-warning/30 px-0.5 text-inherit">
135
135
  {text.slice(idx, idx + query.length)}
136
136
  </mark>
137
137
  {text.slice(idx + query.length)}
@@ -189,7 +189,7 @@ function JsonNode({
189
189
 
190
190
  const rowClass = cn(
191
191
  'group flex items-center gap-0 py-px hover:bg-muted/40 rounded-sm',
192
- nodeMatches && 'bg-amber-100/40 dark:bg-amber-900/20',
192
+ nodeMatches && 'bg-warning/15',
193
193
  );
194
194
 
195
195
  const rowStyle: CSSProperties = { paddingLeft: `${depth * 20 + 8}px` };
@@ -264,7 +264,7 @@ function JsonNode({
264
264
  className={copyIconClass}
265
265
  >
266
266
  {pathCopied ? (
267
- <CopyPlus className="size-3 text-emerald-500" />
267
+ <CopyPlus className="size-3 text-success" />
268
268
  ) : (
269
269
  <CopyPlus className="size-3" />
270
270
  )}
@@ -317,7 +317,7 @@ function JsonNode({
317
317
  className={copyIconClass}
318
318
  >
319
319
  {pathCopied ? (
320
- <CopyPlus className="size-3 text-emerald-500" />
320
+ <CopyPlus className="size-3 text-success" />
321
321
  ) : (
322
322
  <CopyPlus className="size-3" />
323
323
  )}
@@ -489,7 +489,7 @@ function Toolbar({
489
489
  'copy',
490
490
  copiedAll ? 'Copied!' : 'Copy JSON',
491
491
  copiedAll ? (
492
- <Check className={cn(iconCls, 'text-emerald-500')} />
492
+ <Check className={cn(iconCls, 'text-success')} />
493
493
  ) : (
494
494
  <Copy className={iconCls} />
495
495
  ),
@@ -376,6 +376,7 @@ Defaults are English. Function-shaped entries (`confirmDeleteTitle(count)`, `con
376
376
  ```tsx
377
377
  <TreeRoot
378
378
  appearance={{
379
+ variant: 'explorer', // 'explorer' (default) | 'list'
379
380
  density: 'cozy', // 'compact' | 'cozy' | 'comfortable'
380
381
  accent: 'default', // 'subtle' | 'default' | 'strong'
381
382
  radius: 'sm', // 'none' | 'sm' | 'md'
@@ -387,6 +388,35 @@ Defaults are English. Function-shaped entries (`confirmDeleteTitle(count)`, `con
387
388
  />
388
389
  ```
389
390
 
391
+ ### Variant — `explorer` vs `list`
392
+
393
+ `variant` is a high-level look that sets sensible defaults for three
394
+ fields (`hideLeafIcons`, `hideFolderIcons`, `rowSizing`,
395
+ `showActiveIndicator`); any of those passed explicitly still wins.
396
+
397
+ | Variant | Look | Defaults |
398
+ | --- | --- | --- |
399
+ | `'explorer'` (default) | Classic file/folder tree | leaf + folder icons, **fixed** single-line rows, active bar on |
400
+ | `'list'` | macOS-sidebar / **chat-history** list | **no icons** (chevron only on groups), **auto-height** rows (multi-line label friendly), quiet selection (no active bar) |
401
+
402
+ ```tsx
403
+ // A grouped chat-history sidebar (date / topic buckets → sessions),
404
+ // multi-line rows (title + dimmed meta), no file glyphs:
405
+ <TreeRoot<Payload>
406
+ data={groupedSessions}
407
+ getItemName={(n) => label(n)}
408
+ appearance={{ variant: 'list', indent: 10 }}
409
+ showIndentGuides={false}
410
+ renderLabel={({ node }) => /* title + meta */ null}
411
+ />
412
+ ```
413
+
414
+ Fine-grained fields (override the variant):
415
+
416
+ - `hideLeafIcons` / `hideFolderIcons` — drop the per-row icon (chevron stays).
417
+ - `rowSizing: 'fixed' | 'auto'` — `auto` turns `rowHeight` into a *minimum*
418
+ so a multi-line label grows the row instead of overflowing.
419
+
390
420
  The resolved appearance is exposed on the root container as CSS variables:
391
421
 
392
422
  ```
@@ -175,7 +175,16 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
175
175
  tabIndex={-1}
176
176
  style={{
177
177
  paddingLeft: 6 + level * appearance.indent,
178
- height: 'var(--tree-row-height)',
178
+ // `auto` sizing lets a multi-line label (e.g. title + meta in a
179
+ // chat-history list) grow the row instead of overflowing a fixed
180
+ // height. `fixed` keeps the classic single-line explorer row.
181
+ ...(appearance.rowSizing === 'auto'
182
+ ? {
183
+ minHeight: 'var(--tree-row-height)',
184
+ paddingTop: 4,
185
+ paddingBottom: 4,
186
+ }
187
+ : { height: 'var(--tree-row-height)' }),
179
188
  gap: 'var(--tree-gap)',
180
189
  }}
181
190
  {...(dnd.active ? draggable.listeners : {})}
@@ -234,7 +243,7 @@ function TreeRowRaw<T>({ row, className }: TreeRowProps<T>) {
234
243
  />
235
244
  ) : renderIcon ? (
236
245
  renderIcon(slot)
237
- ) : (
246
+ ) : (isFolder ? appearance.hideFolderIcons : appearance.hideLeafIcons) ? null : (
238
247
  <TreeIcon isFolder={isFolder} isExpanded={isExpanded} />
239
248
  )}
240
249
 
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { useState } from 'react';
3
4
  import {
4
5
  ContextMenu,
5
6
  ContextMenuContent,
@@ -14,6 +15,60 @@ import type {
14
15
  TreeRowRenderProps,
15
16
  } from '../../types';
16
17
 
18
+ interface RowContextMenuProps<T> {
19
+ rowProps: TreeRowRenderProps<T>;
20
+ items: TreeContextMenuItem<T>[];
21
+ trigger: React.ReactNode;
22
+ }
23
+
24
+ /**
25
+ * Per-row context menu.
26
+ *
27
+ * PERF: every visible row wraps its trigger in a `<ContextMenu>`, so a
28
+ * large tree mounts hundreds of these. Building the full
29
+ * `<ContextMenuContent>` item subtree for each of them on every render is
30
+ * what made right-click feel laggy (the selection change on right-click
31
+ * re-renders all rows, each rebuilding its menu body). We now render the
32
+ * content body ONLY while this row's menu is actually open — closed rows
33
+ * cost just the lightweight `ContextMenu` + `Trigger` shell.
34
+ */
35
+ function RowContextMenu<T>({
36
+ rowProps,
37
+ items,
38
+ trigger,
39
+ }: RowContextMenuProps<T>) {
40
+ const [open, setOpen] = useState(false);
41
+ return (
42
+ <ContextMenu onOpenChange={setOpen}>
43
+ <ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
44
+ {open ? (
45
+ <ContextMenuContent>
46
+ {items.map((item, idx) => {
47
+ if (item === 'separator') {
48
+ return <ContextMenuSeparator key={`sep-${idx}`} />;
49
+ }
50
+ const Icon = item.icon;
51
+ return (
52
+ <ContextMenuItem
53
+ key={item.id}
54
+ disabled={item.disabled}
55
+ variant={item.destructive ? 'destructive' : undefined}
56
+ onSelect={() => item.onSelect(rowProps)}
57
+ >
58
+ {Icon ? <Icon /> : null}
59
+ {item.label}
60
+ {item.shortcut ? (
61
+ <ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
62
+ ) : null}
63
+ </ContextMenuItem>
64
+ );
65
+ })}
66
+ </ContextMenuContent>
67
+ ) : null}
68
+ </ContextMenu>
69
+ );
70
+ }
71
+
17
72
  /**
18
73
  * Render an array of declarative menu items as a themed `<ContextMenu>`
19
74
  * wrapped around the supplied trigger element. Pure presentational layer
@@ -24,33 +79,7 @@ export function renderItemsAsContextMenu<T>(
24
79
  items: TreeContextMenuItem<T>[],
25
80
  trigger: React.ReactNode,
26
81
  ): React.ReactNode {
27
- return (
28
- <ContextMenu>
29
- <ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
30
- <ContextMenuContent>
31
- {items.map((item, idx) => {
32
- if (item === 'separator') {
33
- return <ContextMenuSeparator key={`sep-${idx}`} />;
34
- }
35
- const Icon = item.icon;
36
- return (
37
- <ContextMenuItem
38
- key={item.id}
39
- disabled={item.disabled}
40
- variant={item.destructive ? 'destructive' : undefined}
41
- onSelect={() => item.onSelect(rowProps)}
42
- >
43
- {Icon ? <Icon /> : null}
44
- {item.label}
45
- {item.shortcut ? (
46
- <ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
47
- ) : null}
48
- </ContextMenuItem>
49
- );
50
- })}
51
- </ContextMenuContent>
52
- </ContextMenu>
53
- );
82
+ return <RowContextMenu rowProps={rowProps} items={items} trigger={trigger} />;
54
83
  }
55
84
 
56
85
  /**
@@ -6,6 +6,20 @@ export type TreeDensity = 'compact' | 'cozy' | 'comfortable';
6
6
  export type TreeAccentIntensity = 'subtle' | 'default' | 'strong';
7
7
  export type TreeRadius = 'none' | 'sm' | 'md';
8
8
 
9
+ /**
10
+ * High-level look. Sets sensible defaults for icons / row sizing / the
11
+ * active indicator; any individual `TreeAppearance` field still overrides.
12
+ *
13
+ * - `'explorer'` (default) — classic file/folder tree: leaf + folder
14
+ * icons, fixed single-line rows, VSCode active-bar.
15
+ * - `'list'` — macOS-sidebar / chat-history look: no icons (chevron
16
+ * only on groups), auto-height rows (multi-line label friendly), quiet
17
+ * selection (no active-bar).
18
+ */
19
+ export type TreeVariant = 'explorer' | 'list';
20
+
21
+ export type TreeRowSizing = 'fixed' | 'auto';
22
+
9
23
  /**
10
24
  * Cosmetic configuration. Every field is optional; missing values fall
11
25
  * back to the `cozy` preset (a comfortable VSCode-Explorer-like density).
@@ -38,6 +52,21 @@ export interface TreeAppearance {
38
52
  * Mimics the VSCode active-tab indicator. Default: `true`.
39
53
  */
40
54
  showActiveIndicator?: boolean;
55
+ /**
56
+ * High-level look. Default: `'explorer'`. Sets defaults for the three
57
+ * fields below; pass any of them explicitly to override the variant.
58
+ */
59
+ variant?: TreeVariant;
60
+ /** Hide per-leaf icons (chevron + label only). Variant default. */
61
+ hideLeafIcons?: boolean;
62
+ /** Hide folder icons too (chevron stays). Variant default. */
63
+ hideFolderIcons?: boolean;
64
+ /**
65
+ * `'fixed'` — every row is exactly `rowHeight` (single-line explorer).
66
+ * `'auto'` — `rowHeight` is a *minimum*; rows grow to fit a multi-line
67
+ * label (title + meta). Variant default.
68
+ */
69
+ rowSizing?: TreeRowSizing;
41
70
  }
42
71
 
43
72
  export interface ResolvedAppearance {
@@ -52,6 +81,9 @@ export interface ResolvedAppearance {
52
81
  radius: TreeRadius;
53
82
  indentGuideOpacity: number;
54
83
  showActiveIndicator: boolean;
84
+ hideLeafIcons: boolean;
85
+ hideFolderIcons: boolean;
86
+ rowSizing: TreeRowSizing;
55
87
  }
56
88
 
57
89
  const DENSITY_PRESETS: Record<
@@ -63,6 +95,31 @@ const DENSITY_PRESETS: Record<
63
95
  comfortable: { rowHeight: 32, iconSize: 16, fontSize: 14, gap: 8 },
64
96
  };
65
97
 
98
+ /**
99
+ * Per-variant defaults for the icon / sizing / indicator fields. The
100
+ * `'list'` variant is the macOS-sidebar / chat-history look.
101
+ */
102
+ const VARIANT_DEFAULTS: Record<
103
+ TreeVariant,
104
+ Pick<
105
+ ResolvedAppearance,
106
+ 'hideLeafIcons' | 'hideFolderIcons' | 'rowSizing' | 'showActiveIndicator'
107
+ >
108
+ > = {
109
+ explorer: {
110
+ hideLeafIcons: false,
111
+ hideFolderIcons: false,
112
+ rowSizing: 'fixed',
113
+ showActiveIndicator: true,
114
+ },
115
+ list: {
116
+ hideLeafIcons: true,
117
+ hideFolderIcons: true,
118
+ rowSizing: 'auto',
119
+ showActiveIndicator: false,
120
+ },
121
+ };
122
+
66
123
  export const DEFAULT_TREE_APPEARANCE: ResolvedAppearance = {
67
124
  density: 'cozy',
68
125
  ...DENSITY_PRESETS.cozy,
@@ -71,7 +128,7 @@ export const DEFAULT_TREE_APPEARANCE: ResolvedAppearance = {
71
128
  accent: 'default',
72
129
  radius: 'sm',
73
130
  indentGuideOpacity: 0.4,
74
- showActiveIndicator: true,
131
+ ...VARIANT_DEFAULTS.explorer,
75
132
  };
76
133
 
77
134
  /**
@@ -88,6 +145,9 @@ export function resolveAppearance(
88
145
 
89
146
  const density: TreeDensity = input?.density ?? 'cozy';
90
147
  const preset = DENSITY_PRESETS[density];
148
+ // Variant sets the icon/sizing/indicator defaults; explicit fields win.
149
+ const variant: TreeVariant = input?.variant ?? 'explorer';
150
+ const v = VARIANT_DEFAULTS[variant];
91
151
 
92
152
  return {
93
153
  density,
@@ -101,8 +161,10 @@ export function resolveAppearance(
101
161
  radius: input?.radius ?? DEFAULT_TREE_APPEARANCE.radius,
102
162
  indentGuideOpacity:
103
163
  input?.indentGuideOpacity ?? DEFAULT_TREE_APPEARANCE.indentGuideOpacity,
104
- showActiveIndicator:
105
- input?.showActiveIndicator ?? DEFAULT_TREE_APPEARANCE.showActiveIndicator,
164
+ showActiveIndicator: input?.showActiveIndicator ?? v.showActiveIndicator,
165
+ hideLeafIcons: input?.hideLeafIcons ?? v.hideLeafIcons,
166
+ hideFolderIcons: input?.hideFolderIcons ?? v.hideFolderIcons,
167
+ rowSizing: input?.rowSizing ?? v.rowSizing,
106
168
  };
107
169
  }
108
170
 
@@ -40,4 +40,6 @@ export type {
40
40
  TreeDensity,
41
41
  TreeAccentIntensity,
42
42
  TreeRadius,
43
+ TreeVariant,
44
+ TreeRowSizing,
43
45
  } from './appearance';
@@ -101,6 +101,8 @@ export type {
101
101
  TreeDensity,
102
102
  TreeAccentIntensity,
103
103
  TreeRadius,
104
+ TreeVariant,
105
+ TreeRowSizing,
104
106
  } from './data';
105
107
 
106
108
  export { DEFAULT_TREE_LABELS } from './types';