@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,147 @@
|
|
|
1
|
+
import { Button } from '@tetherto/mdk-react-devkit/primitives'
|
|
2
|
+
import type { FormEvent, JSX, KeyboardEvent } from 'react'
|
|
3
|
+
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { AGENT_LABELS } from '../branding'
|
|
6
|
+
import { SendIcon, StopIcon } from './icons'
|
|
7
|
+
|
|
8
|
+
/** Grows with the text up to this, then scrolls. Keeps the panel usable. */
|
|
9
|
+
const MAX_ROWS = 6
|
|
10
|
+
|
|
11
|
+
export type ComposerProps = {
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
/**
|
|
14
|
+
* A turn is running. Kept separate from `disabled` — which is about the input —
|
|
15
|
+
* because this swaps Send for Stop, and a composer disabled for any other reason
|
|
16
|
+
* must not offer to stop a turn that is not there.
|
|
17
|
+
*/
|
|
18
|
+
isStreaming?: boolean
|
|
19
|
+
placeholder?: string
|
|
20
|
+
/**
|
|
21
|
+
* Changing this focuses the input — used when the panel switches conversation,
|
|
22
|
+
* so starting a new one lands the caret where you are about to type. Ignored on
|
|
23
|
+
* first render, so mounting the panel never steals focus from the host page.
|
|
24
|
+
*/
|
|
25
|
+
focusKey?: string | number | null
|
|
26
|
+
onSend: (text: string) => void
|
|
27
|
+
/** Aborts the running turn. Without it a turn going wrong can only be escaped by reloading. */
|
|
28
|
+
onStop?: VoidFunction
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The input row: auto-growing, Enter sends, Shift+Enter breaks the line.
|
|
33
|
+
*
|
|
34
|
+
* `disabled` is driven by the session being busy, not by a local guard — the
|
|
35
|
+
* gateway allows one turn at a time per session and answers a second POST with
|
|
36
|
+
* a 409, and a turn paused on an approval still counts as busy.
|
|
37
|
+
*/
|
|
38
|
+
const ComposerView = ({
|
|
39
|
+
disabled = false,
|
|
40
|
+
isStreaming = false,
|
|
41
|
+
placeholder = AGENT_LABELS.placeholder,
|
|
42
|
+
focusKey = null,
|
|
43
|
+
onSend,
|
|
44
|
+
onStop,
|
|
45
|
+
}: ComposerProps): JSX.Element => {
|
|
46
|
+
const [value, setValue] = useState('')
|
|
47
|
+
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
|
48
|
+
const refocusRef = useRef(false)
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hands focus back when the turn releases the input.
|
|
52
|
+
*
|
|
53
|
+
* Disabling a focused `<textarea>` makes the browser blur it, so without this
|
|
54
|
+
* the caret is gone after every message and the next one needs a click. Armed
|
|
55
|
+
* only by an actual submit, so a turn finishing never steals focus from
|
|
56
|
+
* somewhere else the operator has moved to.
|
|
57
|
+
*/
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (disabled || !refocusRef.current) return
|
|
60
|
+
refocusRef.current = false
|
|
61
|
+
textAreaRef.current?.focus()
|
|
62
|
+
}, [disabled])
|
|
63
|
+
|
|
64
|
+
// Focus on a conversation change, but not on the first render — mounting the
|
|
65
|
+
// panel (or a route page) must not pull focus out of whatever the host had.
|
|
66
|
+
const lastFocusKeyRef = useRef<string | number | null | undefined>(undefined)
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const previous = lastFocusKeyRef.current
|
|
69
|
+
lastFocusKeyRef.current = focusKey
|
|
70
|
+
// `disabled` is a dependency only so the focus lands after the input is
|
|
71
|
+
// usable again; a change to it alone must not count as a conversation
|
|
72
|
+
// switch, or every finished turn would grab focus.
|
|
73
|
+
if (previous === undefined || previous === focusKey || disabled) return
|
|
74
|
+
textAreaRef.current?.focus()
|
|
75
|
+
}, [focusKey, disabled])
|
|
76
|
+
|
|
77
|
+
// The devkit `TextArea` is a plain `<textarea>` with no auto-grow, so the
|
|
78
|
+
// height is measured here: reset first, otherwise scrollHeight only ever
|
|
79
|
+
// grows and the box never shrinks back after a deletion.
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
const node = textAreaRef.current
|
|
82
|
+
if (!node) return
|
|
83
|
+
node.style.height = 'auto'
|
|
84
|
+
const lineHeight = Number.parseFloat(globalThis.getComputedStyle(node).lineHeight) || 20
|
|
85
|
+
node.style.height = `${Math.min(node.scrollHeight, lineHeight * MAX_ROWS)}px`
|
|
86
|
+
}, [value])
|
|
87
|
+
|
|
88
|
+
const submit = () => {
|
|
89
|
+
const text = value.trim()
|
|
90
|
+
if (!text || disabled) return
|
|
91
|
+
setValue('')
|
|
92
|
+
refocusRef.current = true
|
|
93
|
+
onSend(text)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handleSubmit = (event: FormEvent) => {
|
|
97
|
+
event.preventDefault()
|
|
98
|
+
submit()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
102
|
+
// `isComposing` guards IME candidate selection, where Enter commits the
|
|
103
|
+
// candidate rather than the message.
|
|
104
|
+
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return
|
|
105
|
+
event.preventDefault()
|
|
106
|
+
submit()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<form className="mdk-agent-composer" onSubmit={handleSubmit}>
|
|
111
|
+
<textarea
|
|
112
|
+
ref={textAreaRef}
|
|
113
|
+
className="mdk-agent-composer__input"
|
|
114
|
+
rows={1}
|
|
115
|
+
value={value}
|
|
116
|
+
placeholder={placeholder}
|
|
117
|
+
aria-label={AGENT_LABELS.composer}
|
|
118
|
+
disabled={disabled}
|
|
119
|
+
onChange={(event) => setValue(event.target.value)}
|
|
120
|
+
onKeyDown={handleKeyDown}
|
|
121
|
+
/>
|
|
122
|
+
{isStreaming && onStop
|
|
123
|
+
? (
|
|
124
|
+
<Button
|
|
125
|
+
type="button"
|
|
126
|
+
variant="primary"
|
|
127
|
+
className="mdk-agent-composer__send"
|
|
128
|
+
aria-label="Stop"
|
|
129
|
+
icon={<StopIcon size={18} />}
|
|
130
|
+
onClick={onStop}
|
|
131
|
+
/>
|
|
132
|
+
)
|
|
133
|
+
: (
|
|
134
|
+
<Button
|
|
135
|
+
type="submit"
|
|
136
|
+
variant="primary"
|
|
137
|
+
className="mdk-agent-composer__send"
|
|
138
|
+
aria-label="Send"
|
|
139
|
+
disabled={disabled || value.trim().length === 0}
|
|
140
|
+
icon={<SendIcon size={18} />}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
</form>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const Composer = memo(ComposerView)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Button } from '@tetherto/mdk-react-devkit/primitives'
|
|
2
|
+
import type { JSX } from 'react'
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { Conversation } from '../core/conversation-store'
|
|
6
|
+
import { AGENT_LABELS } from '../branding'
|
|
7
|
+
import { formatConversationMeta } from '../core/format'
|
|
8
|
+
import { MessagePlusIcon, TrashIcon } from './icons'
|
|
9
|
+
|
|
10
|
+
export type ConversationListProps = {
|
|
11
|
+
conversations: Conversation[]
|
|
12
|
+
activeId: string | null
|
|
13
|
+
/** The conversation with a turn in flight, if any. */
|
|
14
|
+
runningId?: string | null
|
|
15
|
+
onSelect: (id: string) => void
|
|
16
|
+
onCreate: VoidFunction
|
|
17
|
+
/** Omit to hide the delete affordance entirely. */
|
|
18
|
+
onDelete?: (id: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Past conversations, newest first.
|
|
23
|
+
*
|
|
24
|
+
* "Running" is local truth, not server truth: the gateway keeps sessions in
|
|
25
|
+
* memory per process and exposes no way to list them, so the only turn this
|
|
26
|
+
* client can know about is the one it is streaming itself.
|
|
27
|
+
*/
|
|
28
|
+
export const ConversationList = ({
|
|
29
|
+
conversations,
|
|
30
|
+
activeId,
|
|
31
|
+
runningId = null,
|
|
32
|
+
onSelect,
|
|
33
|
+
onCreate,
|
|
34
|
+
onDelete,
|
|
35
|
+
}: ConversationListProps): JSX.Element => {
|
|
36
|
+
// Deleting drops a transcript that exists nowhere else — the gateway keeps no
|
|
37
|
+
// history — so it takes two clicks rather than a modal.
|
|
38
|
+
const [confirmingId, setConfirmingId] = useState<string | null>(null)
|
|
39
|
+
|
|
40
|
+
// A conversation that disappears (deleted elsewhere, or storage cleared) must
|
|
41
|
+
// not leave the row behind it stuck in the confirming state.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (confirmingId && !conversations.some((c) => c.id === confirmingId)) setConfirmingId(null)
|
|
44
|
+
}, [conversations, confirmingId])
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="mdk-agent-conversations">
|
|
48
|
+
<div className="mdk-agent-conversations__scroll">
|
|
49
|
+
{conversations.length === 0
|
|
50
|
+
? <p className="mdk-agent-conversations__empty">No conversations yet.</p>
|
|
51
|
+
: null}
|
|
52
|
+
|
|
53
|
+
{conversations.map((conversation) => {
|
|
54
|
+
const isRunning = conversation.id === runningId
|
|
55
|
+
const isConfirming = conversation.id === confirmingId
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
key={conversation.id}
|
|
60
|
+
className={`mdk-agent-conversations__row${
|
|
61
|
+
conversation.id === activeId ? ' mdk-agent-conversations__row--active' : ''
|
|
62
|
+
}`}
|
|
63
|
+
>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="mdk-agent-conversations__open"
|
|
67
|
+
onClick={() => onSelect(conversation.id)}
|
|
68
|
+
>
|
|
69
|
+
<span className="mdk-agent-conversations__title">{conversation.title}</span>
|
|
70
|
+
<span className="mdk-agent-conversations__meta">
|
|
71
|
+
{formatConversationMeta(conversation.updatedAt, conversation.messages.length)}
|
|
72
|
+
</span>
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
{isConfirming
|
|
76
|
+
? (
|
|
77
|
+
<div className="mdk-agent-conversations__confirm">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className="mdk-agent-conversations__confirm-yes"
|
|
81
|
+
onClick={() => {
|
|
82
|
+
setConfirmingId(null)
|
|
83
|
+
onDelete?.(conversation.id)
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
Delete
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className="mdk-agent-conversations__confirm-no"
|
|
91
|
+
onClick={() => setConfirmingId(null)}
|
|
92
|
+
>
|
|
93
|
+
Cancel
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
: (
|
|
98
|
+
<>
|
|
99
|
+
<span
|
|
100
|
+
className={`mdk-agent-conversations__badge mdk-agent-conversations__badge--${
|
|
101
|
+
isRunning ? 'running' : 'idle'
|
|
102
|
+
}`}
|
|
103
|
+
>
|
|
104
|
+
{isRunning ? 'Running' : 'Idle'}
|
|
105
|
+
</span>
|
|
106
|
+
{onDelete
|
|
107
|
+
? (
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
className="mdk-agent-conversations__delete"
|
|
111
|
+
// Deleting the conversation whose turn is streaming would
|
|
112
|
+
// strand the open stream with nowhere to write its result.
|
|
113
|
+
disabled={isRunning}
|
|
114
|
+
aria-label={`Delete conversation: ${conversation.title}`}
|
|
115
|
+
onClick={() => setConfirmingId(conversation.id)}
|
|
116
|
+
>
|
|
117
|
+
<TrashIcon size={14} />
|
|
118
|
+
</button>
|
|
119
|
+
)
|
|
120
|
+
: null}
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
})}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div className="mdk-agent-conversations__footer">
|
|
129
|
+
<Button
|
|
130
|
+
variant="outline"
|
|
131
|
+
size="sm"
|
|
132
|
+
fullWidth
|
|
133
|
+
icon={<MessagePlusIcon size={16} />}
|
|
134
|
+
onClick={onCreate}
|
|
135
|
+
>
|
|
136
|
+
{AGENT_LABELS.newConversation}
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The glyphs the Co-pilot needs that the devkit icon set does not carry.
|
|
3
|
+
*
|
|
4
|
+
* Built with the devkit's `createIcon` factory so they take the same `size` /
|
|
5
|
+
* `color` props and inherit `currentColor` like every other icon in the system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createIcon } from '@tetherto/mdk-react-devkit/primitives'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The launcher glyph: a round speech bubble whose tail points to the
|
|
12
|
+
* bottom-right corner.
|
|
13
|
+
*
|
|
14
|
+
* Geometry fitted to the design's 22×22 `Chat` instance rather than eyeballed —
|
|
15
|
+
* the ring fills the box (center 11, r 10.2) and the tail is a wedge between two
|
|
16
|
+
* points on the arc, tipped at the corner. Drawn as a single arc-plus-wedge path
|
|
17
|
+
* so the stroke stays continuous through the join.
|
|
18
|
+
*
|
|
19
|
+
* Rendered at 48px against the Figma export this matches ~98% of pixels; the
|
|
20
|
+
* residue is the hollow Figma leaves between the bubble's underside and the
|
|
21
|
+
* tail, which is below one pixel at this size.
|
|
22
|
+
*/
|
|
23
|
+
export const ChatIcon = createIcon({
|
|
24
|
+
displayName: 'ChatIcon',
|
|
25
|
+
viewBox: '0 0 22 22',
|
|
26
|
+
defaultWidth: 22,
|
|
27
|
+
defaultHeight: 22,
|
|
28
|
+
path: ({ color }) => (
|
|
29
|
+
<path
|
|
30
|
+
fill="none"
|
|
31
|
+
stroke={color}
|
|
32
|
+
strokeLinecap="round"
|
|
33
|
+
strokeLinejoin="round"
|
|
34
|
+
strokeWidth="1.9"
|
|
35
|
+
d="M19.83 16.1A10.2 10.2 0 1 0 14.15 20.7L21 21Z"
|
|
36
|
+
/>
|
|
37
|
+
),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const SendIcon = createIcon({
|
|
41
|
+
displayName: 'SendIcon',
|
|
42
|
+
viewBox: '0 0 18 18',
|
|
43
|
+
defaultWidth: 18,
|
|
44
|
+
defaultHeight: 18,
|
|
45
|
+
path: ({ color }) => (
|
|
46
|
+
<path
|
|
47
|
+
stroke={color}
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeLinejoin="round"
|
|
50
|
+
strokeWidth="1.6"
|
|
51
|
+
d="M16.5 1.5 8.25 9.75M16.5 1.5l-5.25 15-3-6.75-6.75-3 15-5.25Z"
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* New conversation: a square-cornered message box with a center-bottom tail and
|
|
58
|
+
* a plus inside.
|
|
59
|
+
*
|
|
60
|
+
* Squared off, not rounded — the design's box runs the full 16px width with hard
|
|
61
|
+
* corners, and the tail drops from the middle of the bottom edge rather than
|
|
62
|
+
* from a corner.
|
|
63
|
+
*/
|
|
64
|
+
export const MessagePlusIcon = createIcon({
|
|
65
|
+
displayName: 'MessagePlusIcon',
|
|
66
|
+
viewBox: '0 0 16 16',
|
|
67
|
+
defaultWidth: 16,
|
|
68
|
+
defaultHeight: 16,
|
|
69
|
+
path: ({ color }) => (
|
|
70
|
+
<g stroke={color} fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.1">
|
|
71
|
+
<path d="M0.75 0.75H15.25V12.05H9.1L7.6 15.1L6.1 12.05H0.75Z" />
|
|
72
|
+
<path d="M7.6 4.2V9.2M5.1 6.7H10.1" />
|
|
73
|
+
</g>
|
|
74
|
+
),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Conversations: two squares, each with a rule to its right — a list whose
|
|
79
|
+
* bullets are boxes, not the plain stack of lines this originally drew.
|
|
80
|
+
*/
|
|
81
|
+
export const ListLayoutIcon = createIcon({
|
|
82
|
+
displayName: 'ListLayoutIcon',
|
|
83
|
+
viewBox: '0 0 16 16',
|
|
84
|
+
defaultWidth: 16,
|
|
85
|
+
defaultHeight: 16,
|
|
86
|
+
path: ({ color }) => (
|
|
87
|
+
<g stroke={color} fill="none" strokeLinecap="square" strokeWidth="1.1">
|
|
88
|
+
<path d="M0.55 1.55H4.45V5.45H0.55Z" />
|
|
89
|
+
<path d="M0.55 10.55H4.45V14.45H0.55Z" />
|
|
90
|
+
<path d="M7.1 3.5H15.45M7.1 12.5H15.45" />
|
|
91
|
+
</g>
|
|
92
|
+
),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export const CloseIcon = createIcon({
|
|
96
|
+
displayName: 'CloseIcon',
|
|
97
|
+
viewBox: '0 0 16 16',
|
|
98
|
+
defaultWidth: 16,
|
|
99
|
+
defaultHeight: 16,
|
|
100
|
+
path: ({ color }) => (
|
|
101
|
+
<path
|
|
102
|
+
stroke={color}
|
|
103
|
+
strokeLinecap="round"
|
|
104
|
+
strokeWidth="1.2"
|
|
105
|
+
d="M4.6 4.6 11.4 11.4M11.4 4.6 4.6 11.4"
|
|
106
|
+
/>
|
|
107
|
+
),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export const InfoIcon = createIcon({
|
|
111
|
+
displayName: 'InfoIcon',
|
|
112
|
+
viewBox: '0 0 12 12',
|
|
113
|
+
defaultWidth: 12,
|
|
114
|
+
defaultHeight: 12,
|
|
115
|
+
path: ({ color }) => (
|
|
116
|
+
<g stroke={color} fill="none" strokeLinecap="round" strokeWidth="1.1">
|
|
117
|
+
<circle cx="6" cy="6" r="5" />
|
|
118
|
+
<path d="M6 5.2v3.1M6 3.6h.005" />
|
|
119
|
+
</g>
|
|
120
|
+
),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export const TrashIcon = createIcon({
|
|
124
|
+
displayName: 'TrashIcon',
|
|
125
|
+
viewBox: '0 0 16 16',
|
|
126
|
+
defaultWidth: 16,
|
|
127
|
+
defaultHeight: 16,
|
|
128
|
+
path: ({ color }) => (
|
|
129
|
+
<g stroke={color} fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.1">
|
|
130
|
+
<path d="M2.5 4.2h11M6.2 4.2V2.6h3.6v1.6" />
|
|
131
|
+
<path d="M3.9 4.2 4.6 13.4h6.8l0.7-9.2" />
|
|
132
|
+
<path d="M6.6 6.8v4M9.4 6.8v4" />
|
|
133
|
+
</g>
|
|
134
|
+
),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
export const BackIcon = createIcon({
|
|
138
|
+
displayName: 'BackIcon',
|
|
139
|
+
viewBox: '0 0 16 16',
|
|
140
|
+
defaultWidth: 16,
|
|
141
|
+
defaultHeight: 16,
|
|
142
|
+
path: ({ color }) => (
|
|
143
|
+
<path
|
|
144
|
+
stroke={color}
|
|
145
|
+
strokeLinecap="round"
|
|
146
|
+
strokeLinejoin="round"
|
|
147
|
+
strokeWidth="1.4"
|
|
148
|
+
d="M10 3 5 8l5 5"
|
|
149
|
+
/>
|
|
150
|
+
),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
/** The filled square a running turn is stopped with, matching the send button's box. */
|
|
154
|
+
export const StopIcon = createIcon({
|
|
155
|
+
displayName: 'StopIcon',
|
|
156
|
+
viewBox: '0 0 18 18',
|
|
157
|
+
defaultWidth: 18,
|
|
158
|
+
defaultHeight: 18,
|
|
159
|
+
path: ({ color }) => <rect x="4" y="4" width="10" height="10" rx="1" fill={color} stroke="none" />,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export const TickIcon = createIcon({
|
|
163
|
+
displayName: 'TickIcon',
|
|
164
|
+
viewBox: '0 0 12 12',
|
|
165
|
+
defaultWidth: 12,
|
|
166
|
+
defaultHeight: 12,
|
|
167
|
+
path: ({ color }) => (
|
|
168
|
+
<path
|
|
169
|
+
stroke={color}
|
|
170
|
+
strokeLinecap="round"
|
|
171
|
+
strokeLinejoin="round"
|
|
172
|
+
strokeWidth="1.5"
|
|
173
|
+
d="m2 6.4 2.6 2.6L10 3.5"
|
|
174
|
+
/>
|
|
175
|
+
),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
export const WarningIcon = createIcon({
|
|
179
|
+
displayName: 'WarningIcon',
|
|
180
|
+
viewBox: '0 0 12 12',
|
|
181
|
+
defaultWidth: 12,
|
|
182
|
+
defaultHeight: 12,
|
|
183
|
+
path: ({ color }) => (
|
|
184
|
+
<g stroke={color} strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.3">
|
|
185
|
+
<path d="M6 1.5 11 10.5H1L6 1.5Z" />
|
|
186
|
+
<path d="M6 5v2.2M6 9h.005" />
|
|
187
|
+
</g>
|
|
188
|
+
),
|
|
189
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { JSX } from 'react'
|
|
2
|
+
|
|
3
|
+
import { AGENT_LABELS } from '../branding'
|
|
4
|
+
import { ChatIcon } from './icons'
|
|
5
|
+
|
|
6
|
+
export type LauncherFabProps = {
|
|
7
|
+
label?: string
|
|
8
|
+
onClick: VoidFunction
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The collapsed state: a 48px round launcher in the bottom-right corner. */
|
|
12
|
+
export const LauncherFab = ({ label = AGENT_LABELS.open, onClick }: LauncherFabProps): JSX.Element => (
|
|
13
|
+
<button type="button" className="mdk-agent-fab" aria-label={label} onClick={onClick}>
|
|
14
|
+
<ChatIcon size={22} />
|
|
15
|
+
</button>
|
|
16
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JSX } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { LeakedToolCall } from '../core/prose'
|
|
4
|
+
|
|
5
|
+
export type LeakedToolCallNoticeProps = {
|
|
6
|
+
call: LeakedToolCall
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shown in place of an answer that is really a tool call the model failed to
|
|
11
|
+
* emit correctly.
|
|
12
|
+
*
|
|
13
|
+
* The agent's tool loop is prompt-based: the model is asked to reply with only
|
|
14
|
+
* `{"tool": …}`, and the producer classifies the reply as a tool call from its
|
|
15
|
+
* first character being `{`. A model that wraps the JSON in a ```json fence
|
|
16
|
+
* leads with a backtick, so the call is classified as prose and streamed to the
|
|
17
|
+
* operator verbatim. Rendering that raw is the most confusing failure in the
|
|
18
|
+
* stack; naming it is cheap and makes the retry obvious.
|
|
19
|
+
*/
|
|
20
|
+
export const LeakedToolCallNotice = ({ call }: LeakedToolCallNoticeProps): JSX.Element => (
|
|
21
|
+
<div className="mdk-agent-leaked" role="status">
|
|
22
|
+
<p className="mdk-agent-leaked__title">The model returned a malformed tool call.</p>
|
|
23
|
+
<p className="mdk-agent-leaked__body">
|
|
24
|
+
{`It tried to call `}
|
|
25
|
+
<code className="mdk-agent-leaked__tool">{call.tool}</code>
|
|
26
|
+
{` but wrapped the request in formatting, so it was never run. Ask again.`}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, JSX } from 'react'
|
|
2
|
+
import type { ExtraProps } from 'react-markdown'
|
|
3
|
+
|
|
4
|
+
export type MarkdownLinkProps = ComponentPropsWithoutRef<'a'> & ExtraProps
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A link inside an answer.
|
|
8
|
+
*
|
|
9
|
+
* Anything pointing away from the document opens in a new tab: the panel is docked
|
|
10
|
+
* over whatever page the operator is working on, and following a link in place
|
|
11
|
+
* would navigate the app out from under them mid-conversation. An in-document
|
|
12
|
+
* target — a GFM footnote reference, `#...` — has to stay in the panel to work at
|
|
13
|
+
* all, so it is left alone.
|
|
14
|
+
*
|
|
15
|
+
* `react-markdown` blocks `javascript:` hrefs in its default `urlTransform`, so
|
|
16
|
+
* this is about not losing the operator's place rather than about safety.
|
|
17
|
+
*/
|
|
18
|
+
export const MarkdownLink = ({ href, children, node, ...rest }: MarkdownLinkProps): JSX.Element => {
|
|
19
|
+
const isInDocument = href?.startsWith('#') ?? false
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<a
|
|
23
|
+
href={href}
|
|
24
|
+
{...(isInDocument ? {} : { target: '_blank', rel: 'noopener noreferrer' })}
|
|
25
|
+
{...rest}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</a>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { JSX } from 'react'
|
|
2
|
+
import type { Components } from 'react-markdown'
|
|
3
|
+
import { memo } from 'react'
|
|
4
|
+
import Markdown from 'react-markdown'
|
|
5
|
+
import remarkGfm from 'remark-gfm'
|
|
6
|
+
|
|
7
|
+
import { unwrapWholeAnswerFence } from '../core/markdown'
|
|
8
|
+
import { CodeBlock } from './code-block'
|
|
9
|
+
import { MarkdownLink } from './markdown-link'
|
|
10
|
+
import { rehypeHighlightSubset } from './rehype-highlight-subset'
|
|
11
|
+
|
|
12
|
+
// Module scope, not inline: a fresh array each render makes react-markdown
|
|
13
|
+
// rebuild its processor for every streamed token.
|
|
14
|
+
const REMARK_PLUGINS = [remarkGfm]
|
|
15
|
+
const REHYPE_PLUGINS = [rehypeHighlightSubset]
|
|
16
|
+
const COMPONENTS: Components = { a: MarkdownLink, pre: CodeBlock }
|
|
17
|
+
|
|
18
|
+
export type MarkdownTextProps = {
|
|
19
|
+
text: string
|
|
20
|
+
/**
|
|
21
|
+
* A turn still streaming renders as plain text. A half-written fence parses as
|
|
22
|
+
* an unterminated code block, so markdown mid-stream flickers between layouts,
|
|
23
|
+
* and re-parsing the whole answer on every token is wasted work either way.
|
|
24
|
+
* Switch on when the turn finishes.
|
|
25
|
+
*/
|
|
26
|
+
streaming?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Renders an assistant answer.
|
|
31
|
+
*
|
|
32
|
+
* Raw HTML is deliberately not enabled — `react-markdown` ignores it unless
|
|
33
|
+
* `rehype-raw` is added, which is what keeps untrusted model output from
|
|
34
|
+
* injecting markup. Do not add `rehype-raw` here.
|
|
35
|
+
*
|
|
36
|
+
* Memoized: a live turn re-renders the transcript, and reparsing every settled
|
|
37
|
+
* answer for each token of the current one is the panel's most expensive
|
|
38
|
+
* avoidable work.
|
|
39
|
+
*/
|
|
40
|
+
const MarkdownTextView = ({ text, streaming = false }: MarkdownTextProps): JSX.Element => {
|
|
41
|
+
if (streaming) {
|
|
42
|
+
return <div className="mdk-agent-markdown mdk-agent-markdown--streaming">{text}</div>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="mdk-agent-markdown">
|
|
47
|
+
<Markdown
|
|
48
|
+
remarkPlugins={REMARK_PLUGINS}
|
|
49
|
+
rehypePlugins={REHYPE_PLUGINS}
|
|
50
|
+
components={COMPONENTS}
|
|
51
|
+
>
|
|
52
|
+
{unwrapWholeAnswerFence(text)}
|
|
53
|
+
</Markdown>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const MarkdownText = memo(MarkdownTextView)
|