@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.
- package/LICENSE +202 -0
- package/README.md +310 -0
- package/dist/branding.d.ts +36 -0
- package/dist/branding.d.ts.map +1 -0
- package/dist/branding.js +37 -0
- package/dist/branding.js.map +1 -0
- package/dist/components/approval-card.d.ts +46 -0
- package/dist/components/approval-card.d.ts.map +1 -0
- package/dist/components/approval-card.js +84 -0
- package/dist/components/approval-card.js.map +1 -0
- package/dist/components/assistant-message.d.ts +25 -0
- package/dist/components/assistant-message.d.ts.map +1 -0
- package/dist/components/assistant-message.js +49 -0
- package/dist/components/assistant-message.js.map +1 -0
- package/dist/components/co-pilot-panel.d.ts +17 -0
- package/dist/components/co-pilot-panel.d.ts.map +1 -0
- package/dist/components/co-pilot-panel.js +33 -0
- package/dist/components/co-pilot-panel.js.map +1 -0
- package/dist/components/code-block.d.ts +17 -0
- package/dist/components/code-block.d.ts.map +1 -0
- package/dist/components/code-block.js +40 -0
- package/dist/components/code-block.js.map +1 -0
- package/dist/components/collapsible-answer.d.ts +26 -0
- package/dist/components/collapsible-answer.d.ts.map +1 -0
- package/dist/components/collapsible-answer.js +30 -0
- package/dist/components/collapsible-answer.js.map +1 -0
- package/dist/components/composer.d.ts +31 -0
- package/dist/components/composer.d.ts.map +1 -0
- package/dist/components/composer.js +82 -0
- package/dist/components/composer.js.map +1 -0
- package/dist/components/conversation-list.d.ts +21 -0
- package/dist/components/conversation-list.d.ts.map +1 -0
- package/dist/components/conversation-list.js +42 -0
- package/dist/components/conversation-list.js.map +1 -0
- package/dist/components/icons.d.ts +88 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +112 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/launcher-fab.d.ts +8 -0
- package/dist/components/launcher-fab.d.ts.map +1 -0
- package/dist/components/launcher-fab.js +6 -0
- package/dist/components/launcher-fab.js.map +1 -0
- package/dist/components/leaked-tool-call-notice.d.ts +18 -0
- package/dist/components/leaked-tool-call-notice.d.ts.map +1 -0
- package/dist/components/leaked-tool-call-notice.js +14 -0
- package/dist/components/leaked-tool-call-notice.js.map +1 -0
- package/dist/components/markdown-link.d.ts +17 -0
- package/dist/components/markdown-link.d.ts.map +1 -0
- package/dist/components/markdown-link.js +18 -0
- package/dist/components/markdown-link.js.map +1 -0
- package/dist/components/markdown-text.d.ts +26 -0
- package/dist/components/markdown-text.d.ts.map +1 -0
- package/dist/components/markdown-text.js +32 -0
- package/dist/components/markdown-text.js.map +1 -0
- package/dist/components/message-list.d.ts +24 -0
- package/dist/components/message-list.d.ts.map +1 -0
- package/dist/components/message-list.js +92 -0
- package/dist/components/message-list.js.map +1 -0
- package/dist/components/no-tools-note.d.ts +26 -0
- package/dist/components/no-tools-note.d.ts.map +1 -0
- package/dist/components/no-tools-note.js +27 -0
- package/dist/components/no-tools-note.js.map +1 -0
- package/dist/components/panel-header.d.ts +26 -0
- package/dist/components/panel-header.d.ts.map +1 -0
- package/dist/components/panel-header.js +13 -0
- package/dist/components/panel-header.js.map +1 -0
- package/dist/components/rehype-highlight-subset.d.ts +17 -0
- package/dist/components/rehype-highlight-subset.d.ts.map +1 -0
- package/dist/components/rehype-highlight-subset.js +77 -0
- package/dist/components/rehype-highlight-subset.js.map +1 -0
- package/dist/components/system-message.d.ts +15 -0
- package/dist/components/system-message.d.ts.map +1 -0
- package/dist/components/system-message.js +12 -0
- package/dist/components/system-message.js.map +1 -0
- package/dist/components/tool-chip.d.ts +19 -0
- package/dist/components/tool-chip.d.ts.map +1 -0
- package/dist/components/tool-chip.js +32 -0
- package/dist/components/tool-chip.js.map +1 -0
- package/dist/components/user-message.d.ts +9 -0
- package/dist/components/user-message.d.ts.map +1 -0
- package/dist/components/user-message.js +7 -0
- package/dist/components/user-message.js.map +1 -0
- package/dist/core/conversation-store.d.ts +107 -0
- package/dist/core/conversation-store.d.ts.map +1 -0
- package/dist/core/conversation-store.js +254 -0
- package/dist/core/conversation-store.js.map +1 -0
- package/dist/core/error-text.d.ts +19 -0
- package/dist/core/error-text.d.ts.map +1 -0
- package/dist/core/error-text.js +35 -0
- package/dist/core/error-text.js.map +1 -0
- package/dist/core/events.d.ts +98 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +50 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/format.d.ts +23 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +49 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/index.d.ts +27 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +20 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/markdown.d.ts +26 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +49 -0
- package/dist/core/markdown.js.map +1 -0
- package/dist/core/prose.d.ts +27 -0
- package/dist/core/prose.d.ts.map +1 -0
- package/dist/core/prose.js +79 -0
- package/dist/core/prose.js.map +1 -0
- package/dist/core/small-talk.d.ts +15 -0
- package/dist/core/small-talk.d.ts.map +1 -0
- package/dist/core/small-talk.js +42 -0
- package/dist/core/small-talk.js.map +1 -0
- package/dist/core/sse.d.ts +108 -0
- package/dist/core/sse.d.ts.map +1 -0
- package/dist/core/sse.js +236 -0
- package/dist/core/sse.js.map +1 -0
- package/dist/core/tool-label.d.ts +16 -0
- package/dist/core/tool-label.d.ts.map +1 -0
- package/dist/core/tool-label.js +31 -0
- package/dist/core/tool-label.js.map +1 -0
- package/dist/core/transport.d.ts +71 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +147 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/core/turn.d.ts +87 -0
- package/dist/core/turn.d.ts.map +1 -0
- package/dist/core/turn.js +232 -0
- package/dist/core/turn.js.map +1 -0
- package/dist/core/uuid.d.ts +21 -0
- package/dist/core/uuid.d.ts.map +1 -0
- package/dist/core/uuid.js +54 -0
- package/dist/core/uuid.js.map +1 -0
- package/dist/entries/chat-page.d.ts +28 -0
- package/dist/entries/chat-page.d.ts.map +1 -0
- package/dist/entries/chat-page.js +25 -0
- package/dist/entries/chat-page.js.map +1 -0
- package/dist/entries/co-pilot.d.ts +41 -0
- package/dist/entries/co-pilot.d.ts.map +1 -0
- package/dist/entries/co-pilot.js +39 -0
- package/dist/entries/co-pilot.js.map +1 -0
- package/dist/entries/lazy-panel.d.ts +15 -0
- package/dist/entries/lazy-panel.d.ts.map +1 -0
- package/dist/entries/lazy-panel.js +19 -0
- package/dist/entries/lazy-panel.js.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-agent-chat.d.ts +46 -0
- package/dist/hooks/use-agent-chat.d.ts.map +1 -0
- package/dist/hooks/use-agent-chat.js +288 -0
- package/dist/hooks/use-agent-chat.js.map +1 -0
- package/dist/hooks/use-agent-config.d.ts +29 -0
- package/dist/hooks/use-agent-config.d.ts.map +1 -0
- package/dist/hooks/use-agent-config.js +45 -0
- package/dist/hooks/use-agent-config.js.map +1 -0
- package/dist/hooks/use-conversations.d.ts +19 -0
- package/dist/hooks/use-conversations.d.ts.map +1 -0
- package/dist/hooks/use-conversations.js +19 -0
- package/dist/hooks/use-conversations.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +105 -1
- package/src/branding.ts +40 -0
- package/src/components/approval-card.tsx +141 -0
- package/src/components/assistant-message.tsx +140 -0
- package/src/components/co-pilot-panel.tsx +109 -0
- package/src/components/code-block.tsx +63 -0
- package/src/components/collapsible-answer.tsx +55 -0
- package/src/components/composer.tsx +147 -0
- package/src/components/conversation-list.tsx +141 -0
- package/src/components/icons.tsx +189 -0
- package/src/components/launcher-fab.tsx +16 -0
- package/src/components/leaked-tool-call-notice.tsx +29 -0
- package/src/components/markdown-link.tsx +30 -0
- package/src/components/markdown-text.tsx +58 -0
- package/src/components/message-list.tsx +164 -0
- package/src/components/no-tools-note.tsx +33 -0
- package/src/components/panel-header.tsx +86 -0
- package/src/components/rehype-highlight-subset.ts +84 -0
- package/src/components/system-message.tsx +19 -0
- package/src/components/tool-chip.tsx +58 -0
- package/src/components/user-message.tsx +18 -0
- package/src/core/conversation-store.ts +342 -0
- package/src/core/error-text.ts +36 -0
- package/src/core/events.ts +127 -0
- package/src/core/format.ts +59 -0
- package/src/core/index.ts +115 -0
- package/src/core/markdown.ts +52 -0
- package/src/core/prose.ts +78 -0
- package/src/core/small-talk.ts +45 -0
- package/src/core/sse.ts +269 -0
- package/src/core/tool-label.ts +33 -0
- package/src/core/transport.ts +208 -0
- package/src/core/turn.ts +299 -0
- package/src/core/uuid.ts +59 -0
- package/src/entries/chat-page.tsx +56 -0
- package/src/entries/co-pilot.tsx +84 -0
- package/src/entries/lazy-panel.tsx +19 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-agent-chat.ts +387 -0
- package/src/hooks/use-agent-config.ts +70 -0
- package/src/hooks/use-conversations.ts +39 -0
- package/src/index.ts +30 -0
- package/src/styles/_approval.scss +68 -0
- package/src/styles/_composer.scss +45 -0
- package/src/styles/_conversations.scss +160 -0
- package/src/styles/_messages.scss +444 -0
- package/src/styles/_mixins.scss +27 -0
- package/src/styles/_panel.scss +147 -0
- package/src/styles/_tokens.scss +30 -0
- 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)
|