@tetherto/mdk-ui-agent 0.0.0 → 0.12.1

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 (217) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +310 -0
  3. package/dist/branding.d.ts +36 -0
  4. package/dist/branding.d.ts.map +1 -0
  5. package/dist/branding.js +37 -0
  6. package/dist/branding.js.map +1 -0
  7. package/dist/components/approval-card.d.ts +46 -0
  8. package/dist/components/approval-card.d.ts.map +1 -0
  9. package/dist/components/approval-card.js +84 -0
  10. package/dist/components/approval-card.js.map +1 -0
  11. package/dist/components/assistant-message.d.ts +25 -0
  12. package/dist/components/assistant-message.d.ts.map +1 -0
  13. package/dist/components/assistant-message.js +49 -0
  14. package/dist/components/assistant-message.js.map +1 -0
  15. package/dist/components/co-pilot-panel.d.ts +17 -0
  16. package/dist/components/co-pilot-panel.d.ts.map +1 -0
  17. package/dist/components/co-pilot-panel.js +33 -0
  18. package/dist/components/co-pilot-panel.js.map +1 -0
  19. package/dist/components/code-block.d.ts +17 -0
  20. package/dist/components/code-block.d.ts.map +1 -0
  21. package/dist/components/code-block.js +40 -0
  22. package/dist/components/code-block.js.map +1 -0
  23. package/dist/components/collapsible-answer.d.ts +26 -0
  24. package/dist/components/collapsible-answer.d.ts.map +1 -0
  25. package/dist/components/collapsible-answer.js +30 -0
  26. package/dist/components/collapsible-answer.js.map +1 -0
  27. package/dist/components/composer.d.ts +31 -0
  28. package/dist/components/composer.d.ts.map +1 -0
  29. package/dist/components/composer.js +82 -0
  30. package/dist/components/composer.js.map +1 -0
  31. package/dist/components/conversation-list.d.ts +21 -0
  32. package/dist/components/conversation-list.d.ts.map +1 -0
  33. package/dist/components/conversation-list.js +42 -0
  34. package/dist/components/conversation-list.js.map +1 -0
  35. package/dist/components/icons.d.ts +88 -0
  36. package/dist/components/icons.d.ts.map +1 -0
  37. package/dist/components/icons.js +112 -0
  38. package/dist/components/icons.js.map +1 -0
  39. package/dist/components/launcher-fab.d.ts +8 -0
  40. package/dist/components/launcher-fab.d.ts.map +1 -0
  41. package/dist/components/launcher-fab.js +6 -0
  42. package/dist/components/launcher-fab.js.map +1 -0
  43. package/dist/components/leaked-tool-call-notice.d.ts +18 -0
  44. package/dist/components/leaked-tool-call-notice.d.ts.map +1 -0
  45. package/dist/components/leaked-tool-call-notice.js +14 -0
  46. package/dist/components/leaked-tool-call-notice.js.map +1 -0
  47. package/dist/components/markdown-link.d.ts +17 -0
  48. package/dist/components/markdown-link.d.ts.map +1 -0
  49. package/dist/components/markdown-link.js +18 -0
  50. package/dist/components/markdown-link.js.map +1 -0
  51. package/dist/components/markdown-text.d.ts +26 -0
  52. package/dist/components/markdown-text.d.ts.map +1 -0
  53. package/dist/components/markdown-text.js +32 -0
  54. package/dist/components/markdown-text.js.map +1 -0
  55. package/dist/components/message-list.d.ts +24 -0
  56. package/dist/components/message-list.d.ts.map +1 -0
  57. package/dist/components/message-list.js +92 -0
  58. package/dist/components/message-list.js.map +1 -0
  59. package/dist/components/no-tools-note.d.ts +26 -0
  60. package/dist/components/no-tools-note.d.ts.map +1 -0
  61. package/dist/components/no-tools-note.js +27 -0
  62. package/dist/components/no-tools-note.js.map +1 -0
  63. package/dist/components/panel-header.d.ts +26 -0
  64. package/dist/components/panel-header.d.ts.map +1 -0
  65. package/dist/components/panel-header.js +13 -0
  66. package/dist/components/panel-header.js.map +1 -0
  67. package/dist/components/rehype-highlight-subset.d.ts +17 -0
  68. package/dist/components/rehype-highlight-subset.d.ts.map +1 -0
  69. package/dist/components/rehype-highlight-subset.js +77 -0
  70. package/dist/components/rehype-highlight-subset.js.map +1 -0
  71. package/dist/components/system-message.d.ts +15 -0
  72. package/dist/components/system-message.d.ts.map +1 -0
  73. package/dist/components/system-message.js +12 -0
  74. package/dist/components/system-message.js.map +1 -0
  75. package/dist/components/tool-chip.d.ts +19 -0
  76. package/dist/components/tool-chip.d.ts.map +1 -0
  77. package/dist/components/tool-chip.js +32 -0
  78. package/dist/components/tool-chip.js.map +1 -0
  79. package/dist/components/user-message.d.ts +9 -0
  80. package/dist/components/user-message.d.ts.map +1 -0
  81. package/dist/components/user-message.js +7 -0
  82. package/dist/components/user-message.js.map +1 -0
  83. package/dist/core/conversation-store.d.ts +107 -0
  84. package/dist/core/conversation-store.d.ts.map +1 -0
  85. package/dist/core/conversation-store.js +254 -0
  86. package/dist/core/conversation-store.js.map +1 -0
  87. package/dist/core/error-text.d.ts +19 -0
  88. package/dist/core/error-text.d.ts.map +1 -0
  89. package/dist/core/error-text.js +35 -0
  90. package/dist/core/error-text.js.map +1 -0
  91. package/dist/core/events.d.ts +98 -0
  92. package/dist/core/events.d.ts.map +1 -0
  93. package/dist/core/events.js +50 -0
  94. package/dist/core/events.js.map +1 -0
  95. package/dist/core/format.d.ts +23 -0
  96. package/dist/core/format.d.ts.map +1 -0
  97. package/dist/core/format.js +49 -0
  98. package/dist/core/format.js.map +1 -0
  99. package/dist/core/index.d.ts +27 -0
  100. package/dist/core/index.d.ts.map +1 -0
  101. package/dist/core/index.js +20 -0
  102. package/dist/core/index.js.map +1 -0
  103. package/dist/core/markdown.d.ts +26 -0
  104. package/dist/core/markdown.d.ts.map +1 -0
  105. package/dist/core/markdown.js +49 -0
  106. package/dist/core/markdown.js.map +1 -0
  107. package/dist/core/prose.d.ts +27 -0
  108. package/dist/core/prose.d.ts.map +1 -0
  109. package/dist/core/prose.js +79 -0
  110. package/dist/core/prose.js.map +1 -0
  111. package/dist/core/small-talk.d.ts +15 -0
  112. package/dist/core/small-talk.d.ts.map +1 -0
  113. package/dist/core/small-talk.js +42 -0
  114. package/dist/core/small-talk.js.map +1 -0
  115. package/dist/core/sse.d.ts +108 -0
  116. package/dist/core/sse.d.ts.map +1 -0
  117. package/dist/core/sse.js +236 -0
  118. package/dist/core/sse.js.map +1 -0
  119. package/dist/core/tool-label.d.ts +16 -0
  120. package/dist/core/tool-label.d.ts.map +1 -0
  121. package/dist/core/tool-label.js +31 -0
  122. package/dist/core/tool-label.js.map +1 -0
  123. package/dist/core/transport.d.ts +71 -0
  124. package/dist/core/transport.d.ts.map +1 -0
  125. package/dist/core/transport.js +147 -0
  126. package/dist/core/transport.js.map +1 -0
  127. package/dist/core/turn.d.ts +87 -0
  128. package/dist/core/turn.d.ts.map +1 -0
  129. package/dist/core/turn.js +232 -0
  130. package/dist/core/turn.js.map +1 -0
  131. package/dist/core/uuid.d.ts +21 -0
  132. package/dist/core/uuid.d.ts.map +1 -0
  133. package/dist/core/uuid.js +54 -0
  134. package/dist/core/uuid.js.map +1 -0
  135. package/dist/entries/chat-page.d.ts +28 -0
  136. package/dist/entries/chat-page.d.ts.map +1 -0
  137. package/dist/entries/chat-page.js +25 -0
  138. package/dist/entries/chat-page.js.map +1 -0
  139. package/dist/entries/co-pilot.d.ts +41 -0
  140. package/dist/entries/co-pilot.d.ts.map +1 -0
  141. package/dist/entries/co-pilot.js +39 -0
  142. package/dist/entries/co-pilot.js.map +1 -0
  143. package/dist/entries/lazy-panel.d.ts +15 -0
  144. package/dist/entries/lazy-panel.d.ts.map +1 -0
  145. package/dist/entries/lazy-panel.js +19 -0
  146. package/dist/entries/lazy-panel.js.map +1 -0
  147. package/dist/hooks/index.d.ts +7 -0
  148. package/dist/hooks/index.d.ts.map +1 -0
  149. package/dist/hooks/index.js +4 -0
  150. package/dist/hooks/index.js.map +1 -0
  151. package/dist/hooks/use-agent-chat.d.ts +46 -0
  152. package/dist/hooks/use-agent-chat.d.ts.map +1 -0
  153. package/dist/hooks/use-agent-chat.js +288 -0
  154. package/dist/hooks/use-agent-chat.js.map +1 -0
  155. package/dist/hooks/use-agent-config.d.ts +29 -0
  156. package/dist/hooks/use-agent-config.d.ts.map +1 -0
  157. package/dist/hooks/use-agent-config.js +45 -0
  158. package/dist/hooks/use-agent-config.js.map +1 -0
  159. package/dist/hooks/use-conversations.d.ts +19 -0
  160. package/dist/hooks/use-conversations.d.ts.map +1 -0
  161. package/dist/hooks/use-conversations.js +19 -0
  162. package/dist/hooks/use-conversations.js.map +1 -0
  163. package/dist/index.d.ts +28 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +26 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/styles.css +1 -0
  168. package/package.json +105 -1
  169. package/src/branding.ts +40 -0
  170. package/src/components/approval-card.tsx +141 -0
  171. package/src/components/assistant-message.tsx +140 -0
  172. package/src/components/co-pilot-panel.tsx +109 -0
  173. package/src/components/code-block.tsx +63 -0
  174. package/src/components/collapsible-answer.tsx +55 -0
  175. package/src/components/composer.tsx +147 -0
  176. package/src/components/conversation-list.tsx +141 -0
  177. package/src/components/icons.tsx +189 -0
  178. package/src/components/launcher-fab.tsx +16 -0
  179. package/src/components/leaked-tool-call-notice.tsx +29 -0
  180. package/src/components/markdown-link.tsx +30 -0
  181. package/src/components/markdown-text.tsx +58 -0
  182. package/src/components/message-list.tsx +164 -0
  183. package/src/components/no-tools-note.tsx +33 -0
  184. package/src/components/panel-header.tsx +86 -0
  185. package/src/components/rehype-highlight-subset.ts +84 -0
  186. package/src/components/system-message.tsx +19 -0
  187. package/src/components/tool-chip.tsx +58 -0
  188. package/src/components/user-message.tsx +18 -0
  189. package/src/core/conversation-store.ts +342 -0
  190. package/src/core/error-text.ts +36 -0
  191. package/src/core/events.ts +127 -0
  192. package/src/core/format.ts +59 -0
  193. package/src/core/index.ts +115 -0
  194. package/src/core/markdown.ts +52 -0
  195. package/src/core/prose.ts +78 -0
  196. package/src/core/small-talk.ts +45 -0
  197. package/src/core/sse.ts +269 -0
  198. package/src/core/tool-label.ts +33 -0
  199. package/src/core/transport.ts +208 -0
  200. package/src/core/turn.ts +299 -0
  201. package/src/core/uuid.ts +59 -0
  202. package/src/entries/chat-page.tsx +56 -0
  203. package/src/entries/co-pilot.tsx +84 -0
  204. package/src/entries/lazy-panel.tsx +19 -0
  205. package/src/hooks/index.ts +8 -0
  206. package/src/hooks/use-agent-chat.ts +387 -0
  207. package/src/hooks/use-agent-config.ts +70 -0
  208. package/src/hooks/use-conversations.ts +39 -0
  209. package/src/index.ts +30 -0
  210. package/src/styles/_approval.scss +68 -0
  211. package/src/styles/_composer.scss +45 -0
  212. package/src/styles/_conversations.scss +160 -0
  213. package/src/styles/_messages.scss +444 -0
  214. package/src/styles/_mixins.scss +27 -0
  215. package/src/styles/_panel.scss +147 -0
  216. package/src/styles/_tokens.scss +30 -0
  217. package/src/styles.scss +16 -0
@@ -0,0 +1,164 @@
1
+ import type { JSX } from 'react'
2
+ import { useCallback, useEffect, useRef } from 'react'
3
+
4
+ import type { ChatMessage } from '../core/conversation-store'
5
+ import type { ToolLabels } from '../core/tool-label'
6
+ import type { AgentTurnState, PendingApproval } from '../core/turn'
7
+ import { MESSAGE_ROLE } from '../core/conversation-store'
8
+ import { TURN_STATUS } from '../core/turn'
9
+ import { AssistantMessage } from './assistant-message'
10
+ import { SystemMessage } from './system-message'
11
+ import { UserMessage } from './user-message'
12
+
13
+ /** How close to the bottom still counts as "following along", in pixels. */
14
+ const PIN_THRESHOLD_PX = 48
15
+
16
+ /**
17
+ * The operator message an assistant reply answers: the nearest user message above it.
18
+ *
19
+ * A turn can leave several assistant entries behind, so this is a scan rather than
20
+ * `messages[index - 1]`. Read only to tell a fleet question from a greeting.
21
+ */
22
+ function precedingQuestion(messages: ChatMessage[], index: number): string {
23
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
24
+ const message = messages[cursor]
25
+ if (message?.role === MESSAGE_ROLE.USER) return message.text
26
+ }
27
+ return ''
28
+ }
29
+
30
+ export type MessageListProps = {
31
+ messages: ChatMessage[]
32
+ live: AgentTurnState | null
33
+ toolLabels?: ToolLabels
34
+ pendingApproval: PendingApproval | null
35
+ isDecisionPending: boolean
36
+ onDecide: (approved: boolean) => void
37
+ /** Re-asks the last question. Offered on the newest failed turn only. */
38
+ onRetry?: VoidFunction
39
+ }
40
+
41
+ /**
42
+ * The transcript, pinned to the bottom while the operator is following along.
43
+ *
44
+ * There is no `ScrollArea` primitive in the devkit and `@radix-ui/react-scroll-area`
45
+ * is not a dependency, so this is a plain overflow container. The pinning rule
46
+ * matters more than the chrome: scrolling up to re-read an earlier answer must
47
+ * not be undone by the next token arriving.
48
+ */
49
+ export const MessageList = ({
50
+ messages,
51
+ live,
52
+ toolLabels,
53
+ pendingApproval,
54
+ isDecisionPending,
55
+ onDecide,
56
+ onRetry,
57
+ }: MessageListProps): JSX.Element => {
58
+ const scrollRef = useRef<HTMLDivElement>(null)
59
+ const pinnedRef = useRef(true)
60
+
61
+ const handleScroll = useCallback(() => {
62
+ const node = scrollRef.current
63
+ if (!node) return
64
+ const distance = node.scrollHeight - node.scrollTop - node.clientHeight
65
+ pinnedRef.current = distance <= PIN_THRESHOLD_PX
66
+ }, [])
67
+
68
+ // Depends on the streamed length rather than the state object so it also fires
69
+ // while tokens land inside one unchanged turn.
70
+ const liveLength = live ? live.text.length + live.tools.length : 0
71
+
72
+ /**
73
+ * Whether a turn is open at all — not the same question as how much of it has arrived.
74
+ *
75
+ * `liveLength` is 0 both before a turn exists and at the instant one starts, so on its own it
76
+ * cannot see the transition. The assistant block (speaker label and thinking indicator) is
77
+ * appended in exactly that commit and went unscrolled: the operator was left looking at their
78
+ * own question with the reply below the fold, which reads as the panel ignoring them. It
79
+ * self-corrected on the first token or tool call, so the dead window was however long the
80
+ * model took to say anything — seconds locally, far longer on a thinking model.
81
+ */
82
+ const isLive = live !== null
83
+
84
+ /**
85
+ * A turn opening re-pins, whatever the operator had scrolled to.
86
+ *
87
+ * A turn only opens because they just sent something, and sending is an explicit request to
88
+ * see the answer — so this is not the "don't yank the view while they read" case the pinning
89
+ * rule protects. Without it, asking a question after scrolling up back through the transcript
90
+ * left both the question and its answer off-screen, with no indication either had happened.
91
+ */
92
+ useEffect(() => {
93
+ if (isLive) pinnedRef.current = true
94
+ }, [isLive])
95
+
96
+ useEffect(() => {
97
+ const node = scrollRef.current
98
+ if (!node || !pinnedRef.current) return
99
+ node.scrollTop = node.scrollHeight
100
+ }, [messages.length, liveLength, isLive, pendingApproval])
101
+
102
+ const isEmpty = messages.length === 0 && live === null
103
+ // Retrying an older turn would send it as the newest question, so only the last
104
+ // message is offered the affordance — and never while a turn is running.
105
+ const retryableId
106
+ = live === null && onRetry ? (messages.at(-1)?.error ? messages.at(-1)?.id : undefined) : undefined
107
+
108
+ return (
109
+ /* `aria-live` so an answer, and the approval prompt in particular, reaches a
110
+ screen reader as it arrives — nothing else moves focus into the transcript. */
111
+ <div
112
+ className="mdk-agent-messages"
113
+ ref={scrollRef}
114
+ aria-live="polite"
115
+ aria-busy={live !== null}
116
+ onScroll={handleScroll}
117
+ >
118
+ {isEmpty
119
+ ? (
120
+ <p className="mdk-agent-messages__empty">
121
+ Ask about your fleet — device status, telemetry, or an action to run.
122
+ </p>
123
+ )
124
+ : null}
125
+
126
+ {messages.map((message, index) => {
127
+ if (message.role === MESSAGE_ROLE.USER) {
128
+ return <UserMessage key={message.id} text={message.text} />
129
+ }
130
+ if (message.role === MESSAGE_ROLE.SYSTEM) {
131
+ return <SystemMessage key={message.id} text={message.text} />
132
+ }
133
+ return (
134
+ <AssistantMessage
135
+ key={message.id}
136
+ text={message.text}
137
+ question={precedingQuestion(messages, index)}
138
+ tools={message.tools}
139
+ error={message.error}
140
+ interrupted={message.interrupted}
141
+ toolLabels={toolLabels}
142
+ {...(message.id === retryableId && onRetry ? { onRetry } : {})}
143
+ />
144
+ )
145
+ })}
146
+
147
+ {live
148
+ ? (
149
+ <AssistantMessage
150
+ text={live.text}
151
+ question={precedingQuestion(messages, messages.length)}
152
+ tools={live.tools}
153
+ error={live.error}
154
+ streaming={live.status !== TURN_STATUS.COMPLETE && live.status !== TURN_STATUS.ERROR}
155
+ toolLabels={toolLabels}
156
+ approval={pendingApproval}
157
+ isDecisionPending={isDecisionPending}
158
+ onDecide={onDecide}
159
+ />
160
+ )
161
+ : null}
162
+ </div>
163
+ )
164
+ }
@@ -0,0 +1,33 @@
1
+ import type { JSX } from 'react'
2
+
3
+ import { InfoIcon } from './icons'
4
+
5
+ /**
6
+ * Marks an answer that consulted nothing.
7
+ *
8
+ * The agent's premise is that it reads the fleet through tools rather than
9
+ * inventing it, so a turn that emitted no `tool_call` answered from the model's
10
+ * own weights and its conversation history — including, sometimes, a confident
11
+ * claim that a write was carried out when none was. That failure is not
12
+ * detectable from the text, but the absence of tool activity is a fact the UI
13
+ * holds exactly, so it is stated rather than left to be inferred from a missing
14
+ * chip.
15
+ *
16
+ * Still shown for benign cases — a decline, an answer the tools cannot cover —
17
+ * because "this answer did not consult the fleet" is true and useful there too.
18
+ * The one exception is small talk (`core/small-talk`): against "hey" there was
19
+ * never a tool to call, and marking every greeting turned the note into
20
+ * wallpaper.
21
+ *
22
+ * Rendered as a suffix on the existing `CO-PILOT` label rather than a sentence
23
+ * of its own: in a degraded session every answer carries it, and a repeated
24
+ * full-width line turned the transcript into a column of warnings. On the label
25
+ * row it costs no vertical space and still reads at a glance; the full sentence
26
+ * lives in the tooltip.
27
+ */
28
+ export const NoToolsNote = (): JSX.Element => (
29
+ <span className="mdk-agent-no-tools" title="No tool was called for this answer — it came from the model alone, not from the fleet.">
30
+ <InfoIcon size={11} aria-hidden="true" />
31
+ no tools used
32
+ </span>
33
+ )
@@ -0,0 +1,86 @@
1
+ import { Button } from '@tetherto/mdk-react-devkit/primitives'
2
+ import type { JSX, ReactNode } from 'react'
3
+ import { memo } from 'react'
4
+
5
+ import { AGENT_LABELS } from '../branding'
6
+ import { BackIcon, CloseIcon, ListLayoutIcon, MessagePlusIcon } from './icons'
7
+
8
+ export const PANEL_VIEW = { CHAT: 'chat', CONVERSATIONS: 'conversations' } as const
9
+
10
+ export type PanelView = (typeof PANEL_VIEW)[keyof typeof PANEL_VIEW]
11
+
12
+ export type PanelHeaderProps = {
13
+ view: PanelView
14
+ title: string
15
+ /**
16
+ * The line under the title. Nothing on the gateway reports the model name or
17
+ * the tool count — the agent is built lazily on the first session and its MCP
18
+ * tools are never enumerated over HTTP — so the host supplies this or it stays
19
+ * empty.
20
+ */
21
+ status?: ReactNode
22
+ /** Disabled mid-turn: starting or switching conversation would strand the stream. */
23
+ actionsDisabled?: boolean
24
+ onNewConversation: VoidFunction
25
+ onToggleConversations: VoidFunction
26
+ onClose?: VoidFunction | undefined
27
+ }
28
+
29
+ const PanelHeaderView = ({
30
+ view,
31
+ title,
32
+ status,
33
+ actionsDisabled = false,
34
+ onNewConversation,
35
+ onToggleConversations,
36
+ onClose,
37
+ }: PanelHeaderProps): JSX.Element => (
38
+ <header className="mdk-agent-header">
39
+ <div className="mdk-agent-header__titles">
40
+ <h2 className="mdk-agent-header__title">
41
+ {view === PANEL_VIEW.CONVERSATIONS
42
+ ? (
43
+ <Button
44
+ variant="icon"
45
+ className="mdk-agent-header__back"
46
+ aria-label={AGENT_LABELS.backToConversation}
47
+ icon={<BackIcon size={16} />}
48
+ onClick={onToggleConversations}
49
+ />
50
+ )
51
+ : null}
52
+ {title}
53
+ </h2>
54
+ {status ? <p className="mdk-agent-header__status">{status}</p> : null}
55
+ </div>
56
+
57
+ <div className="mdk-agent-header__actions">
58
+ <Button
59
+ variant="icon"
60
+ aria-label={AGENT_LABELS.newConversation}
61
+ disabled={actionsDisabled}
62
+ icon={<MessagePlusIcon size={16} />}
63
+ onClick={onNewConversation}
64
+ />
65
+ <Button
66
+ variant="icon"
67
+ aria-label={AGENT_LABELS.conversations}
68
+ aria-pressed={view === PANEL_VIEW.CONVERSATIONS}
69
+ icon={<ListLayoutIcon size={16} />}
70
+ onClick={onToggleConversations}
71
+ />
72
+ {onClose
73
+ ? (
74
+ <Button
75
+ variant="icon"
76
+ aria-label={AGENT_LABELS.close}
77
+ icon={<CloseIcon size={16} />}
78
+ onClick={onClose}
79
+ />
80
+ )
81
+ : null}
82
+ </div>
83
+ </header>
84
+ )
85
+
86
+ export const PanelHeader = memo(PanelHeaderView)
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Syntax highlighting for fenced code, restricted to the grammars this panel
3
+ * actually sees.
4
+ *
5
+ * `rehype-highlight` would do the same job, but it statically imports lowlight's
6
+ * `common` set — 37 grammars and most of highlight.js's bulk — so its
7
+ * `languages` option changes what gets *registered*, never what gets *bundled*.
8
+ * Measured against the generated shell, that was 179 kB raw / 55 kB gzip, more
9
+ * than doubling the panel's chunk to highlight languages a fleet co-pilot never
10
+ * emits. Registering three grammars directly costs a fraction of that.
11
+ *
12
+ * Anything outside the set still renders — unhighlighted, in the mono face the
13
+ * panel already uses.
14
+ */
15
+
16
+ import bash from 'highlight.js/lib/languages/bash'
17
+ import json from 'highlight.js/lib/languages/json'
18
+ import yaml from 'highlight.js/lib/languages/yaml'
19
+ import type { Element, ElementContent, Node, Parent, Root, Text } from 'hast'
20
+ import { createLowlight } from 'lowlight'
21
+
22
+ const lowlight = createLowlight({ bash, json, yaml })
23
+
24
+ const LANGUAGE_PREFIX = 'language-'
25
+
26
+ function isElement(node: Node): node is Element {
27
+ return node.type === 'element'
28
+ }
29
+
30
+ function isParent(node: Node): node is Parent {
31
+ return Array.isArray((node as Parent).children)
32
+ }
33
+
34
+ /** Concatenates the text under a node, which for a code block is its source. */
35
+ function textOf(node: Node): string {
36
+ if (node.type === 'text') return (node as Text).value
37
+ if (!isParent(node)) return ''
38
+ return node.children.map((child) => textOf(child)).join('')
39
+ }
40
+
41
+ function languageOf(node: Element): string | null {
42
+ const className = node.properties?.className
43
+ const classes = Array.isArray(className) ? className.map(String) : []
44
+ const match = classes.find((name) => name.startsWith(LANGUAGE_PREFIX))
45
+ return match ? match.slice(LANGUAGE_PREFIX.length) : null
46
+ }
47
+
48
+ function highlightCodeBlock(node: Element): void {
49
+ const language = languageOf(node)
50
+ if (!language || !lowlight.registered(language)) return
51
+
52
+ try {
53
+ const result = lowlight.highlight(language, textOf(node))
54
+ // lowlight returns a Root, whose children are typed wider than a code
55
+ // element's. In practice it only ever emits elements and text.
56
+ node.children = result.children as ElementContent[]
57
+ node.properties = {
58
+ ...node.properties,
59
+ className: [...(Array.isArray(node.properties?.className) ? node.properties.className : []), 'hljs'],
60
+ }
61
+ } catch {
62
+ // A grammar that throws on odd input must not take the whole answer with it;
63
+ // the block simply stays unhighlighted.
64
+ }
65
+ }
66
+
67
+ function walk(node: Node): void {
68
+ if (!isParent(node)) return
69
+
70
+ for (const child of node.children) {
71
+ // Only `<pre><code>` — an inline `code` span is not a code block.
72
+ if (isElement(node) && node.tagName === 'pre' && isElement(child) && child.tagName === 'code') {
73
+ highlightCodeBlock(child)
74
+ continue
75
+ }
76
+ walk(child)
77
+ }
78
+ }
79
+
80
+ export function rehypeHighlightSubset() {
81
+ return (tree: Root): void => {
82
+ walk(tree)
83
+ }
84
+ }
@@ -0,0 +1,19 @@
1
+ import type { JSX } from 'react'
2
+ import { memo } from 'react'
3
+
4
+ export type SystemMessageProps = {
5
+ text: string
6
+ }
7
+
8
+ /**
9
+ * A note from the panel itself, not from the model.
10
+ *
11
+ * Used where the transcript would otherwise imply something untrue — most of all
12
+ * a re-minted session, where the conversation reads as continuous but the agent
13
+ * remembers none of it.
14
+ */
15
+ const SystemMessageView = ({ text }: SystemMessageProps): JSX.Element => (
16
+ <p className="mdk-agent-system" role="note">{text}</p>
17
+ )
18
+
19
+ export const SystemMessage = memo(SystemMessageView)
@@ -0,0 +1,58 @@
1
+ import type { JSX } from 'react'
2
+ import { memo } from 'react'
3
+
4
+ import type { ToolLabels } from '../core/tool-label'
5
+ import type { ToolStep } from '../core/turn'
6
+ import { formatDuration, toolLabel } from '../core/tool-label'
7
+ import { TOOL_STATUS } from '../core/turn'
8
+ import { CloseIcon, TickIcon, WarningIcon } from './icons'
9
+
10
+ export type ToolChipProps = {
11
+ step: ToolStep
12
+ toolLabels?: ToolLabels
13
+ }
14
+
15
+ /**
16
+ * One tool call, as the small pill above an answer: `✓ Site status · 0.4s`.
17
+ *
18
+ * Showing tool activity is not decoration. Model routing is the weak point of
19
+ * the stack — a turn that answers without ever calling a tool is answering from
20
+ * the prompt, not from the fleet — so the chips are what make that visible
21
+ * instead of mysterious.
22
+ */
23
+ const ToolChipView = ({ step, toolLabels }: ToolChipProps): JSX.Element => {
24
+ const duration = formatDuration(step.durationMs)
25
+ const label = toolLabel(step.name, toolLabels)
26
+
27
+ const detail
28
+ = step.status === TOOL_STATUS.AWAITING_APPROVAL
29
+ ? 'awaiting approval'
30
+ : step.status === TOOL_STATUS.RUNNING
31
+ ? 'running…'
32
+ : step.status === TOOL_STATUS.REJECTED
33
+ // Deliberately no duration: nothing ran, so there is nothing to time.
34
+ ? 'not run'
35
+ // The turn ended before the result came back, so whether it ran is unknown
36
+ // — and the elapsed time would measure the failure, not the tool.
37
+ : step.status === TOOL_STATUS.INTERRUPTED
38
+ ? 'no result'
39
+ : duration
40
+
41
+ return (
42
+ <span
43
+ className={`mdk-agent-tool-chip mdk-agent-tool-chip--${step.status}`}
44
+ title={step.contractViolation ?? step.text ?? label}
45
+ >
46
+ <span className="mdk-agent-tool-chip__icon" aria-hidden="true">
47
+ {step.status === TOOL_STATUS.OK ? <TickIcon size={12} /> : null}
48
+ {step.status === TOOL_STATUS.ERROR ? <WarningIcon size={12} /> : null}
49
+ {step.status === TOOL_STATUS.REJECTED ? <CloseIcon size={12} /> : null}
50
+ {step.status === TOOL_STATUS.INTERRUPTED ? <WarningIcon size={12} /> : null}
51
+ </span>
52
+ <span className="mdk-agent-tool-chip__label">{label}</span>
53
+ {detail ? <span className="mdk-agent-tool-chip__detail">{`· ${detail}`}</span> : null}
54
+ </span>
55
+ )
56
+ }
57
+
58
+ export const ToolChip = memo(ToolChipView)
@@ -0,0 +1,18 @@
1
+ import type { JSX } from 'react'
2
+ import { memo } from 'react'
3
+
4
+ import { AGENT_LABELS } from '../branding'
5
+
6
+ export type UserMessageProps = {
7
+ text: string
8
+ }
9
+
10
+ /** Right-aligned operator turn: an `OPERATOR` caption above a bordered bubble. */
11
+ const UserMessageView = ({ text }: UserMessageProps): JSX.Element => (
12
+ <div className="mdk-agent-message mdk-agent-message--user">
13
+ <p className="mdk-agent-message__role">{AGENT_LABELS.operator}</p>
14
+ <div className="mdk-agent-message__bubble">{text}</div>
15
+ </div>
16
+ )
17
+
18
+ export const UserMessage = memo(UserMessageView)