@getdom/studio 0.1.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.
Files changed (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,737 @@
1
+ <script>
2
+ /**
3
+ * @typedef {object} AgentChatAgent
4
+ * @property {string} id Stable agent id.
5
+ * @property {string} name Human-readable agent name.
6
+ * @property {string} [role] Agent role or specialty.
7
+ * @property {string} [status] Agent availability label.
8
+ * @property {string} [model] Model or runtime label.
9
+ * @property {string} [avatarUrl] Optional avatar image URL.
10
+ * @property {Array<string>} [tools] Tool names available to the agent.
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} AgentChatConversation
15
+ * @property {string} id Stable conversation id.
16
+ * @property {string} title Display title.
17
+ * @property {string} [summary] Short summary for the active thread.
18
+ * @property {string} [agentId] Active agent id.
19
+ * @property {'idle'|'streaming'|'waiting'|'error'|'complete'} [status] Conversation state.
20
+ * @property {'text'|'audio'} [mode] Preferred input mode.
21
+ * @property {string} [updatedAt] ISO timestamp.
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} AgentChatMessagePart
26
+ * @property {'text'|'reasoning'|'tool-call'|'tool-result'|'audio'|'handoff'} type Part renderer type.
27
+ * @property {string} [id] Stable part id.
28
+ * @property {string} [text] Text content for text and reasoning parts.
29
+ * @property {string} [name] Tool or handoff name.
30
+ * @property {string} [label] Human-readable part label.
31
+ * @property {'queued'|'running'|'complete'|'error'|'cancelled'} [status] Part status.
32
+ * @property {Record<string, unknown>} [args] Tool call arguments.
33
+ * @property {Record<string, unknown>} [result] Tool call result.
34
+ * @property {string} [rendererKey] Custom renderer lookup key.
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} AgentChatMessage
39
+ * @property {string} id Stable message id.
40
+ * @property {'user'|'assistant'|'system'|'tool'} role Message author role.
41
+ * @property {string} [agentId] Agent id for assistant-authored messages.
42
+ * @property {string} [authorName] Display author override.
43
+ * @property {string} [content] Plain text content.
44
+ * @property {Array<AgentChatMessagePart>} [parts] Ordered message parts.
45
+ * @property {'queued'|'streaming'|'complete'|'error'|'cancelled'} [status] Message state.
46
+ * @property {'text'|'audio'} [mode] Message modality.
47
+ * @property {string} [createdAt] ISO timestamp.
48
+ */
49
+
50
+ export const agentConversationProps = {
51
+ conversation: {
52
+ type: Object,
53
+ default: null,
54
+ _edit: { description: 'Active conversation record. Used for the header, status, and empty state context.' },
55
+ },
56
+ messages: {
57
+ type: Array,
58
+ default: () => [],
59
+ _edit: { description: 'Ordered conversation messages. Messages may contain text, reasoning, audio, handoff, and tool-call parts.' },
60
+ },
61
+ agents: {
62
+ type: Array,
63
+ default: () => [],
64
+ _edit: { description: 'Agents that can author assistant messages or be displayed in the header.' },
65
+ },
66
+ modelValue: {
67
+ type: String,
68
+ default: '',
69
+ _edit: { description: 'Controlled composer text.' },
70
+ },
71
+ mode: {
72
+ type: String,
73
+ default: 'text',
74
+ validator: (value) => ['text', 'audio'].includes(value),
75
+ _edit: { options: ['text', 'audio'], description: 'Active composer modality.' },
76
+ },
77
+ streaming: {
78
+ type: Boolean,
79
+ default: false,
80
+ _edit: { description: 'Show the stop control and live status while the assistant response streams.' },
81
+ },
82
+ loading: {
83
+ type: Boolean,
84
+ default: false,
85
+ _edit: { description: 'Show a loading state before conversation messages are available.' },
86
+ },
87
+ autoScroll: {
88
+ type: Boolean,
89
+ default: true,
90
+ _edit: { description: 'Keep the scroller pinned to the bottom while new content arrives.' },
91
+ },
92
+ showHeader: {
93
+ type: Boolean,
94
+ default: true,
95
+ _edit: { description: 'Render the built-in conversation header.' },
96
+ },
97
+ showFooter: {
98
+ type: Boolean,
99
+ default: true,
100
+ _edit: { description: 'Render the built-in composer footer.' },
101
+ },
102
+ showReasoning: {
103
+ type: Boolean,
104
+ default: true,
105
+ _edit: { description: 'Render reasoning summary parts when the supplied message data includes them.' },
106
+ },
107
+ placeholder: {
108
+ type: String,
109
+ default: 'Ask the agent to plan, inspect, or create...',
110
+ _edit: { description: 'Composer placeholder text.' },
111
+ },
112
+ emptyText: {
113
+ type: String,
114
+ default: 'Select a conversation or start a new one.',
115
+ _edit: { description: 'Message shown when there are no messages.' },
116
+ },
117
+ disabled: {
118
+ type: Boolean,
119
+ default: false,
120
+ _edit: { description: 'Disable composer input and send controls.' },
121
+ },
122
+ toolComponents: {
123
+ type: Object,
124
+ default: () => ({}),
125
+ _edit: { description: 'Component map keyed by tool rendererKey or tool name. Falls back to the generic tool card.' },
126
+ },
127
+ maxMessageWidth: {
128
+ type: String,
129
+ default: '48rem',
130
+ _edit: { description: 'CSS max-width applied to message bubbles.' },
131
+ },
132
+ };
133
+ </script>
134
+
135
+ <script setup>
136
+ import { computed, nextTick, ref, useId, watch } from 'vue';
137
+ import DomIconButton from '../../components/icon-button/DomIconButton.vue';
138
+ import DomTextareaComposer from '../../forms/textarea-composer/DomTextareaComposer.vue';
139
+ import DomStatusPill from '../../visual/status-pill/DomStatusPill.vue';
140
+
141
+ defineOptions({
142
+ __doc: {
143
+ name: 'Agent conversation',
144
+ tag: '<DomAgentConversation>',
145
+ description: 'A scroll-contained AI conversation surface with a fixed header, streaming messages, reasoning summaries, audio/text mode switching, tool-call rendering, and a composer footer.',
146
+ icon: 'M5 7h14M5 12h10M5 17h6M18 15l3 3-3 3',
147
+ events: [
148
+ { name: 'update:modelValue', payload: 'string', description: 'Fired when the composer text changes.' },
149
+ { name: 'update:mode', payload: '\'text\' | \'audio\'', description: 'Fired when the composer switches between text and audio mode.' },
150
+ { name: 'submit', payload: '({ content, mode, conversation })', description: 'Fired when the user submits the composer.' },
151
+ { name: 'stop', payload: '({ conversation })', description: 'Fired when the user stops an active stream.' },
152
+ { name: 'tool-action', payload: '({ action, tool, message })', description: 'Fired by generic and custom tool renderers for approvals, retries, and inspection.' },
153
+ { name: 'scroll-bottom', payload: '({ conversation })', description: 'Fired when the built-in jump-to-bottom control is used.' },
154
+ ],
155
+ slots: [
156
+ { name: 'header-leading', payload: '{ conversation, activeAgent }', description: 'Controls shown before the title, useful for opening an app sidebar.' },
157
+ { name: 'header-actions', payload: '{ conversation, activeAgent, streaming }', description: 'Controls shown at the end of the header.' },
158
+ { name: 'message', payload: '{ message, activeAgent, parts }', description: 'Replace the full message renderer.' },
159
+ { name: 'tool', payload: '{ tool, message, component, action }', description: 'Replace generic tool rendering without supplying toolComponents.' },
160
+ { name: 'composer-attachments', payload: '{ mode, disabled, streaming }', description: 'Application-owned file, image, or context previews above the composer text.' },
161
+ { name: 'composer-leading', payload: '{ mode, setMode, disabled, streaming }', description: 'Controls at the start of the composer action row.' },
162
+ { name: 'composer-actions', payload: '{ mode, setMode, disabled, streaming }', description: 'Controls before the built-in submit or stop action.' },
163
+ { name: 'footer', payload: '{ value, mode, submit, setMode }', description: 'Replace the built-in composer footer.' },
164
+ { name: 'empty', description: 'Custom empty conversation state.' },
165
+ ],
166
+ keyboard: [
167
+ { keys: 'Enter', action: 'Submit the composer.' },
168
+ { keys: 'Shift + Enter', action: 'Insert a new line.' },
169
+ ],
170
+ },
171
+ });
172
+
173
+ const props = defineProps(agentConversationProps);
174
+ const emit = defineEmits([
175
+ 'update:modelValue',
176
+ 'update:mode',
177
+ 'submit',
178
+ 'stop',
179
+ 'tool-action',
180
+ 'scroll-bottom',
181
+ ]);
182
+
183
+ const scroller = ref(null);
184
+ const isPinnedToBottom = ref(true);
185
+ const titleId = useId();
186
+
187
+ const activeAgent = computed(() => findAgentById(props.agents, props.conversation?.agentId));
188
+ const conversationTitle = computed(() => props.conversation?.title || activeAgent.value?.name || 'Agent conversation');
189
+ const conversationSummary = computed(() => props.conversation?.summary || activeAgent.value?.role || 'Ask, inspect, and continue the work in one thread.');
190
+ const statusTone = computed(() => statusToneFor(props.streaming ? 'streaming' : props.conversation?.status));
191
+ const visibleMessages = computed(() => props.messages.filter(isVisibleMessage));
192
+ const canSubmit = computed(() => !props.disabled && !props.streaming && props.modelValue.trim().length > 0);
193
+ const composerPlaceholder = computed(() => props.mode === 'audio' ? 'Describe what you want to say, or connect this to speech input...' : props.placeholder);
194
+ const messageWidthStyle = computed(() => ({ maxWidth: props.maxMessageWidth }));
195
+ const shouldShowJumpButton = computed(() => !isPinnedToBottom.value && visibleMessages.value.length > 2);
196
+
197
+ watch(() => props.conversation?.id, async () => {
198
+ await nextTick();
199
+ scrollToBottom('auto');
200
+ });
201
+
202
+ watch(() => [visibleMessages.value.length, lastMessageSignature(visibleMessages.value)], async () => {
203
+ if (!props.autoScroll || !isPinnedToBottom.value) return;
204
+ await nextTick();
205
+ scrollToBottom('smooth');
206
+ });
207
+
208
+ /**
209
+ * Emits composer changes from the shared textarea composer.
210
+ *
211
+ * @param {string} value Controlled composer value.
212
+ * @returns {void}
213
+ */
214
+ function updateComposer(value) {
215
+ emit('update:modelValue', value);
216
+ }
217
+
218
+ /**
219
+ * Emits the current composer content if the component can submit.
220
+ *
221
+ * @returns {void}
222
+ */
223
+ function submitComposer() {
224
+ const content = props.modelValue.trim();
225
+ if (!content || !canSubmit.value) return;
226
+ emit('submit', {
227
+ content,
228
+ mode: props.mode,
229
+ conversation: props.conversation,
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Emits a stream stop request for the active conversation.
235
+ *
236
+ * @returns {void}
237
+ */
238
+ function stopStreaming() {
239
+ emit('stop', { conversation: props.conversation });
240
+ }
241
+
242
+ /**
243
+ * Changes the active input mode.
244
+ *
245
+ * @param {'text'|'audio'} mode Input mode.
246
+ * @returns {void}
247
+ */
248
+ function setMode(mode) {
249
+ emit('update:mode', mode === 'audio' ? 'audio' : 'text');
250
+ }
251
+
252
+ /**
253
+ * Tracks whether the message list is close enough to the bottom for autoscroll.
254
+ *
255
+ * @returns {void}
256
+ */
257
+ function updateScrollState() {
258
+ const element = scroller.value;
259
+ if (!element) return;
260
+ isPinnedToBottom.value = element.scrollHeight - element.scrollTop - element.clientHeight < 96;
261
+ }
262
+
263
+ /**
264
+ * Scrolls the message viewport to the latest message.
265
+ *
266
+ * @param {'auto'|'smooth'} behavior Browser scroll behavior.
267
+ * @returns {void}
268
+ */
269
+ function scrollToBottom(behavior = 'smooth') {
270
+ const element = scroller.value;
271
+ if (!element) return;
272
+ element.scrollTo({ top: element.scrollHeight, behavior });
273
+ isPinnedToBottom.value = true;
274
+ emit('scroll-bottom', { conversation: props.conversation });
275
+ }
276
+
277
+ /**
278
+ * Emits a generic action from a tool renderer.
279
+ *
280
+ * @param {string} action Action name.
281
+ * @param {AgentChatMessagePart} tool Tool part.
282
+ * @param {AgentChatMessage} message Parent message.
283
+ * @returns {void}
284
+ */
285
+ function emitToolAction(action, tool, message) {
286
+ emit('tool-action', { action, tool, message });
287
+ }
288
+
289
+ /**
290
+ * Returns the ordered parts for a message, falling back to plain text content.
291
+ *
292
+ * @param {AgentChatMessage} message Message record.
293
+ * @returns {Array<AgentChatMessagePart>} Normalized parts.
294
+ */
295
+ function partsForMessage(message) {
296
+ if (Array.isArray(message.parts) && message.parts.length) return message.parts;
297
+ if (!message.content) return [];
298
+ return [{ type: 'text', text: message.content }];
299
+ }
300
+
301
+ /**
302
+ * Returns text paragraphs that preserve blank-line breaks without HTML injection.
303
+ *
304
+ * @param {string} text Text content.
305
+ * @returns {Array<string>} Paragraph strings.
306
+ */
307
+ function textParagraphs(text) {
308
+ return String(text || '')
309
+ .split(/\n{2,}/)
310
+ .map(trimText)
311
+ .filter(Boolean);
312
+ }
313
+
314
+ /**
315
+ * Resolves the Vue component for a tool part.
316
+ *
317
+ * @param {AgentChatMessagePart} tool Tool part.
318
+ * @returns {object|Function|string|null} Custom renderer component.
319
+ */
320
+ function toolComponentFor(tool) {
321
+ if (!tool || typeof tool !== 'object') return null;
322
+ return props.toolComponents[tool.rendererKey] || props.toolComponents[tool.name] || null;
323
+ }
324
+
325
+ /**
326
+ * Returns a compact JSON string for tool arguments or results.
327
+ *
328
+ * @param {unknown} value Value to serialize.
329
+ * @returns {string} Pretty JSON.
330
+ */
331
+ function formatJson(value) {
332
+ if (value == null) return '{}';
333
+ try {
334
+ return JSON.stringify(value, null, '\t');
335
+ } catch {
336
+ return String(value);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Resolves the visible author name for a message.
342
+ *
343
+ * @param {AgentChatMessage} message Message record.
344
+ * @returns {string} Display name.
345
+ */
346
+ function authorName(message) {
347
+ if (message.authorName) return message.authorName;
348
+ if (message.role === 'user') return 'You';
349
+ if (message.role === 'system') return 'System';
350
+ return findAgentById(props.agents, message.agentId)?.name || activeAgent.value?.name || 'Assistant';
351
+ }
352
+
353
+ /**
354
+ * Resolves the short avatar label for a message.
355
+ *
356
+ * @param {AgentChatMessage} message Message record.
357
+ * @returns {string} Avatar initials.
358
+ */
359
+ function avatarLabel(message) {
360
+ return authorName(message)
361
+ .split(/\s+/)
362
+ .filter(Boolean)
363
+ .slice(0, 2)
364
+ .map(firstCharacter)
365
+ .join('')
366
+ .toUpperCase() || 'AI';
367
+ }
368
+
369
+ /**
370
+ * Resolves the timestamp label for a message.
371
+ *
372
+ * @param {AgentChatMessage} message Message record.
373
+ * @returns {string} Locale time label.
374
+ */
375
+ function messageTime(message) {
376
+ if (!message.createdAt) return '';
377
+ const date = new Date(message.createdAt);
378
+ if (Number.isNaN(date.getTime())) return '';
379
+ return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date);
380
+ }
381
+
382
+ /**
383
+ * Checks whether a message should align as user-authored content.
384
+ *
385
+ * @param {AgentChatMessage} message Message record.
386
+ * @returns {boolean} True for user messages.
387
+ */
388
+ function isUserMessage(message) {
389
+ return message.role === 'user';
390
+ }
391
+
392
+ /**
393
+ * Checks whether a message contains a streaming status.
394
+ *
395
+ * @param {AgentChatMessage} message Message record.
396
+ * @returns {boolean} True while the message is streaming.
397
+ */
398
+ function isStreamingMessage(message) {
399
+ return message.status === 'streaming';
400
+ }
401
+
402
+ /**
403
+ * Returns accessible text for a tool status.
404
+ *
405
+ * @param {string} status Tool status.
406
+ * @returns {string} Status label.
407
+ */
408
+ function toolStatusLabel(status) {
409
+ return {
410
+ queued: 'Queued',
411
+ running: 'Running',
412
+ complete: 'Complete',
413
+ error: 'Error',
414
+ cancelled: 'Cancelled',
415
+ }[status] || 'Ready';
416
+ }
417
+
418
+ /**
419
+ * Returns status indicator classes for a tool card.
420
+ *
421
+ * @param {string} status Tool status.
422
+ * @returns {string} Token-based class list.
423
+ */
424
+ function toolStatusClass(status) {
425
+ return {
426
+ queued: 'border-border bg-secondary text-muted-fg',
427
+ running: 'border-primary/35 bg-primary/10 text-primary',
428
+ complete: 'border-success/35 bg-success/10 text-success',
429
+ error: 'border-destructive/35 bg-destructive/10 text-destructive',
430
+ cancelled: 'border-border bg-muted text-muted-fg',
431
+ }[status] || 'border-border bg-secondary text-muted-fg';
432
+ }
433
+
434
+ /**
435
+ * Finds an agent by id from the supplied agent collection.
436
+ *
437
+ * @param {Array<AgentChatAgent>} agents Agent collection.
438
+ * @param {string} id Agent id.
439
+ * @returns {AgentChatAgent|null} Matching agent.
440
+ */
441
+ function findAgentById(agents, id) {
442
+ return agents.find((agent) => agent.id === id) || null;
443
+ }
444
+
445
+ /**
446
+ * Checks whether a message should be rendered.
447
+ *
448
+ * @param {AgentChatMessage} message Message record.
449
+ * @returns {boolean} True when the message has an id and visible role.
450
+ */
451
+ function isVisibleMessage(message) {
452
+ return Boolean(message?.id && ['user', 'assistant', 'system', 'tool'].includes(message.role));
453
+ }
454
+
455
+ /**
456
+ * Returns a stable signature for the last message content.
457
+ *
458
+ * @param {Array<AgentChatMessage>} messages Message collection.
459
+ * @returns {string} Last message signature.
460
+ */
461
+ function lastMessageSignature(messages) {
462
+ const message = messages[messages.length - 1];
463
+ if (!message) return '';
464
+ return `${message.id}:${message.status}:${JSON.stringify(message.parts || message.content || '')}`;
465
+ }
466
+
467
+ /**
468
+ * Resolves a status tone for the header status pill.
469
+ *
470
+ * @param {string} status Conversation status.
471
+ * @returns {'neutral'|'info'|'success'|'warning'|'danger'} Status pill tone.
472
+ */
473
+ function statusToneFor(status) {
474
+ return {
475
+ streaming: 'info',
476
+ waiting: 'warning',
477
+ error: 'danger',
478
+ complete: 'success',
479
+ }[status] || 'neutral';
480
+ }
481
+
482
+ /**
483
+ * Trims text while preserving a function reference for array mapping.
484
+ *
485
+ * @param {string} value Source text.
486
+ * @returns {string} Trimmed text.
487
+ */
488
+ function trimText(value) {
489
+ return String(value || '').trim();
490
+ }
491
+
492
+ /**
493
+ * Returns the first character from a word.
494
+ *
495
+ * @param {string} value Source word.
496
+ * @returns {string} First character.
497
+ */
498
+ function firstCharacter(value) {
499
+ return value.charAt(0);
500
+ }
501
+ </script>
502
+
503
+ <template>
504
+ <section
505
+ class="flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-canvas text-canvas-fg shadow-sm"
506
+ :aria-labelledby="titleId"
507
+ >
508
+ <header v-if="showHeader" class="flex min-h-16 shrink-0 items-center gap-3 border-b border-border bg-secondary/25 px-3 py-2 sm:px-4">
509
+ <slot name="header-leading" :conversation="conversation" :active-agent="activeAgent" />
510
+ <div class="min-w-0 flex-1">
511
+ <div class="flex min-w-0 items-center gap-2">
512
+ <h2 :id="titleId" class="truncate text-sm font-semibold text-canvas-fg">{{ conversationTitle }}</h2>
513
+ <DomStatusPill class="hidden sm:inline-flex" size="sm" :tone="statusTone">{{ streaming ? 'Streaming' : (conversation?.status || 'Idle') }}</DomStatusPill>
514
+ </div>
515
+ <p class="mt-0.5 truncate text-xs text-muted-fg">{{ conversationSummary }}</p>
516
+ </div>
517
+ <div class="flex shrink-0 items-center gap-1">
518
+ <button
519
+ type="button"
520
+ class="rounded-full px-2.5 py-1 text-xs font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
521
+ :class="mode === 'text' ? 'bg-primary text-primary-fg' : 'text-muted-fg hover:bg-secondary hover:text-canvas-fg'"
522
+ :aria-pressed="mode === 'text'"
523
+ @click="setMode('text')"
524
+ >
525
+ Text
526
+ </button>
527
+ <button
528
+ type="button"
529
+ class="rounded-full px-2.5 py-1 text-xs font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
530
+ :class="mode === 'audio' ? 'bg-primary text-primary-fg' : 'text-muted-fg hover:bg-secondary hover:text-canvas-fg'"
531
+ :aria-pressed="mode === 'audio'"
532
+ @click="setMode('audio')"
533
+ >
534
+ Audio
535
+ </button>
536
+ <slot name="header-actions" :conversation="conversation" :active-agent="activeAgent" :streaming="streaming" />
537
+ </div>
538
+ </header>
539
+
540
+ <div
541
+ ref="scroller"
542
+ class="min-h-0 flex-1 overflow-y-auto px-3 py-4 sm:px-5"
543
+ @scroll.passive="updateScrollState"
544
+ >
545
+ <div v-if="loading" class="mx-auto flex min-h-full max-w-3xl flex-col justify-end gap-3">
546
+ <div v-for="index in 3" :key="index" class="h-24 animate-pulse rounded-lg border border-border bg-secondary/35"></div>
547
+ </div>
548
+
549
+ <slot v-else-if="!visibleMessages.length" name="empty">
550
+ <div class="flex min-h-full items-center justify-center">
551
+ <div class="max-w-sm text-center">
552
+ <div class="mx-auto grid size-12 place-items-center rounded-full bg-secondary text-muted-fg">
553
+ <svg viewBox="0 0 24 24" class="size-5" fill="none" aria-hidden="true">
554
+ <path d="M5 7h14M5 12h10M5 17h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
555
+ </svg>
556
+ </div>
557
+ <p class="mt-3 text-sm font-semibold text-canvas-fg">No messages yet</p>
558
+ <p class="mt-1 text-sm leading-6 text-muted-fg">{{ emptyText }}</p>
559
+ </div>
560
+ </div>
561
+ </slot>
562
+
563
+ <div v-else class="mx-auto flex max-w-5xl flex-col gap-5">
564
+ <article
565
+ v-for="message in visibleMessages"
566
+ :key="message.id"
567
+ class="flex min-w-0 gap-3"
568
+ :class="isUserMessage(message) ? 'justify-end' : 'justify-start'"
569
+ >
570
+ <div v-if="!isUserMessage(message)" class="mt-1 hidden size-8 shrink-0 place-items-center rounded-full border border-border bg-secondary text-xs font-semibold text-muted-fg sm:grid">
571
+ {{ avatarLabel(message) }}
572
+ </div>
573
+
574
+ <slot name="message" :message="message" :active-agent="activeAgent" :parts="partsForMessage(message)">
575
+ <div
576
+ class="min-w-0"
577
+ :class="isUserMessage(message) ? 'flex justify-end' : 'flex justify-start'"
578
+ :style="messageWidthStyle"
579
+ >
580
+ <div
581
+ class="min-w-0 rounded-lg border px-4 py-3 text-sm leading-6 shadow-sm"
582
+ :class="isUserMessage(message)
583
+ ? 'border-primary/20 bg-primary text-primary-fg'
584
+ : 'border-border bg-canvas'"
585
+ >
586
+ <div class="mb-2 flex items-center justify-between gap-3 text-xs" :class="isUserMessage(message) ? 'text-primary-fg/80' : 'text-muted-fg'">
587
+ <span class="font-semibold">{{ authorName(message) }}</span>
588
+ <span v-if="messageTime(message)">{{ messageTime(message) }}</span>
589
+ </div>
590
+
591
+ <div class="space-y-3">
592
+ <template v-for="(part, index) in partsForMessage(message)" :key="part.id || `${message.id}-${index}`">
593
+ <div v-if="part.type === 'text'" class="space-y-2">
594
+ <p v-for="paragraph in textParagraphs(part.text)" :key="paragraph" class="whitespace-pre-wrap">{{ paragraph }}</p>
595
+ </div>
596
+
597
+ <details
598
+ v-else-if="part.type === 'reasoning' && showReasoning"
599
+ class="rounded-md border border-border bg-secondary/35 px-3 py-2"
600
+ :open="part.open"
601
+ >
602
+ <summary class="cursor-pointer text-xs font-semibold text-muted-fg">Reasoning summary</summary>
603
+ <p class="mt-2 whitespace-pre-wrap text-xs leading-5 text-muted-fg">{{ part.text }}</p>
604
+ </details>
605
+
606
+ <div v-else-if="part.type === 'audio'" class="rounded-md border border-border bg-secondary/35 p-3">
607
+ <div class="flex items-center gap-3">
608
+ <div class="grid size-8 place-items-center rounded-full bg-primary/10 text-primary">
609
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
610
+ <path d="M12 5v14M7 9v6M17 9v6M4 11v2M20 11v2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
611
+ </svg>
612
+ </div>
613
+ <div class="min-w-0 flex-1">
614
+ <p class="text-xs font-semibold text-canvas-fg">{{ part.label || 'Audio message' }}</p>
615
+ <div class="mt-2 flex h-6 items-end gap-1" aria-hidden="true">
616
+ <span v-for="bar in 18" :key="bar" class="w-1 rounded-full bg-primary/45" :style="{ height: `${8 + ((bar * 7) % 18)}px` }"></span>
617
+ </div>
618
+ </div>
619
+ </div>
620
+ <p v-if="part.text" class="mt-2 text-xs leading-5 text-muted-fg">{{ part.text }}</p>
621
+ </div>
622
+
623
+ <div v-else-if="part.type === 'tool-call' || part.type === 'tool-result'" class="rounded-md border border-border bg-secondary/25 p-3">
624
+ <slot
625
+ name="tool"
626
+ :tool="part"
627
+ :message="message"
628
+ :component="toolComponentFor(part)"
629
+ :action="(action) => emitToolAction(action, part, message)"
630
+ >
631
+ <component
632
+ :is="toolComponentFor(part)"
633
+ v-if="toolComponentFor(part)"
634
+ :tool="part"
635
+ :message="message"
636
+ :result="part.result"
637
+ @action="emitToolAction($event, part, message)"
638
+ />
639
+ <div v-else class="space-y-3">
640
+ <div class="flex items-start justify-between gap-3">
641
+ <div class="min-w-0">
642
+ <p class="truncate text-sm font-semibold text-canvas-fg">{{ part.label || part.name || 'Tool call' }}</p>
643
+ <p class="mt-0.5 text-xs text-muted-fg">{{ part.name || 'tool' }}</p>
644
+ </div>
645
+ <span class="shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-medium" :class="toolStatusClass(part.status)">
646
+ {{ toolStatusLabel(part.status) }}
647
+ </span>
648
+ </div>
649
+ <div class="grid gap-3 md:grid-cols-2">
650
+ <div>
651
+ <p class="mb-1 text-[11px] font-semibold uppercase text-muted-fg">Arguments</p>
652
+ <pre class="max-h-40 overflow-auto rounded-md border border-border bg-canvas p-2 text-xs leading-5 text-muted-fg">{{ formatJson(part.args) }}</pre>
653
+ </div>
654
+ <div>
655
+ <p class="mb-1 text-[11px] font-semibold uppercase text-muted-fg">Result</p>
656
+ <pre class="max-h-40 overflow-auto rounded-md border border-border bg-canvas p-2 text-xs leading-5 text-muted-fg">{{ formatJson(part.result) }}</pre>
657
+ </div>
658
+ </div>
659
+ </div>
660
+ </slot>
661
+ </div>
662
+
663
+ <div v-else-if="part.type === 'handoff'" class="rounded-md border border-warning/35 bg-warning/10 p-3 text-warning">
664
+ <p class="text-xs font-semibold">{{ part.label || 'Agent handoff' }}</p>
665
+ <p class="mt-1 text-xs leading-5">{{ part.text }}</p>
666
+ </div>
667
+ </template>
668
+
669
+ <div v-if="isStreamingMessage(message)" class="flex items-center gap-1.5 pt-1 text-muted-fg" aria-label="Assistant is typing">
670
+ <span class="size-1.5 animate-pulse rounded-full bg-current"></span>
671
+ <span class="size-1.5 animate-pulse rounded-full bg-current [animation-delay:120ms]"></span>
672
+ <span class="size-1.5 animate-pulse rounded-full bg-current [animation-delay:240ms]"></span>
673
+ </div>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </slot>
678
+
679
+ <div v-if="isUserMessage(message)" class="mt-1 hidden size-8 shrink-0 place-items-center rounded-full border border-primary/25 bg-primary/10 text-xs font-semibold text-primary sm:grid">
680
+ {{ avatarLabel(message) }}
681
+ </div>
682
+ </article>
683
+ </div>
684
+ </div>
685
+
686
+ <div v-if="shouldShowJumpButton" class="pointer-events-none relative">
687
+ <DomIconButton
688
+ class="pointer-events-auto absolute bottom-3 left-1/2 z-10 -translate-x-1/2 shadow-md"
689
+ label="Jump to latest message"
690
+ size="sm"
691
+ variant="secondary"
692
+ icon="M12 5v14M18 13l-6 6-6-6"
693
+ @click="scrollToBottom('smooth')"
694
+ />
695
+ </div>
696
+
697
+ <footer v-if="showFooter" class="shrink-0 border-t border-border bg-canvas p-3">
698
+ <slot name="footer" :value="modelValue" :mode="mode" :submit="submitComposer" :set-mode="setMode">
699
+ <div>
700
+ <DomTextareaComposer
701
+ :model-value="modelValue"
702
+ :placeholder="composerPlaceholder"
703
+ :disabled="disabled || streaming"
704
+ :submit-disabled="!canSubmit"
705
+ :show-submit="!streaming"
706
+ :rows="2"
707
+ :max-rows="8"
708
+ :chrome="false"
709
+ @update:model-value="updateComposer"
710
+ @submit="submitComposer"
711
+ >
712
+ <template v-if="$slots['composer-attachments']" #attachments>
713
+ <slot name="composer-attachments" :mode="mode" :disabled="disabled" :streaming="streaming" />
714
+ </template>
715
+ <template v-if="$slots['composer-leading']" #leading>
716
+ <slot name="composer-leading" :mode="mode" :set-mode="setMode" :disabled="disabled" :streaming="streaming" />
717
+ </template>
718
+ <template #actions>
719
+ <slot name="composer-actions" :mode="mode" :set-mode="setMode" :disabled="disabled" :streaming="streaming" />
720
+ <DomIconButton
721
+ v-if="streaming"
722
+ label="Stop streaming"
723
+ variant="danger"
724
+ icon="M8 8h8v8H8z"
725
+ @click="stopStreaming"
726
+ />
727
+ </template>
728
+ </DomTextareaComposer>
729
+ <div class="mt-2 flex items-center justify-between gap-2 px-1 text-[11px] text-muted-fg">
730
+ <span>{{ mode === 'audio' ? 'Audio handoff ready' : 'Enter to send, Shift + Enter for a new line' }}</span>
731
+ <span v-if="activeAgent">{{ activeAgent.model }}</span>
732
+ </div>
733
+ </div>
734
+ </slot>
735
+ </footer>
736
+ </section>
737
+ </template>