@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
package/package.json
CHANGED
|
@@ -1 +1,105 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@tetherto/mdk-ui-agent",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.12.1",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "Injectable operator Co-pilot chat UI for the MDK agent gateway plugin. Streams the six-event agent contract over SSE and renders it as a docked panel or a full-page route.",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"sideEffects": [
|
|
9
|
+
"*.css",
|
|
10
|
+
"*.scss"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./core": {
|
|
19
|
+
"types": "./dist/core/index.d.ts",
|
|
20
|
+
"import": "./dist/core/index.js",
|
|
21
|
+
"default": "./dist/core/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./chat-page": {
|
|
24
|
+
"types": "./dist/entries/chat-page.d.ts",
|
|
25
|
+
"import": "./dist/entries/chat-page.js",
|
|
26
|
+
"default": "./dist/entries/chat-page.js"
|
|
27
|
+
},
|
|
28
|
+
"./panel": {
|
|
29
|
+
"types": "./dist/components/co-pilot-panel.d.ts",
|
|
30
|
+
"import": "./dist/components/co-pilot-panel.js",
|
|
31
|
+
"default": "./dist/components/co-pilot-panel.js"
|
|
32
|
+
},
|
|
33
|
+
"./styles.css": "./dist/styles.css",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"main": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/tetherto/mdk.git",
|
|
41
|
+
"directory": "ui/packages/ui-agent"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"src",
|
|
49
|
+
"!src/**/*.test.*",
|
|
50
|
+
"!src/**/specs",
|
|
51
|
+
"!src/**/test-utils"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "npm run check:styles && npm run build:ts && npm run build:scss",
|
|
55
|
+
"prepublishOnly": "npm run build",
|
|
56
|
+
"build:ts": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
57
|
+
"build:scss": "vite build",
|
|
58
|
+
"check:styles": "node scripts/check-style-forwards.mjs",
|
|
59
|
+
"clean": "rimraf dist node_modules .turbo",
|
|
60
|
+
"dev": "concurrently -n ts,scss -c cyan,magenta \"npm run dev:ts\" \"npm run dev:scss\"",
|
|
61
|
+
"dev:ts": "tsc --watch -p tsconfig.build.json",
|
|
62
|
+
"dev:scss": "vite build --watch",
|
|
63
|
+
"lint": "oxlint",
|
|
64
|
+
"lint:fix": "oxlint --fix",
|
|
65
|
+
"format": "oxlint --fix",
|
|
66
|
+
"test": "vitest run --config vitest.config.js",
|
|
67
|
+
"test:watch": "vitest --watch",
|
|
68
|
+
"test:coverage": "vitest run --config vitest.config.js --coverage",
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"watch": "npm run dev",
|
|
71
|
+
"watch:ts": "npm run dev:ts",
|
|
72
|
+
"watch:scss": "npm run dev:scss"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"react": "^19.0.0",
|
|
76
|
+
"react-dom": "^19.0.0"
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"@tetherto/mdk-react-adapter": "^0.12.1",
|
|
80
|
+
"@tetherto/mdk-react-devkit": "^0.12.1",
|
|
81
|
+
"@tetherto/mdk-ui-foundation": "^0.12.1",
|
|
82
|
+
"highlight.js": "^11.11.1",
|
|
83
|
+
"lodash": "^4.18.1",
|
|
84
|
+
"lowlight": "^3.3.0",
|
|
85
|
+
"react-markdown": "^10.1.0",
|
|
86
|
+
"remark-gfm": "^4.0.1",
|
|
87
|
+
"zustand": "^5.0.0"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@tanstack/react-query": "^5.59.0",
|
|
91
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
92
|
+
"@testing-library/react": "^16.1.0",
|
|
93
|
+
"@testing-library/user-event": "^14.5.2",
|
|
94
|
+
"@types/lodash": "^4.17.24",
|
|
95
|
+
"@types/react": "^19.2.14",
|
|
96
|
+
"@types/react-dom": "^19.2.3",
|
|
97
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
98
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
99
|
+
"happy-dom": "^21.0.4",
|
|
100
|
+
"react": "^19.2.0",
|
|
101
|
+
"react-dom": "^19.2.0",
|
|
102
|
+
"sass": "^1.83.0",
|
|
103
|
+
"vitest": "^4.1.2"
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/branding.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The product name, in one place.
|
|
3
|
+
*
|
|
4
|
+
* "Co-pilot" is a placeholder — it collides with GitHub Copilot and the final
|
|
5
|
+
* name has not been decided. Every user-visible string in the package derives
|
|
6
|
+
* from `AGENT_NAME` below, so renaming it is a one-line change here.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately kept out of the identifiers: the package is `@tetherto/mdk-ui-agent`,
|
|
9
|
+
* the CSS classes are `mdk-agent-*`, and the storage key is `mdk-ui-agent:*`.
|
|
10
|
+
* None of those need to move when the name does. The exported component names
|
|
11
|
+
* (`CoPilot`, `CoPilotProps`, …) are the only remaining mention, and they are a
|
|
12
|
+
* mechanical rename plus a deprecated alias whenever the decision lands.
|
|
13
|
+
*
|
|
14
|
+
* Hosts that want a different name per deployment do not need to fork this:
|
|
15
|
+
* pass `title` to `CoPilot` / `ChatUIEntry`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const AGENT_NAME = 'Co-pilot'
|
|
19
|
+
|
|
20
|
+
/** How the operator's own turns are labelled. */
|
|
21
|
+
export const OPERATOR_NAME = 'Operator'
|
|
22
|
+
|
|
23
|
+
const lower = AGENT_NAME.toLowerCase()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Accessible names and headings, derived so they stay consistent with
|
|
27
|
+
* {@link AGENT_NAME}.
|
|
28
|
+
*/
|
|
29
|
+
export const AGENT_LABELS = {
|
|
30
|
+
name: AGENT_NAME,
|
|
31
|
+
operator: OPERATOR_NAME,
|
|
32
|
+
open: `Open ${lower}`,
|
|
33
|
+
close: `Close ${lower}`,
|
|
34
|
+
composer: `Message the ${lower}`,
|
|
35
|
+
conversations: 'Conversations',
|
|
36
|
+
newConversation: 'New conversation',
|
|
37
|
+
backToConversation: 'Back to conversation',
|
|
38
|
+
approvalRequired: 'Approval required',
|
|
39
|
+
placeholder: 'Ask about your fleet...',
|
|
40
|
+
} as const
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Button } from '@tetherto/mdk-react-devkit/primitives'
|
|
2
|
+
import isNil from 'lodash/isNil'
|
|
3
|
+
import type { JSX } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { ToolLabels } from '../core/tool-label'
|
|
6
|
+
import type { PendingApproval } from '../core/turn'
|
|
7
|
+
import { AGENT_LABELS } from '../branding'
|
|
8
|
+
import { humanizeToolName, toolLabel } from '../core/tool-label'
|
|
9
|
+
|
|
10
|
+
/** Args the reference site tools use to name their target, most specific first. */
|
|
11
|
+
const TARGET_KEYS = ['ref', 'deviceId', 'device', 'id', 'name'] as const
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Friendlier names for argument keys the reference tools use.
|
|
15
|
+
*
|
|
16
|
+
* `action` matters most: without it the card shows two rows both labelled
|
|
17
|
+
* "Action" — the tool itself, and the tool's `action` argument — which is what
|
|
18
|
+
* the design calls "Command". Anything not listed is humanized from its key.
|
|
19
|
+
*/
|
|
20
|
+
const ARG_LABELS: Record<string, string> = {
|
|
21
|
+
action: 'Command',
|
|
22
|
+
mode: 'Mode',
|
|
23
|
+
family: 'Family',
|
|
24
|
+
state: 'State',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Shown for an argument the tool declared but sent no value for. */
|
|
28
|
+
const EMPTY_VALUE = '—'
|
|
29
|
+
|
|
30
|
+
const describeValue = (value: unknown): string => {
|
|
31
|
+
if (isNil(value)) return EMPTY_VALUE
|
|
32
|
+
if (typeof value === 'string') return value
|
|
33
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
34
|
+
return JSON.stringify(value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ApprovalRow = { label: string, value: string }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Flattens the tool args into the Action / Device / Command rows the design
|
|
41
|
+
* shows.
|
|
42
|
+
*
|
|
43
|
+
* Tools are supplied by whichever MCP server the operator points the agent at,
|
|
44
|
+
* so no argument name is guaranteed. The target is looked up across the names
|
|
45
|
+
* the reference tools use and everything else is listed as-is, which degrades to
|
|
46
|
+
* a plain key/value dump for an unknown tool rather than to nothing.
|
|
47
|
+
*/
|
|
48
|
+
export function describeApproval(approval: PendingApproval, labels?: ToolLabels): ApprovalRow[] {
|
|
49
|
+
const rows: ApprovalRow[] = [{ label: 'Action', value: toolLabel(approval.name, labels) }]
|
|
50
|
+
|
|
51
|
+
const targetKey = TARGET_KEYS.find((key) => key in approval.args)
|
|
52
|
+
if (targetKey) rows.push({ label: 'Device', value: describeValue(approval.args[targetKey]) })
|
|
53
|
+
|
|
54
|
+
for (const [key, value] of Object.entries(approval.args)) {
|
|
55
|
+
if (key === targetKey) continue
|
|
56
|
+
rows.push({ label: ARG_LABELS[key] ?? humanizeToolName(key), value: describeValue(value) })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return rows
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The one-line "here is what you are about to allow", shown above the card.
|
|
64
|
+
*
|
|
65
|
+
* Synthesized rather than streamed: `pending_approval` arrives between a
|
|
66
|
+
* `tool_call` and its `tool_result`, and the model does not emit a single token
|
|
67
|
+
* until after the tool returns — so at the moment the operator is asked to
|
|
68
|
+
* decide, the agent has said nothing at all. The design shows a sentence there,
|
|
69
|
+
* and it has to be built from the call itself.
|
|
70
|
+
*/
|
|
71
|
+
export function describeApprovalIntent(approval: PendingApproval, labels?: ToolLabels): string {
|
|
72
|
+
const action = toolLabel(approval.name, labels).toLowerCase()
|
|
73
|
+
const targetKey = TARGET_KEYS.find((key) => key in approval.args)
|
|
74
|
+
const target = targetKey ? describeValue(approval.args[targetKey]) : null
|
|
75
|
+
|
|
76
|
+
return target
|
|
77
|
+
? `This runs ${action} on ${target}. Review and approve to proceed.`
|
|
78
|
+
: `This runs ${action}. Review and approve to proceed.`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type ApprovalCardProps = {
|
|
82
|
+
approval: PendingApproval
|
|
83
|
+
toolLabels?: ToolLabels
|
|
84
|
+
/** A decision has been sent; the stream has not resumed yet. */
|
|
85
|
+
pending?: boolean
|
|
86
|
+
onDecide: (approved: boolean) => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The inline approval prompt. The stream is paused behind this.
|
|
91
|
+
*
|
|
92
|
+
* Rendered in the transcript rather than as a blocking modal, because it is not
|
|
93
|
+
* always the operator who resolves it: an approval left unanswered for
|
|
94
|
+
* `agent.approvalTimeoutMs` is auto-rejected server-side and the stream resumes
|
|
95
|
+
* on its own. The card must be able to disappear because the turn moved on — a
|
|
96
|
+
* modal that only closes on click would strand the operator on a dead prompt.
|
|
97
|
+
* Unmounting is driven by the reducer clearing `pendingApproval`.
|
|
98
|
+
*/
|
|
99
|
+
export const ApprovalCard = ({
|
|
100
|
+
approval,
|
|
101
|
+
toolLabels,
|
|
102
|
+
pending = false,
|
|
103
|
+
onDecide,
|
|
104
|
+
}: ApprovalCardProps): JSX.Element => {
|
|
105
|
+
const rows = describeApproval(approval, toolLabels)
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<section className="mdk-agent-approval" aria-label={AGENT_LABELS.approvalRequired}>
|
|
109
|
+
<h3 className="mdk-agent-approval__title">{AGENT_LABELS.approvalRequired}</h3>
|
|
110
|
+
|
|
111
|
+
<dl className="mdk-agent-approval__details">
|
|
112
|
+
{rows.map((row) => (
|
|
113
|
+
<div className="mdk-agent-approval__row" key={row.label}>
|
|
114
|
+
<dt className="mdk-agent-approval__label">{row.label}</dt>
|
|
115
|
+
<dd className="mdk-agent-approval__value">{row.value}</dd>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</dl>
|
|
119
|
+
|
|
120
|
+
<div className="mdk-agent-approval__actions">
|
|
121
|
+
<Button
|
|
122
|
+
variant="outline"
|
|
123
|
+
size="sm"
|
|
124
|
+
disabled={pending}
|
|
125
|
+
onClick={() => onDecide(false)}
|
|
126
|
+
>
|
|
127
|
+
Reject
|
|
128
|
+
</Button>
|
|
129
|
+
<Button
|
|
130
|
+
variant="primary"
|
|
131
|
+
size="sm"
|
|
132
|
+
loading={pending}
|
|
133
|
+
disabled={pending}
|
|
134
|
+
onClick={() => onDecide(true)}
|
|
135
|
+
>
|
|
136
|
+
Approve
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Loader } from '@tetherto/mdk-react-devkit/primitives'
|
|
2
|
+
import type { JSX } from 'react'
|
|
3
|
+
import { memo, useMemo } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { ToolLabels } from '../core/tool-label'
|
|
6
|
+
import type { PendingApproval, ToolStep } from '../core/turn'
|
|
7
|
+
import { AGENT_LABELS } from '../branding'
|
|
8
|
+
import { describeAgentError } from '../core/error-text'
|
|
9
|
+
import { parseLeakedToolCall } from '../core/prose'
|
|
10
|
+
import { isSmallTalk } from '../core/small-talk'
|
|
11
|
+
import { ApprovalCard, describeApprovalIntent } from './approval-card'
|
|
12
|
+
import { CollapsibleAnswer } from './collapsible-answer'
|
|
13
|
+
import { LeakedToolCallNotice } from './leaked-tool-call-notice'
|
|
14
|
+
import { NoToolsNote } from './no-tools-note'
|
|
15
|
+
import { ToolChip } from './tool-chip'
|
|
16
|
+
|
|
17
|
+
export type AssistantMessageProps = {
|
|
18
|
+
text: string
|
|
19
|
+
/** The operator message this answers — only read to decide whether it was a fleet question. */
|
|
20
|
+
question?: string
|
|
21
|
+
tools?: ToolStep[]
|
|
22
|
+
/** The `error` the turn ended on. A raw `ERR_*` code is mapped before it is shown. */
|
|
23
|
+
error?: string | null
|
|
24
|
+
/** The turn was stopped before it finished, so this is a part of an answer. */
|
|
25
|
+
interrupted?: boolean
|
|
26
|
+
/** The turn is still running: render plain text and the thinking indicator. */
|
|
27
|
+
streaming?: boolean
|
|
28
|
+
toolLabels?: ToolLabels
|
|
29
|
+
approval?: PendingApproval | null
|
|
30
|
+
isDecisionPending?: boolean
|
|
31
|
+
onDecide?: (approved: boolean) => void
|
|
32
|
+
/** Offered on a failed turn, so the operator does not retype the question. */
|
|
33
|
+
onRetry?: VoidFunction
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const AssistantMessageView = ({
|
|
37
|
+
text,
|
|
38
|
+
question = '',
|
|
39
|
+
tools = [],
|
|
40
|
+
error = null,
|
|
41
|
+
interrupted = false,
|
|
42
|
+
streaming = false,
|
|
43
|
+
toolLabels,
|
|
44
|
+
approval = null,
|
|
45
|
+
isDecisionPending = false,
|
|
46
|
+
onDecide,
|
|
47
|
+
onRetry,
|
|
48
|
+
}: AssistantMessageProps): JSX.Element => {
|
|
49
|
+
// Only worth checking once the turn has settled — a partial answer can look
|
|
50
|
+
// like a bare JSON object simply because the rest has not arrived yet.
|
|
51
|
+
const leaked = useMemo(() => (streaming ? null : parseLeakedToolCall(text)), [streaming, text])
|
|
52
|
+
|
|
53
|
+
const hasText = text.trim().length > 0
|
|
54
|
+
const isThinking = streaming && !hasText && approval === null
|
|
55
|
+
|
|
56
|
+
// The chip for the gated step is suppressed while its card is up: the card
|
|
57
|
+
// already names the action, the device and the command, so the chip would just
|
|
58
|
+
// restate it directly above.
|
|
59
|
+
const chips = approval ? tools.filter((step) => step.id !== approval.toolStepId) : tools
|
|
60
|
+
|
|
61
|
+
// Stated only once the turn has settled — mid-stream the tools may simply not
|
|
62
|
+
// have been called *yet*. A leaked call and an errored turn already say their
|
|
63
|
+
// own thing, so the note would only add noise there. Nor is it said against a
|
|
64
|
+
// greeting: consulting nothing is the right answer to "hey", and the marker
|
|
65
|
+
// was appearing on every one of them.
|
|
66
|
+
const answeredWithoutTools
|
|
67
|
+
= !streaming
|
|
68
|
+
&& tools.length === 0
|
|
69
|
+
&& hasText
|
|
70
|
+
&& leaked === null
|
|
71
|
+
&& !error
|
|
72
|
+
&& !isSmallTalk(question)
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="mdk-agent-message mdk-agent-message--assistant">
|
|
76
|
+
<p className="mdk-agent-message__role">
|
|
77
|
+
<span className="mdk-agent-message__dot" aria-hidden="true" />
|
|
78
|
+
{AGENT_LABELS.name}
|
|
79
|
+
{answeredWithoutTools ? <NoToolsNote /> : null}
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
{chips.length > 0
|
|
83
|
+
? (
|
|
84
|
+
<div className="mdk-agent-message__tools">
|
|
85
|
+
{chips.map((step) => (
|
|
86
|
+
<ToolChip key={step.id} step={step} toolLabels={toolLabels} />
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
: null}
|
|
91
|
+
|
|
92
|
+
{approval
|
|
93
|
+
? (
|
|
94
|
+
<p className="mdk-agent-message__intent">
|
|
95
|
+
{describeApprovalIntent(approval, toolLabels)}
|
|
96
|
+
</p>
|
|
97
|
+
)
|
|
98
|
+
: null}
|
|
99
|
+
|
|
100
|
+
{/* `inline`, or the loader's 200px block height strands the dots ~100px below this
|
|
101
|
+
label with dead space between, and the turn reads as frozen while it is working. */}
|
|
102
|
+
{isThinking ? <Loader inline size={6} count={3} color="orange" /> : null}
|
|
103
|
+
|
|
104
|
+
{hasText && leaked === null ? <CollapsibleAnswer text={text} streaming={streaming} /> : null}
|
|
105
|
+
{hasText && leaked !== null ? <LeakedToolCallNotice call={leaked} /> : null}
|
|
106
|
+
|
|
107
|
+
{approval && onDecide
|
|
108
|
+
? (
|
|
109
|
+
<ApprovalCard
|
|
110
|
+
approval={approval}
|
|
111
|
+
toolLabels={toolLabels}
|
|
112
|
+
pending={isDecisionPending}
|
|
113
|
+
onDecide={onDecide}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
: null}
|
|
117
|
+
|
|
118
|
+
{interrupted && !error
|
|
119
|
+
? <p className="mdk-agent-message__note">Stopped early — this is not the whole answer.</p>
|
|
120
|
+
: null}
|
|
121
|
+
|
|
122
|
+
{error
|
|
123
|
+
? (
|
|
124
|
+
<p className="mdk-agent-message__error" role="alert">
|
|
125
|
+
{describeAgentError(error)}
|
|
126
|
+
{onRetry
|
|
127
|
+
? (
|
|
128
|
+
<button type="button" className="mdk-agent-message__retry" onClick={onRetry}>
|
|
129
|
+
Try again
|
|
130
|
+
</button>
|
|
131
|
+
)
|
|
132
|
+
: null}
|
|
133
|
+
</p>
|
|
134
|
+
)
|
|
135
|
+
: null}
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const AssistantMessage = memo(AssistantMessageView)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { JSX, ReactNode } from 'react'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { ToolLabels } from '../core/tool-label'
|
|
5
|
+
import type { UseAgentChatResult } from '../hooks/use-agent-chat'
|
|
6
|
+
import type { PanelView } from './panel-header'
|
|
7
|
+
import { AGENT_LABELS } from '../branding'
|
|
8
|
+
import { formatConversationSummary } from '../core/format'
|
|
9
|
+
import { Composer } from './composer'
|
|
10
|
+
import { ConversationList } from './conversation-list'
|
|
11
|
+
import { MessageList } from './message-list'
|
|
12
|
+
import { PANEL_VIEW, PanelHeader } from './panel-header'
|
|
13
|
+
|
|
14
|
+
export type CoPilotPanelProps = {
|
|
15
|
+
/** The single hook instance. Both entries own one and pass it down. */
|
|
16
|
+
chat: UseAgentChatResult
|
|
17
|
+
title?: string
|
|
18
|
+
status?: ReactNode
|
|
19
|
+
toolLabels?: ToolLabels
|
|
20
|
+
placeholder?: string
|
|
21
|
+
/** Omitted by the route page, which has nothing to collapse into. */
|
|
22
|
+
onClose?: VoidFunction | undefined
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The panel body, shared by the docked overlay and the full-page route. */
|
|
27
|
+
export const CoPilotPanel = ({
|
|
28
|
+
chat,
|
|
29
|
+
title = AGENT_LABELS.name,
|
|
30
|
+
status,
|
|
31
|
+
toolLabels,
|
|
32
|
+
placeholder,
|
|
33
|
+
onClose,
|
|
34
|
+
className,
|
|
35
|
+
}: CoPilotPanelProps): JSX.Element => {
|
|
36
|
+
const [view, setView] = useState<PanelView>(PANEL_VIEW.CHAT)
|
|
37
|
+
|
|
38
|
+
const toggleConversations = useCallback(() => {
|
|
39
|
+
setView((current) => (current === PANEL_VIEW.CHAT ? PANEL_VIEW.CONVERSATIONS : PANEL_VIEW.CHAT))
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
const handleNewConversation = useCallback(() => {
|
|
43
|
+
chat.newConversation()
|
|
44
|
+
setView(PANEL_VIEW.CHAT)
|
|
45
|
+
}, [chat])
|
|
46
|
+
|
|
47
|
+
const handleSelectConversation = useCallback(
|
|
48
|
+
(id: string) => {
|
|
49
|
+
chat.selectConversation(id)
|
|
50
|
+
setView(PANEL_VIEW.CHAT)
|
|
51
|
+
},
|
|
52
|
+
[chat],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const isConversations = view === PANEL_VIEW.CONVERSATIONS
|
|
56
|
+
|
|
57
|
+
// The conversations view describes the list rather than the connection — the
|
|
58
|
+
// host-supplied status is about the agent, which is not what is on screen.
|
|
59
|
+
const headerStatus = isConversations
|
|
60
|
+
? formatConversationSummary(chat.conversations.length, chat.isStreaming)
|
|
61
|
+
: status
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section className={`mdk-agent-panel${className ? ` ${className}` : ''}`} aria-label={title}>
|
|
65
|
+
<PanelHeader
|
|
66
|
+
view={view}
|
|
67
|
+
title={isConversations ? AGENT_LABELS.conversations : title}
|
|
68
|
+
status={headerStatus}
|
|
69
|
+
actionsDisabled={chat.isStreaming}
|
|
70
|
+
onNewConversation={handleNewConversation}
|
|
71
|
+
onToggleConversations={toggleConversations}
|
|
72
|
+
onClose={onClose}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
{isConversations
|
|
76
|
+
? (
|
|
77
|
+
<ConversationList
|
|
78
|
+
conversations={chat.conversations}
|
|
79
|
+
activeId={chat.active?.id ?? null}
|
|
80
|
+
runningId={chat.isStreaming ? (chat.active?.id ?? null) : null}
|
|
81
|
+
onSelect={handleSelectConversation}
|
|
82
|
+
onCreate={handleNewConversation}
|
|
83
|
+
onDelete={chat.removeConversation}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
: (
|
|
87
|
+
<>
|
|
88
|
+
<MessageList
|
|
89
|
+
messages={chat.messages}
|
|
90
|
+
live={chat.live}
|
|
91
|
+
toolLabels={toolLabels}
|
|
92
|
+
pendingApproval={chat.pendingApproval}
|
|
93
|
+
isDecisionPending={chat.isDecisionPending}
|
|
94
|
+
onDecide={chat.decide}
|
|
95
|
+
onRetry={chat.retry}
|
|
96
|
+
/>
|
|
97
|
+
<Composer
|
|
98
|
+
disabled={!chat.canSend}
|
|
99
|
+
isStreaming={chat.isStreaming}
|
|
100
|
+
placeholder={placeholder}
|
|
101
|
+
focusKey={chat.active?.id ?? null}
|
|
102
|
+
onSend={chat.send}
|
|
103
|
+
onStop={chat.abort}
|
|
104
|
+
/>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
</section>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, JSX } from 'react'
|
|
2
|
+
import type { ExtraProps } from 'react-markdown'
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
/** How long the button confirms the copy before going back to its label. */
|
|
6
|
+
const COPIED_FEEDBACK_MS = 1500
|
|
7
|
+
|
|
8
|
+
export type CodeBlockProps = ComponentPropsWithoutRef<'pre'> & ExtraProps
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A fenced code block, with a copy button.
|
|
12
|
+
*
|
|
13
|
+
* What the panel puts in fences is mostly shell commands and JSON an operator
|
|
14
|
+
* wants to run or paste somewhere, so copying it is the common next action.
|
|
15
|
+
*
|
|
16
|
+
* The text is read off the rendered node rather than reassembled from the hast
|
|
17
|
+
* tree: the highlighter has already replaced the block's children with token
|
|
18
|
+
* spans, and `textContent` is exactly the source again. The button is absent
|
|
19
|
+
* where the Clipboard API is — it needs a secure context, and a gateway on a site
|
|
20
|
+
* LAN is served over plain http.
|
|
21
|
+
*/
|
|
22
|
+
export const CodeBlock = ({ children, node, ...rest }: CodeBlockProps): JSX.Element => {
|
|
23
|
+
const codeRef = useRef<HTMLPreElement>(null)
|
|
24
|
+
const [copied, setCopied] = useState(false)
|
|
25
|
+
|
|
26
|
+
const canCopy = typeof navigator?.clipboard?.writeText === 'function'
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!copied) return
|
|
30
|
+
const timer = setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS)
|
|
31
|
+
return () => clearTimeout(timer)
|
|
32
|
+
}, [copied])
|
|
33
|
+
|
|
34
|
+
const copy = () => {
|
|
35
|
+
const text = codeRef.current?.textContent ?? ''
|
|
36
|
+
if (!text) return
|
|
37
|
+
void navigator.clipboard.writeText(text).then(
|
|
38
|
+
() => setCopied(true),
|
|
39
|
+
() => {
|
|
40
|
+
// Denied by permissions policy, or the document is not focused. Nothing
|
|
41
|
+
// to report — the code is still on screen and selectable.
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="mdk-agent-code">
|
|
48
|
+
<pre ref={codeRef} {...rest}>{children}</pre>
|
|
49
|
+
{canCopy
|
|
50
|
+
? (
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className="mdk-agent-code__copy"
|
|
54
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
55
|
+
onClick={copy}
|
|
56
|
+
>
|
|
57
|
+
{copied ? 'Copied' : 'Copy'}
|
|
58
|
+
</button>
|
|
59
|
+
)
|
|
60
|
+
: null}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JSX } from 'react'
|
|
2
|
+
import { memo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { clipToBoundary } from '../core/markdown'
|
|
5
|
+
import { MarkdownText } from './markdown-text'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where an answer stops being readable in a docked panel.
|
|
9
|
+
*
|
|
10
|
+
* The panel is 420×640, so a few thousand characters is already several screens
|
|
11
|
+
* of scrolling between one question and the next.
|
|
12
|
+
*/
|
|
13
|
+
export const COLLAPSE_ABOVE_CHARS = 3000
|
|
14
|
+
/** How much of it is shown collapsed. */
|
|
15
|
+
export const COLLAPSED_CHARS = 1500
|
|
16
|
+
|
|
17
|
+
export type CollapsibleAnswerProps = {
|
|
18
|
+
text: string
|
|
19
|
+
/** While the turn streams the answer is never collapsed: growth has to stay visible. */
|
|
20
|
+
streaming?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An answer, collapsed behind a toggle when it is long enough to bury the rest of
|
|
25
|
+
* the transcript.
|
|
26
|
+
*
|
|
27
|
+
* Only ever collapsed once the turn has settled — mid-stream, hiding the tail
|
|
28
|
+
* would hide the very thing the operator is watching.
|
|
29
|
+
*/
|
|
30
|
+
const CollapsibleAnswerView = ({ text, streaming = false }: CollapsibleAnswerProps): JSX.Element => {
|
|
31
|
+
const [expanded, setExpanded] = useState(false)
|
|
32
|
+
|
|
33
|
+
const isLong = !streaming && text.length > COLLAPSE_ABOVE_CHARS
|
|
34
|
+
const shown = isLong && !expanded ? clipToBoundary(text, COLLAPSED_CHARS) : text
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<MarkdownText text={shown} streaming={streaming} />
|
|
39
|
+
{isLong
|
|
40
|
+
? (
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
className="mdk-agent-markdown__more"
|
|
44
|
+
aria-expanded={expanded}
|
|
45
|
+
onClick={() => setExpanded(!expanded)}
|
|
46
|
+
>
|
|
47
|
+
{expanded ? 'Show less' : 'Show full answer'}
|
|
48
|
+
</button>
|
|
49
|
+
)
|
|
50
|
+
: null}
|
|
51
|
+
</>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const CollapsibleAnswer = memo(CollapsibleAnswerView)
|