@neurosquad/card-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/dist/card-sdk.js +5266 -0
- package/dist/cli.js +2838 -0
- package/dist/react.js +257 -0
- package/dist/testing.js +1150 -0
- package/dist/types/client/base64.d.ts +7 -0
- package/dist/types/client/card.d.ts +653 -0
- package/dist/types/client/channel.d.ts +49 -0
- package/dist/types/client/coalesce.d.ts +14 -0
- package/dist/types/client/connect.d.ts +39 -0
- package/dist/types/client/errors.d.ts +46 -0
- package/dist/types/client/helpers.d.ts +25 -0
- package/dist/types/client/net.d.ts +111 -0
- package/dist/types/client/theme.d.ts +31 -0
- package/dist/types/client/toolResult.d.ts +16 -0
- package/dist/types/contract/api.d.ts +812 -0
- package/dist/types/contract/index.d.ts +11 -0
- package/dist/types/contract/jsonSchema.d.ts +88 -0
- package/dist/types/contract/localized.d.ts +10 -0
- package/dist/types/contract/manifest.d.ts +179 -0
- package/dist/types/contract/network.d.ts +48 -0
- package/dist/types/contract/permissions.d.ts +180 -0
- package/dist/types/contract/ports.d.ts +237 -0
- package/dist/types/contract/protocol.d.ts +74 -0
- package/dist/types/contract/source.d.ts +111 -0
- package/dist/types/contract/theme.d.ts +21 -0
- package/dist/types/contract/version.d.ts +126 -0
- package/dist/types/i18n.d.ts +50 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/react/index.d.ts +132 -0
- package/dist/types/testing/index.d.ts +7 -0
- package/dist/types/testing/mockHost.d.ts +315 -0
- package/dist/types/version.d.ts +2 -0
- package/dist/ui.css +581 -0
- package/package.json +77 -0
- package/schema/neurosquad-card.v1.json +434 -0
- package/templates/react/README.md +34 -0
- package/templates/react/_gitignore +10 -0
- package/templates/react/icon.png +0 -0
- package/templates/react/index.html +12 -0
- package/templates/react/neurosquad-card.json +94 -0
- package/templates/react/package.json +25 -0
- package/templates/react/src/App.tsx +131 -0
- package/templates/react/src/i18n.ts +61 -0
- package/templates/react/src/main.tsx +77 -0
- package/templates/react/src/styles.css +42 -0
- package/templates/react/tsconfig.json +18 -0
- package/templates/react/vite.config.ts +19 -0
- package/templates/vanilla/README.md +37 -0
- package/templates/vanilla/_gitignore +6 -0
- package/templates/vanilla/icon.png +0 -0
- package/templates/vanilla/index.html +41 -0
- package/templates/vanilla/main.js +256 -0
- package/templates/vanilla/neurosquad-card.json +94 -0
- package/templates/vanilla/style.css +41 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
permissionForPeer,
|
|
3
|
+
requestPermissions,
|
|
4
|
+
type AgentStatus,
|
|
5
|
+
type PermissionId
|
|
6
|
+
} from '@neurosquad/card-sdk'
|
|
7
|
+
import {
|
|
8
|
+
useAgents,
|
|
9
|
+
useCard,
|
|
10
|
+
useCardContext,
|
|
11
|
+
useEmit,
|
|
12
|
+
usePort,
|
|
13
|
+
useSettings,
|
|
14
|
+
useStorage,
|
|
15
|
+
useTool,
|
|
16
|
+
useTranslator
|
|
17
|
+
} from '@neurosquad/card-sdk/react'
|
|
18
|
+
import { useEffect, useState } from 'react'
|
|
19
|
+
import { catalog } from './i18n'
|
|
20
|
+
|
|
21
|
+
const TONE: Record<AgentStatus, string> = {
|
|
22
|
+
working: 'ns-dot--accent',
|
|
23
|
+
'needs-input': 'ns-dot--warning',
|
|
24
|
+
finished: 'ns-dot--success',
|
|
25
|
+
idle: '',
|
|
26
|
+
exited: 'ns-dot--danger'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function App(): React.JSX.Element {
|
|
30
|
+
const card = useCard()
|
|
31
|
+
const { workspace, instance } = useCardContext()
|
|
32
|
+
const [settings] = useSettings()
|
|
33
|
+
const t = useTranslator(catalog)
|
|
34
|
+
const { agents } = useAgents()
|
|
35
|
+
const [pad, setPad] = useStorage('scratchpad', '')
|
|
36
|
+
const [hint, setHint] = useState('')
|
|
37
|
+
const emitScratchpad = useEmit<string>('scratchpad')
|
|
38
|
+
|
|
39
|
+
const working = agents.filter((agent) => agent.status === 'working').length
|
|
40
|
+
|
|
41
|
+
// What the overview tile shows when the canvas is zoomed out.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
void card.setOverview({
|
|
44
|
+
primary: t('working', { count: working }),
|
|
45
|
+
tone: working ? 'accent' : 'default',
|
|
46
|
+
icon: 'users'
|
|
47
|
+
})
|
|
48
|
+
}, [card, t, working])
|
|
49
|
+
|
|
50
|
+
// Text arriving on the "text" input port is appended to the scratchpad.
|
|
51
|
+
const { message } = usePort<string>('text')
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (message)
|
|
54
|
+
setPad((previous) =>
|
|
55
|
+
previous ? `${previous}\n${message.data as string}` : (message.data as string)
|
|
56
|
+
)
|
|
57
|
+
}, [message, setPad])
|
|
58
|
+
|
|
59
|
+
// A tool for connected agents: they can read what the user wrote here.
|
|
60
|
+
useTool('read_scratchpad', () => pad || '(the scratchpad is empty)')
|
|
61
|
+
|
|
62
|
+
const send = async (): Promise<void> => {
|
|
63
|
+
try {
|
|
64
|
+
// A note's input needs the optional cards.connected permission: ask for
|
|
65
|
+
// it (the app shows its own prompt) before the first send.
|
|
66
|
+
const needed = card.ports.peers
|
|
67
|
+
.filter((peer) => peer.direction !== 'upstream')
|
|
68
|
+
.map((peer) => permissionForPeer(peer, { outputType: 'ns:markdown' }))
|
|
69
|
+
.filter((id): id is PermissionId => id !== null)
|
|
70
|
+
if (needed.length > 0) await requestPermissions(card, ...new Set(needed))
|
|
71
|
+
const delivered = await emitScratchpad(pad)
|
|
72
|
+
setHint(delivered > 0 ? t('sent', { count: delivered }) : t('notConnected'))
|
|
73
|
+
} catch (error) {
|
|
74
|
+
setHint(error instanceof Error ? error.message : String(error))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<main className="app">
|
|
80
|
+
<header className="ns-spread">
|
|
81
|
+
<div className="ns-grow">
|
|
82
|
+
<p className="ns-subtitle ns-truncate">{workspace.name}</p>
|
|
83
|
+
<h1 className="ns-title ns-truncate">
|
|
84
|
+
{String(settings.values['greeting'] ?? instance.displayName)}
|
|
85
|
+
</h1>
|
|
86
|
+
</div>
|
|
87
|
+
<span className="ns-badge ns-badge--accent" title={t('working', { count: working })}>
|
|
88
|
+
{working}
|
|
89
|
+
</span>
|
|
90
|
+
</header>
|
|
91
|
+
|
|
92
|
+
<section className="ns-surface ns-surface--inset agents">
|
|
93
|
+
<p className="ns-subtitle">{t('agents')}</p>
|
|
94
|
+
<ul className="ns-list ns-scroll">
|
|
95
|
+
{agents.length === 0 && <li className="ns-list-item ns-muted">{t('noAgents')}</li>}
|
|
96
|
+
{agents.map((agent) => (
|
|
97
|
+
<li key={agent.id} className="ns-list-item">
|
|
98
|
+
<span className={`ns-dot ${TONE[agent.status]}`} />
|
|
99
|
+
<span className="ns-truncate">{agent.name}</span>
|
|
100
|
+
<span className="agent-status ns-muted">{t(`status.${agent.status}`)}</span>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
</section>
|
|
105
|
+
|
|
106
|
+
<section className="ns-field pad">
|
|
107
|
+
<label className="ns-label" htmlFor="pad">
|
|
108
|
+
{t('scratchpad')}
|
|
109
|
+
</label>
|
|
110
|
+
<textarea
|
|
111
|
+
id="pad"
|
|
112
|
+
className="ns-textarea"
|
|
113
|
+
spellCheck={false}
|
|
114
|
+
value={pad}
|
|
115
|
+
onChange={(event) => setPad(event.target.value)}
|
|
116
|
+
/>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<footer className="ns-spread">
|
|
120
|
+
<span className="ns-small ns-muted ns-truncate">{hint}</span>
|
|
121
|
+
<button
|
|
122
|
+
className="ns-btn ns-btn--primary ns-btn--sm"
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={() => void send()}
|
|
125
|
+
>
|
|
126
|
+
{t('send')}
|
|
127
|
+
</button>
|
|
128
|
+
</footer>
|
|
129
|
+
</main>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Catalog } from '@neurosquad/card-sdk'
|
|
2
|
+
|
|
3
|
+
/** The card's own strings in the app's three languages (en is the fallback). */
|
|
4
|
+
export const catalog: Catalog = {
|
|
5
|
+
en: {
|
|
6
|
+
agents: 'Agents',
|
|
7
|
+
scratchpad: 'Scratchpad',
|
|
8
|
+
send: 'Send',
|
|
9
|
+
noAgents: 'No agents on this canvas yet',
|
|
10
|
+
working_one: '{{count}} agent working',
|
|
11
|
+
working_other: '{{count}} agents working',
|
|
12
|
+
sent_one: 'Sent to {{count}} card',
|
|
13
|
+
sent_other: 'Sent to {{count}} cards',
|
|
14
|
+
notConnected: 'Draw an arrow from this card to a note to send it there',
|
|
15
|
+
status: {
|
|
16
|
+
working: 'working',
|
|
17
|
+
'needs-input': 'waiting for you',
|
|
18
|
+
finished: 'finished',
|
|
19
|
+
idle: 'idle',
|
|
20
|
+
exited: 'stopped'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
ru: {
|
|
24
|
+
agents: 'Агенты',
|
|
25
|
+
scratchpad: 'Черновик',
|
|
26
|
+
send: 'Отправить',
|
|
27
|
+
noAgents: 'На холсте пока нет агентов',
|
|
28
|
+
working_one: 'Работает {{count}} агент',
|
|
29
|
+
working_few: 'Работают {{count}} агента',
|
|
30
|
+
working_many: 'Работают {{count}} агентов',
|
|
31
|
+
working_other: 'Работают {{count}} агента',
|
|
32
|
+
sent_one: 'Отправлено в {{count}} карточку',
|
|
33
|
+
sent_few: 'Отправлено в {{count}} карточки',
|
|
34
|
+
sent_many: 'Отправлено в {{count}} карточек',
|
|
35
|
+
sent_other: 'Отправлено в {{count}} карточки',
|
|
36
|
+
notConnected: 'Проведите стрелку от этой карточки к заметке, чтобы отправить туда',
|
|
37
|
+
status: {
|
|
38
|
+
working: 'работает',
|
|
39
|
+
'needs-input': 'ждёт вас',
|
|
40
|
+
finished: 'закончил',
|
|
41
|
+
idle: 'свободен',
|
|
42
|
+
exited: 'остановлен'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
zh: {
|
|
46
|
+
agents: '智能体',
|
|
47
|
+
scratchpad: '草稿',
|
|
48
|
+
send: '发送',
|
|
49
|
+
noAgents: '画布上还没有智能体',
|
|
50
|
+
working_other: '{{count}} 个智能体正在工作',
|
|
51
|
+
sent_other: '已发送到 {{count}} 张卡片',
|
|
52
|
+
notConnected: '从此卡片画一条箭头到笔记,即可发送到那里',
|
|
53
|
+
status: {
|
|
54
|
+
working: '工作中',
|
|
55
|
+
'needs-input': '等待你',
|
|
56
|
+
finished: '已完成',
|
|
57
|
+
idle: '空闲',
|
|
58
|
+
exited: '已停止'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Import the SDK first, from the entry module: it starts listening for the
|
|
2
|
+
// app's handshake as soon as it loads.
|
|
3
|
+
import { connect, type Card, type CardManifest } from '@neurosquad/card-sdk'
|
|
4
|
+
import { CardProvider } from '@neurosquad/card-sdk/react'
|
|
5
|
+
import '@neurosquad/card-sdk/ui.css'
|
|
6
|
+
import { StrictMode } from 'react'
|
|
7
|
+
import { createRoot } from 'react-dom/client'
|
|
8
|
+
import manifest from '../neurosquad-card.json'
|
|
9
|
+
import { App } from './App'
|
|
10
|
+
import './styles.css'
|
|
11
|
+
|
|
12
|
+
/** Opened on its own (`npm run dev`), the card runs against a mock host with sample data. */
|
|
13
|
+
async function previewCard(): Promise<Card> {
|
|
14
|
+
const { createMockHost } = await import('@neurosquad/card-sdk/testing')
|
|
15
|
+
const host = createMockHost({
|
|
16
|
+
manifest: manifest as unknown as CardManifest,
|
|
17
|
+
agents: [
|
|
18
|
+
{
|
|
19
|
+
id: 'a1',
|
|
20
|
+
name: 'Claude Code',
|
|
21
|
+
harness: 'claude-code',
|
|
22
|
+
kind: 'ai',
|
|
23
|
+
status: 'working',
|
|
24
|
+
connected: true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'a2',
|
|
28
|
+
name: 'Codex',
|
|
29
|
+
harness: 'codex-cli',
|
|
30
|
+
kind: 'ai',
|
|
31
|
+
status: 'needs-input',
|
|
32
|
+
connected: false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 't1',
|
|
36
|
+
name: 'PowerShell',
|
|
37
|
+
harness: 'shell-powershell',
|
|
38
|
+
kind: 'shell',
|
|
39
|
+
status: 'idle',
|
|
40
|
+
connected: false
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
peers: [
|
|
44
|
+
{
|
|
45
|
+
cardId: 'note-1',
|
|
46
|
+
kind: 'note',
|
|
47
|
+
name: 'Notes',
|
|
48
|
+
direction: 'downstream',
|
|
49
|
+
inputs: [
|
|
50
|
+
{
|
|
51
|
+
id: 'append',
|
|
52
|
+
label: 'Append',
|
|
53
|
+
type: 'ns:markdown',
|
|
54
|
+
mode: 'stream',
|
|
55
|
+
retain: false,
|
|
56
|
+
default: true,
|
|
57
|
+
// Like the app: sending to a note needs cards.connected.
|
|
58
|
+
permission: 'cards.connected'
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
outputs: []
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
})
|
|
65
|
+
Object.assign(window, { mockHost: host }) // try mockHost.setAgentStatus('a2', 'working') in DevTools
|
|
66
|
+
return host.connect()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const card = window.parent === window ? await previewCard() : await connect()
|
|
70
|
+
|
|
71
|
+
createRoot(document.getElementById('root')!).render(
|
|
72
|
+
<StrictMode>
|
|
73
|
+
<CardProvider card={card}>
|
|
74
|
+
<App />
|
|
75
|
+
</CardProvider>
|
|
76
|
+
</StrictMode>
|
|
77
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Your card's own styles, on top of the optional kit (@neurosquad/card-sdk/ui.css). */
|
|
2
|
+
html,
|
|
3
|
+
body,
|
|
4
|
+
#root {
|
|
5
|
+
height: 100%;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.app {
|
|
10
|
+
display: grid;
|
|
11
|
+
grid-template-rows: auto minmax(64px, 1fr) minmax(96px, 1.2fr) auto;
|
|
12
|
+
grid-template-columns: minmax(0, 1fr);
|
|
13
|
+
gap: 10px;
|
|
14
|
+
height: 100%;
|
|
15
|
+
padding: 12px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.agents {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 6px;
|
|
22
|
+
min-height: 0;
|
|
23
|
+
padding: 10px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.agents .ns-list {
|
|
27
|
+
flex: 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.pad {
|
|
31
|
+
min-height: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.pad .ns-textarea {
|
|
35
|
+
height: 100%;
|
|
36
|
+
resize: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.agent-status {
|
|
40
|
+
margin-left: auto;
|
|
41
|
+
font-size: 12px;
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"noUnusedParameters": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"types": ["vite/client"],
|
|
15
|
+
"noEmit": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src", "vite.config.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react'
|
|
2
|
+
import { defineConfig } from 'vite'
|
|
3
|
+
|
|
4
|
+
// The app serves the built card from nscard://<id>/dist/…, so every URL must be
|
|
5
|
+
// relative (base './'). Commit dist/: installs come straight from the repository.
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
base: './',
|
|
8
|
+
plugins: [react()],
|
|
9
|
+
// One React even when the SDK is linked from a local checkout (file: deps are
|
|
10
|
+
// symlinks, and Vite would resolve react next to the SDK's real path).
|
|
11
|
+
resolve: { dedupe: ['react', 'react-dom'] },
|
|
12
|
+
build: {
|
|
13
|
+
outDir: 'dist',
|
|
14
|
+
emptyOutDir: true,
|
|
15
|
+
// The card CSP allows scripts only from the package; no inline preload polyfill.
|
|
16
|
+
modulePreload: { polyfill: false },
|
|
17
|
+
target: 'es2022'
|
|
18
|
+
}
|
|
19
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# __CARD_DISPLAY_NAME__
|
|
2
|
+
|
|
3
|
+
A custom card for [NeuroSquad](https://neurosquad.ai), made with `@neurosquad/card-sdk`
|
|
4
|
+
(vanilla template — no build step).
|
|
5
|
+
|
|
6
|
+
## Files
|
|
7
|
+
|
|
8
|
+
| File | What it is |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `neurosquad-card.json` | The manifest: name, size, permissions, settings, ports, tools |
|
|
11
|
+
| `index.html`, `main.js`, `style.css` | The card itself — a small web page |
|
|
12
|
+
| `icon.png` | Square PNG/WebP, ≤ 128 KB |
|
|
13
|
+
| `vendor/` | The SDK (`card-sdk.js`), the optional UI kit (`ui.css`), the mock host for previews and the manifest schema |
|
|
14
|
+
|
|
15
|
+
## Develop
|
|
16
|
+
|
|
17
|
+
1. In NeuroSquad: **Settings → Custom cards → Developer mode**.
|
|
18
|
+
2. In this folder: `npx @neurosquad/card-sdk dev` — confirm the link in the app,
|
|
19
|
+
then add the card from the canvas (**+ → Custom card…**). Saving a file reloads it;
|
|
20
|
+
the card's log prints in your terminal.
|
|
21
|
+
|
|
22
|
+
Preview in a browser without the app: `npx serve .` and open the page — it runs
|
|
23
|
+
against a mock host with sample agents.
|
|
24
|
+
|
|
25
|
+
## Check and share
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx @neurosquad/card-sdk validate # the app's own checks + the install dialog preview
|
|
29
|
+
npx @neurosquad/card-sdk pack # what would be installed, and the tree hash
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Push this folder to GitHub. People install it by pasting `owner/repo` (or
|
|
33
|
+
`owner/repo/path/to/folder`) into **Settings → Custom cards**.
|
|
34
|
+
|
|
35
|
+
Card code runs in a sandbox: no inline scripts, no external resources (bundle
|
|
36
|
+
everything into the folder), network only through `card.net.fetch` to hosts you
|
|
37
|
+
declare in the `network` permission.
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>__CARD_DISPLAY_NAME__</title>
|
|
7
|
+
<!-- Optional dark kit that follows the app theme (--ns-* variables). Delete it to style from scratch. -->
|
|
8
|
+
<link rel="stylesheet" href="vendor/ui.css" />
|
|
9
|
+
<link rel="stylesheet" href="style.css" />
|
|
10
|
+
<!-- Card code must live in files: the card's CSP blocks inline scripts. -->
|
|
11
|
+
<script type="module" src="main.js"></script>
|
|
12
|
+
</head>
|
|
13
|
+
<body class="ns-kit">
|
|
14
|
+
<main class="app" aria-busy="true">
|
|
15
|
+
<header class="ns-spread">
|
|
16
|
+
<div class="ns-grow">
|
|
17
|
+
<p class="ns-subtitle ns-truncate" id="workspace"> </p>
|
|
18
|
+
<h1 class="ns-title ns-truncate" id="greeting"> </h1>
|
|
19
|
+
</div>
|
|
20
|
+
<span class="ns-badge ns-badge--accent" id="working" title="">0</span>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<section class="ns-surface ns-surface--inset agents">
|
|
24
|
+
<p class="ns-subtitle" data-t="agents">Agents</p>
|
|
25
|
+
<ul class="ns-list ns-scroll" id="agents"></ul>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<section class="ns-field pad">
|
|
29
|
+
<label class="ns-label" for="pad" data-t="scratchpad">Scratchpad</label>
|
|
30
|
+
<textarea class="ns-textarea ns-grow" id="pad" spellcheck="false"></textarea>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<footer class="ns-spread">
|
|
34
|
+
<span class="ns-small ns-muted ns-truncate" id="hint"></span>
|
|
35
|
+
<button class="ns-btn ns-btn--primary ns-btn--sm" id="send" type="button" data-t="send">
|
|
36
|
+
Send
|
|
37
|
+
</button>
|
|
38
|
+
</footer>
|
|
39
|
+
</main>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|