@nuxtagentic/agents 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +99 -2
  3. package/module.ts +35 -0
  4. package/package.json +56 -3
  5. package/runtime/actions.ts +22 -0
  6. package/runtime/components/AgentEditor.vue +446 -0
  7. package/runtime/components/AgentList.vue +110 -0
  8. package/runtime/components/AgentToolPicker.vue +371 -0
  9. package/runtime/components/AgentsWorkspace.vue +328 -0
  10. package/runtime/components/ModelSelect.vue +113 -0
  11. package/runtime/components/RunPanel.vue +114 -0
  12. package/runtime/components/RuntimeModelPicker.vue +276 -0
  13. package/runtime/components/SourceEditor.vue +78 -0
  14. package/runtime/composables/useAgentIntegrationTools.ts +24 -0
  15. package/runtime/composables/useAgentRuntimes.ts +32 -0
  16. package/runtime/composables/useAgents.ts +24 -0
  17. package/runtime/index.ts +71 -0
  18. package/runtime/middleware/agents-route.ts +10 -0
  19. package/runtime/pages/agents.vue +26 -0
  20. package/runtime/server/api/agents/[id]/runs/[run].get.ts +11 -0
  21. package/runtime/server/api/agents/[id]/runs.post.ts +33 -0
  22. package/runtime/server/api/agents/runtimes.get.ts +37 -0
  23. package/runtime/server/utils/agent-execution.ts +124 -0
  24. package/runtime/server/utils/agent-gateway.ts +13 -0
  25. package/runtime/server/utils/agent-model-catalog.ts +93 -0
  26. package/runtime/server/utils/agent-provider-keys.ts +46 -0
  27. package/runtime/server/utils/local-agent-catalog.ts +151 -0
  28. package/runtime/server/utils/local-agent-runtime.ts +8 -0
  29. package/runtime/server/utils/model-fetch.ts +66 -0
  30. package/runtime/server/utils/run-local-agent.ts +208 -0
  31. package/runtime/server/utils/tool-result.ts +22 -0
  32. package/runtime/shared/agent-summary.ts +9 -0
  33. package/runtime/shared/agent.ts +4 -0
  34. package/runtime/shared/definition.ts +16 -0
  35. package/runtime/shared/editorTheme.ts +43 -0
  36. package/runtime/shared/integration-tool.ts +11 -0
  37. package/runtime/shared/presentation.ts +50 -0
  38. package/runtime/shared/prompt.ts +37 -0
  39. package/runtime/shared/runtime.ts +40 -0
  40. package/runtime/shared/source.ts +81 -0
  41. package/schema.ts +2 -0
  42. package/server.ts +2 -0
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ Proprietary software. All rights reserved.
2
+
3
+ Use of Nuxt Agentic packages and starter materials is governed by the applicable
4
+ commercial agreement supplied with a purchase. This repository does not grant
5
+ an open-source license or permission to redistribute the packages as a competing
6
+ template or module library.
7
+
8
+ Third-party dependencies retain their respective licenses. This notice does not
9
+ replace or modify those licenses.
package/README.md CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ # Agents
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Saved agents run through CloudConfig's provider harness. “Test agent” runs the
4
+ saved version with JSON input and displays progress, approval waits and output.
5
+ Runs are scoped to the authenticated owner, active workspace, agent and session.
6
+
7
+ Agent code never names a platform. `server/cloudconfig/agents.ts`
8
+ default-exports `defineAgentHost(...)`, built from `agentRunHost`; server code
9
+ opens runs and schedules through `useCloudConfigAgents(event)` or
10
+ `getAgentPlatform(env)`. CloudConfig's Nuxt module creates the platform's agent
11
+ from the host: the official Cloudflare Agents SDK with SQLite checkpoints in
12
+ Cloudflare builds, and an in-memory runner with the local CLI adapters in
13
+ development.
14
+
15
+ Deployment declarations live in the host application's `cloud.config.ts`: `deploy.agents`
16
+ names the host entry, and `deploy.workflows` exports the automation runner. Use `cloudconfig deploy` to reconcile their bindings and
17
+ migrations. OpenAI uses `NUXT_OPENAI_API_KEY`; configure
18
+ `NUXT_ANTHROPIC_API_KEY` or `NUXT_GOOGLE_AI_API_KEY` for those providers.
19
+ Optional keys are pushed when supplied locally. Secrets never enter saved
20
+ definitions or the browser.
21
+
22
+ `@nuxtagentic/agents` provides the Agents collection and Markdown editor. The
23
+ framework-independent definitions, ownership rules, validation, frontmatter and
24
+ provider tool schemas belong to `@cloudconfig/core/agents` in CloudConfig.
25
+
26
+ Agents is a core feature, always available in every workspace. It has no install
27
+ or remove controls in the Modules catalog.
28
+
29
+ Open Agents in the workspace navigation, or type `agents` in the hero prompt.
30
+ `Create agent: Release notes writer` creates a named draft agent.
31
+ **New agent** inserts a draft record right away and opens it; edit its
32
+ instructions, choose a runtime and model, and add tool declarations. Edits
33
+ autosave after an 800 ms pause. **⌘/Ctrl Enter** saves immediately. Leaving the editor saves pending
34
+ edits first; failed saves keep your changes available to retry. Search and selection
35
+ live in the URL; open `/dashboard/agents` or link directly with
36
+ `/dashboard/agents?agent=<id>`. Older query-based links redirect to these URLs.
37
+
38
+ In local development at `localhost`, the runtime picker lists Codex CLI and
39
+ Claude Code when they are installed on the server's PATH (including
40
+ `~/.local/bin`). Install the CLI and sign in using `codex login` or
41
+ `claude auth login`. Model choices come from the installed runtime's catalog;
42
+ **Runtime default** uses the CLI default. Runtime and model have separate inline
43
+ menus. Search the model list by name or ID, or enter a custom ID. Claude
44
+ discovery follows Open Run: it includes models with effort support alongside the
45
+ latest model in each family, and falls back to the `opus`, `sonnet` and `haiku`
46
+ aliases. Catalogs are read once per installed CLI version (or Codex models cache)
47
+ and reused by every editor in the session; opening a menu refreshes them after a
48
+ minute.
49
+
50
+ Local runtime and model choices are saved separately from the production API
51
+ provider and model. The cloud button opens that API configuration when using a
52
+ local runtime. Deployed hosts always execute through OpenAI, Anthropic, or Google APIs;
53
+ they never attempt to spawn a local process. Configure the selected server key:
54
+ `NUXT_AGENTS_OPENAI_API_KEY`, `NUXT_AGENTS_ANTHROPIC_API_KEY`, or
55
+ `NUXT_AGENTS_GOOGLE_AI_API_KEY`. Existing `NUXT_OPENAI_API_KEY`,
56
+ `NUXT_ANTHROPIC_API_KEY`, `NUXT_GOOGLE_AI_API_KEY` and Automations key overrides
57
+ continue to work. Add your chosen key to `deploy.secrets` before deploying with
58
+ CloudConfig. API model lists are fetched from the provider account, cached for an
59
+ hour and refreshed in the background; saved and custom IDs remain editable if
60
+ discovery is unavailable.
61
+
62
+ `@cloudconfig/core/agents` owns the runtime fields, validation and Markdown
63
+ frontmatter alongside the existing agent contract. This package supplies the UI,
64
+ catalog discovery and local execution adapters. Both execution paths use the
65
+ granted integration handlers; local CLI built-in tools are disabled.
66
+
67
+ The visual editor and Markdown source edit the same definition. Source shows the
68
+ settings as YAML frontmatter: runtime, model, status, tool choice and the names
69
+ of enabled tools. The toolbar and Tools drawer stay available in both views and
70
+ update the frontmatter as you change them. Tool declarations are edited in the
71
+ drawer; frontmatter with complete JSON tool declarations is still accepted.
72
+ The tools drawer supports enabling tools, auto/required/none choice, parameters,
73
+ and copying schemas for OpenAI Responses, Anthropic and Google. Connected
74
+ integration tools appear as suggestions at the bottom of the drawer.
75
+
76
+ To add Agents to another host, install its local packages and register
77
+ `@nuxtagentic/agents` in Nuxt. It remains available outside the optional catalog. Include `agentsModels` from its
78
+ `schema` export in `cloud.config.ts`, then generate and apply a migration. The
79
+ host uses generated CloudConfig CRUD and the typed SDK; no custom agent CRUD
80
+ routes or database tables are needed.
81
+
82
+ Active marks a definition ready for automatic execution. Use
83
+ [Automations](../automations/README.md) to run it from events and webhooks, or use
84
+ a test run while the definition is still a draft. Configure its provider key on
85
+ the server. Connect accounts in [Integrations](../integrations/README.md), then pick an
86
+ integration tool suggestion in the Tools drawer to attach granted runtime handlers.
87
+ Custom tool declarations need a handler before execution. The existing
88
+ `@nuxtagentic/prompt` package continues to own workspace prompt planning and
89
+ reviewed mutations.
90
+
91
+ Deployment verification runs `pnpm build` followed by `pnpm test:worker`. The
92
+ Worker test loads the complete production bundle with its real D1 migrations,
93
+ authentication, Nuxt SSR, Agent and AutomationRunner exports. Provider responses
94
+ are mocked, so verification does not call a paid model API. `pnpm test:agents`
95
+ also checks durable execution and the generated deployment wrapper, while
96
+ `pnpm test:automations` checks approvals, retries and integration permissions.
97
+
98
+ The production build uses a bounded Node heap and exposes garbage collection.
99
+ Default code verification is `pnpm typecheck`; build and runtime verification
100
+ require an explicit request.
package/module.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { addRouteMiddleware, createResolver, defineNuxtModule } from '@nuxt/kit'
2
+ import { registerWorkspaceModule } from '@nuxtagentic/kit'
3
+
4
+ export default defineNuxtModule({
5
+ meta: { name: '@nuxtagentic/agents' },
6
+ moduleDependencies: { '@nuxtagentic/workspace': {} },
7
+ setup(_options, nuxt) {
8
+ const resolver = createResolver(import.meta.url)
9
+ nuxt.options.runtimeConfig.agents = Object.assign(
10
+ { openaiApiKey: '', anthropicApiKey: '', googleAiApiKey: '' },
11
+ nuxt.options.runtimeConfig.agents,
12
+ )
13
+ registerWorkspaceModule(
14
+ {
15
+ id: 'agents',
16
+ runtime: resolver.resolve('./runtime'),
17
+ feature: { from: resolver.resolve('./runtime/index.ts'), name: 'agentsFeature' },
18
+ metadata: resolver.resolve('./runtime/shared/agent.ts'),
19
+ },
20
+ nuxt,
21
+ )
22
+ addRouteMiddleware({
23
+ name: 'agents-route',
24
+ path: resolver.resolve('./runtime/middleware/agents-route.ts'),
25
+ global: true,
26
+ })
27
+ nuxt.hook('pages:extend', (pages) => {
28
+ pages.push({
29
+ name: 'dashboard-agents',
30
+ path: '/dashboard/agents',
31
+ file: resolver.resolve('./runtime/pages/agents.vue'),
32
+ })
33
+ })
34
+ },
35
+ })
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@nuxtagentic/agents",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "files": [
6
+ "module.ts",
7
+ "runtime",
8
+ "schema.ts",
9
+ "README.md",
10
+ "server.ts",
11
+ "LICENSE.md"
12
+ ],
13
+ "exports": {
14
+ ".": "./module.ts",
15
+ "./package.json": "./package.json",
16
+ "./schema": "./schema.ts",
17
+ "./server": "./server.ts",
18
+ "./runtime/*.vue": "./runtime/*.vue",
19
+ "./runtime/*.css": "./runtime/*.css",
20
+ "./runtime/*.json": "./runtime/*.json",
21
+ "./runtime/*.svg": "./runtime/*.svg",
22
+ "./runtime/*": "./runtime/*.ts"
23
+ },
24
+ "dependencies": {
25
+ "@cloudconfig/core": "0.2.0-beta.2",
26
+ "@codemirror/commands": "^6.11.1",
27
+ "@codemirror/lang-markdown": "^6.5.2",
28
+ "@codemirror/lang-yaml": "^6.1.3",
29
+ "@codemirror/language": "^6.13.1",
30
+ "@codemirror/state": "^6.7.6",
31
+ "@codemirror/view": "^6.43.14",
32
+ "@lezer/highlight": "^1.2.5",
33
+ "@nuxt/kit": "4.6.1",
34
+ "@nuxt/ui": "^4.11.1",
35
+ "h3": "1.15.11",
36
+ "vue-router": "^5.4.0",
37
+ "@nuxtagentic/integrations": "0.1.1",
38
+ "@nuxtagentic/prompt": "0.1.1",
39
+ "@nuxtagentic/data": "0.1.1",
40
+ "@nuxtagentic/kit": "0.1.1",
41
+ "@nuxtagentic/workspace": "0.1.1"
42
+ },
43
+ "peerDependencies": {
44
+ "nuxt": "^4.6.1",
45
+ "vue": "^3.5.43"
46
+ },
47
+ "cloudconfig": {
48
+ "models": "agentsModels"
49
+ },
50
+ "license": "SEE LICENSE IN LICENSE.md",
51
+ "description": "Saved agent definitions, editing, tools and execution for Nuxt Agentic.",
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org/"
55
+ },
56
+ "engines": {
57
+ "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
58
+ }
6
59
  }
@@ -0,0 +1,22 @@
1
+ import type { AgentAction } from '@nuxtagentic/workspace/runtime/shared/agent-actions'
2
+
3
+ export const actions = {
4
+ agents: [
5
+ {
6
+ id: 'new',
7
+ name: 'New agent',
8
+ aliases: ['Create agent', 'Create an agent'],
9
+ description: 'Write the responsibilities and tools for a new agent.',
10
+ icon: 'i-lucide-plus',
11
+ run: (context) => context.navigate('/dashboard/agents', { agent: 'new' }),
12
+ },
13
+ {
14
+ id: 'save',
15
+ name: 'Save agent',
16
+ description: 'Save this agent’s instructions and tools.',
17
+ icon: 'i-lucide-check',
18
+ available: (context) => context.canRun('agents.save'),
19
+ run: (context) => context.run('agents.save'),
20
+ },
21
+ ] satisfies AgentAction[],
22
+ }
@@ -0,0 +1,446 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
3
+ import { defineShortcuts, navigateTo, useRoute } from '#imports'
4
+ import { onBeforeRouteLeave, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'
5
+ import { agentName, validateAgentDefinition, type AgentDefinition } from '@cloudconfig/core/agents'
6
+ import { useAgentActionHandler } from '@nuxtagentic/workspace/runtime/composables/useAgentActions'
7
+ import { parseAgentSource, serializeAgentSource } from '../shared/source'
8
+ import { MENU_UI, PILL_CLASS } from '../shared/presentation'
9
+ import RuntimeModelPicker from './RuntimeModelPicker.vue'
10
+ import SourceEditor from './SourceEditor.vue'
11
+ import AgentToolPicker from './AgentToolPicker.vue'
12
+ import type { AgentRuntimeCatalog } from '../shared/runtime'
13
+ import type { AgentIntegrationTool } from '../shared/integration-tool'
14
+
15
+ const { agent, busy, error, active, removing, save, embedded, runtimeCatalog, integrationTools } =
16
+ defineProps<{
17
+ agent: AgentDefinition
18
+ busy: boolean
19
+ error: string
20
+ active: boolean
21
+ removing: boolean
22
+ save: (agent: AgentDefinition) => Promise<boolean>
23
+ embedded?: boolean
24
+ runtimeCatalog?: AgentRuntimeCatalog
25
+ integrationTools?: AgentIntegrationTool[]
26
+ }>()
27
+ const emit = defineEmits<{
28
+ duplicate: [agent: AgentDefinition]
29
+ remove: []
30
+ close: []
31
+ }>()
32
+ function clone(agent: AgentDefinition): AgentDefinition {
33
+ return JSON.parse(JSON.stringify(agent))
34
+ }
35
+ const MODES = [
36
+ { value: 'visual', label: 'Visual', icon: 'i-lucide-eye' },
37
+ { value: 'source', label: 'Markdown', icon: 'i-lucide-code' },
38
+ ] as const
39
+ const draft = ref<AgentDefinition>(clone(agent))
40
+ const mode = ref<'visual' | 'source'>('visual')
41
+ // What the visual editor holds; it stays mounted while Markdown source is shown.
42
+ const visualMarkdown = ref(draft.value.markdown)
43
+ const source = ref('')
44
+ const sourceError = ref('')
45
+ const validationError = ref('')
46
+ const isToolsOpen = ref(false)
47
+ const isToolsMounted = ref(false)
48
+ const visualInput = useTemplateRef('visualInput')
49
+ const editorRoot = useTemplateRef('editorRoot')
50
+ const sourceInput = useTemplateRef('sourceInput')
51
+ const route = useRoute()
52
+ const isDiscardOpen = ref(false)
53
+ let nextLocation = ''
54
+ let canNavigate = false
55
+ let isParsing = false
56
+ const baseline = ref(JSON.stringify(agent))
57
+ const snapshot = computed(() => JSON.stringify(draft.value))
58
+ const hasChanges = computed(() => !!sourceError.value || snapshot.value !== baseline.value)
59
+ let savePromise: Promise<boolean> | null = null
60
+ const enabled = computed(() => draft.value.tools.filter((tool) => tool.enabled).length)
61
+ const settings = computed(() => {
62
+ const { provider, model, status, toolChoice, tools, runtime, localModel } = draft.value
63
+ return serializeAgentSource({
64
+ markdown: '',
65
+ provider,
66
+ model,
67
+ status,
68
+ toolChoice,
69
+ tools,
70
+ runtime,
71
+ localModel,
72
+ })
73
+ })
74
+ const moreItems = computed(() => [
75
+ { label: 'Duplicate', disabled: busy, onSelect: duplicate },
76
+ { label: 'Delete agent', disabled: busy, onSelect: () => emit('remove') },
77
+ ])
78
+ const toolbar = [
79
+ [
80
+ {
81
+ kind: 'mark' as const,
82
+ mark: 'bold' as const,
83
+ icon: 'i-lucide-bold',
84
+ tooltip: { text: 'Bold' },
85
+ },
86
+ {
87
+ kind: 'mark' as const,
88
+ mark: 'italic' as const,
89
+ icon: 'i-lucide-italic',
90
+ tooltip: { text: 'Italic' },
91
+ },
92
+ {
93
+ kind: 'mark' as const,
94
+ mark: 'code' as const,
95
+ icon: 'i-lucide-code',
96
+ tooltip: { text: 'Inline code' },
97
+ },
98
+ ],
99
+ ]
100
+ const layer =
101
+ 'min-w-0 [grid-area:1/1] transition-[opacity,translate,visibility] duration-200 ease-[cubic-bezier(.2,0,0,1)] motion-reduce:transition-none'
102
+ const shown = 'visible opacity-100'
103
+ const hidden = 'invisible translate-y-1 opacity-0'
104
+
105
+ // Toolbar and drawer changes rewrite the settings; typed source is never reformatted.
106
+ watch(
107
+ settings,
108
+ () => {
109
+ if (mode.value === 'source' && !isParsing && !sourceError.value) {
110
+ source.value = serializeAgentSource(draft.value)
111
+ }
112
+ },
113
+ { flush: 'sync' },
114
+ )
115
+ function updateVisual(value: string) {
116
+ visualMarkdown.value = value
117
+ draft.value.markdown = value
118
+ }
119
+ function syncVisual() {
120
+ const editor = visualInput.value?.editor
121
+ if (!editor || draft.value.markdown === visualMarkdown.value) {
122
+ return
123
+ }
124
+ // Loading source edits must not round-trip TipTap's normalized Markdown into the draft.
125
+ editor.commands.setContent(draft.value.markdown, { contentType: 'markdown', emitUpdate: false })
126
+ visualMarkdown.value = editor.getMarkdown()
127
+ }
128
+ function updateSource(value: string) {
129
+ source.value = value
130
+ isParsing = true
131
+ try {
132
+ draft.value = parseAgentSource(value, draft.value)
133
+ sourceError.value = ''
134
+ } catch (cause) {
135
+ sourceError.value = cause instanceof Error ? cause.message : 'Check the agent settings.'
136
+ } finally {
137
+ isParsing = false
138
+ }
139
+ }
140
+ async function setMode(value: typeof mode.value) {
141
+ if (mode.value === value || (value === 'visual' && sourceError.value)) {
142
+ return
143
+ }
144
+ if (value === 'source') {
145
+ source.value = serializeAgentSource(draft.value)
146
+ } else {
147
+ syncVisual()
148
+ }
149
+ mode.value = value
150
+ await nextTick()
151
+ if (value === 'visual') {
152
+ visualInput.value?.editor?.commands.focus()
153
+ } else {
154
+ sourceInput.value?.focus()
155
+ }
156
+ }
157
+ function toggleTools() {
158
+ isToolsMounted.value = true
159
+ isToolsOpen.value = !isToolsOpen.value
160
+ }
161
+ function validate() {
162
+ validationError.value =
163
+ sourceError.value ||
164
+ validateAgentDefinition({ ...draft.value })
165
+ .map((issue) => issue.message)
166
+ .join(' ')
167
+ return !validationError.value
168
+ }
169
+ watch([snapshot, sourceError, () => removing], (_, __, onCleanup) => {
170
+ validationError.value = ''
171
+ if (!active || removing || !hasChanges.value || sourceError.value) {
172
+ return
173
+ }
174
+ const timer = setTimeout(() => void saveDraft(), 800)
175
+ onCleanup(() => clearTimeout(timer))
176
+ })
177
+
178
+ async function saveDraft(): Promise<boolean> {
179
+ if (savePromise && !(await savePromise)) {
180
+ return false
181
+ }
182
+ if (!hasChanges.value) {
183
+ return true
184
+ }
185
+ if (busy || !validate()) {
186
+ return false
187
+ }
188
+ const submitted = snapshot.value
189
+ savePromise = save(clone(draft.value))
190
+ .then((saved) => {
191
+ if (saved) {
192
+ baseline.value = submitted
193
+ }
194
+ return saved
195
+ })
196
+ .finally(() => {
197
+ savePromise = null
198
+ })
199
+ return savePromise
200
+ }
201
+ async function duplicate() {
202
+ if (await saveDraft()) {
203
+ emit('duplicate', clone(draft.value))
204
+ }
205
+ }
206
+ async function guardNavigation(to: RouteLocationNormalized) {
207
+ if (embedded || !active || canNavigate || removing) {
208
+ return true
209
+ }
210
+ if (to.path === route.path && to.query.agent === route.query.agent) {
211
+ return true
212
+ }
213
+ if (await saveDraft()) {
214
+ return true
215
+ }
216
+ nextLocation = to.fullPath
217
+ isDiscardOpen.value = true
218
+ return false
219
+ }
220
+ function discard() {
221
+ canNavigate = true
222
+ isDiscardOpen.value = false
223
+ void navigateTo(nextLocation)
224
+ }
225
+ onBeforeRouteLeave(guardNavigation)
226
+ onBeforeRouteUpdate(guardNavigation)
227
+ useAgentActionHandler('agents.save', saveDraft, () => !embedded && active && !busy)
228
+ defineShortcuts({
229
+ meta_enter: {
230
+ usingInput: true,
231
+ handler: () => {
232
+ if (
233
+ active &&
234
+ (!embedded || editorRoot.value?.contains(document.activeElement)) &&
235
+ !document.querySelector(
236
+ '[data-state="open"]:is([role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"])',
237
+ )
238
+ ) {
239
+ saveDraft()
240
+ }
241
+ },
242
+ },
243
+ })
244
+ function onKeydown(event: KeyboardEvent) {
245
+ if (
246
+ event.key === 'Escape' &&
247
+ isToolsOpen.value &&
248
+ !event.defaultPrevented &&
249
+ !document.querySelector(
250
+ '[data-state="open"]:is([role="dialog"], [role="menu"], [role="listbox"])',
251
+ )
252
+ ) {
253
+ event.preventDefault()
254
+ event.stopPropagation()
255
+ isToolsOpen.value = false
256
+ }
257
+ }
258
+
259
+ defineExpose({ save: saveDraft })
260
+ </script>
261
+
262
+ <template>
263
+ <section
264
+ ref="editorRoot"
265
+ class="relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
266
+ :aria-label="agentName(draft.markdown)"
267
+ @keydown="onKeydown"
268
+ >
269
+ <div
270
+ class="relative z-20 flex h-13 shrink-0 items-center gap-1.5 border-b border-default pr-3 pl-4"
271
+ >
272
+ <UButton
273
+ icon="i-lucide-arrow-left"
274
+ aria-label="Back to agents"
275
+ variant="ghost"
276
+ square
277
+ class="-ml-1.5 size-7.5 md:hidden"
278
+ @click="$emit('close')"
279
+ />
280
+ <label
281
+ class="flex h-7.5 shrink-0 cursor-pointer items-center rounded-full border border-(--gray-500) pr-2.5 pl-1.5 transition-colors duration-160 hover:bg-(--gray-400) motion-reduce:transition-none"
282
+ :title="draft.status === 'active' ? 'Active' : 'Draft'"
283
+ >
284
+ <USwitch
285
+ :model-value="draft.status === 'active'"
286
+ size="xs"
287
+ color="success"
288
+ :ui="{ thumb: 'data-[state=checked]:bg-white' }"
289
+ aria-label="Active agent"
290
+ @update:model-value="draft.status = $event ? 'active' : 'draft'"
291
+ />
292
+ </label>
293
+ <div class="flex min-w-0 items-center gap-1.5">
294
+ <RuntimeModelPicker
295
+ v-model="draft"
296
+ :catalog="runtimeCatalog"
297
+ >
298
+ <UButton
299
+ icon="i-lucide-wrench"
300
+ :label="draft.tools.length ? `${enabled}/${draft.tools.length}` : 'Tools'"
301
+ title="Tools"
302
+ :aria-expanded="isToolsOpen"
303
+ variant="ghost"
304
+ class="shrink-0"
305
+ :class="PILL_CLASS"
306
+ :ui="{ leadingIcon: 'size-3.5' }"
307
+ @click="toggleTools"
308
+ />
309
+ </RuntimeModelPicker>
310
+ </div>
311
+ <div class="min-w-0 flex-1" />
312
+ <div
313
+ role="group"
314
+ aria-label="Editor view"
315
+ class="relative grid h-7.5 w-17 shrink-0 grid-cols-2 rounded-lg border border-(--gray-500) p-0.5"
316
+ >
317
+ <span
318
+ class="pointer-events-none absolute top-0.5 bottom-0.5 left-0.5 w-[calc((100%-4px)/2)] rounded-md bg-(--gray-400) transition-transform duration-240 ease-[cubic-bezier(.2,0,0,1)] motion-reduce:transition-none"
319
+ :class="mode === 'source' && 'translate-x-full'"
320
+ />
321
+ <button
322
+ v-for="view in MODES"
323
+ :key="view.value"
324
+ type="button"
325
+ :title="view.label"
326
+ :aria-label="view.label"
327
+ :aria-pressed="mode === view.value"
328
+ :disabled="view.value === 'visual' && !!sourceError"
329
+ class="relative grid cursor-pointer place-items-center rounded-md transition-colors duration-200 focus-visible:outline-2 focus-visible:outline-accented disabled:cursor-not-allowed motion-reduce:transition-none"
330
+ :class="mode === view.value ? 'text-highlighted' : 'text-muted hover:text-highlighted'"
331
+ @click="setMode(view.value)"
332
+ >
333
+ <UIcon
334
+ :name="view.icon"
335
+ class="size-3.5"
336
+ />
337
+ </button>
338
+ </div>
339
+ <UDropdownMenu
340
+ :items="moreItems"
341
+ :content="{ align: 'end', sideOffset: 6 }"
342
+ :ui="{
343
+ ...MENU_UI,
344
+ content: 'w-45 rounded-[10px] p-1.5 ring-(--gray-500)',
345
+ item: `${MENU_UI.item} min-h-8.5`,
346
+ }"
347
+ >
348
+ <UButton
349
+ icon="i-lucide-ellipsis"
350
+ aria-label="Agent actions"
351
+ variant="ghost"
352
+ square
353
+ class="size-7.5 shrink-0 rounded-lg hover:bg-(--gray-400) aria-expanded:bg-(--gray-400)"
354
+ :ui="{ leadingIcon: 'size-4' }"
355
+ />
356
+ </UDropdownMenu>
357
+ </div>
358
+ <UAlert
359
+ v-if="validationError || sourceError || error"
360
+ :title="validationError || sourceError || error"
361
+ color="error"
362
+ variant="subtle"
363
+ class="shrink-0 rounded-none"
364
+ />
365
+ <div class="min-h-0 flex-1 overflow-y-auto">
366
+ <div class="mx-auto grid max-w-180 px-[clamp(16px,4vw,40px)] pt-10 pb-40">
367
+ <UEditor
368
+ ref="visualInput"
369
+ v-slot="{ editor }"
370
+ :model-value="visualMarkdown"
371
+ content-type="markdown"
372
+ :mention="false"
373
+ :inert="mode !== 'visual'"
374
+ :editor-props="{
375
+ attributes: {
376
+ 'aria-label': 'Agent instructions',
377
+ role: 'textbox',
378
+ 'aria-multiline': 'true',
379
+ },
380
+ }"
381
+ placeholder="Write this agent’s responsibilities…"
382
+ :class="[layer, mode === 'visual' ? shown : hidden]"
383
+ :ui="{
384
+ base: 'min-h-120 text-[15px] leading-6 text-default caret-highlighted *:my-0 sm:px-0 [&_p]:mb-3 [&_p]:leading-6 [&_:is(h1,h2,h3,h4,h5,h6)]:font-medium [&_h1]:mb-2 [&_h1]:text-[28px] [&_h1]:leading-9 [&_h1]:tracking-[-0.01em] [&_h1+p]:mb-2 [&_h1+p]:text-muted [&_h2]:mt-7 [&_h2]:mb-1.5 [&_h2]:text-[17px] [&_h2]:leading-[26px] [&_h3]:mt-6 [&_h3]:mb-1.5 [&_h3]:text-base [&_h3]:leading-6 [&_:is(ul,ol)]:mb-3 [&_:is(ul,ol)]:ps-6 [&_li]:my-0 [&_li]:mb-1 [&_li]:ps-0.5 [&_li>p]:mb-0 [&_strong]:font-semibold [&_strong]:text-highlighted [&_code]:inline [&_code]:rounded [&_code]:border-0 [&_code]:bg-(--gray-400) [&_code]:px-[5px] [&_code]:py-px [&_code]:text-[13px] [&_code]:font-normal [&_blockquote]:mb-5 [&_blockquote]:rounded-lg [&_blockquote]:border-s-0 [&_blockquote]:bg-(--gray-300) [&_blockquote]:px-4 [&_blockquote]:py-3 [&_blockquote]:not-italic [&_blockquote]:text-highlighted',
385
+ }"
386
+ @update:model-value="updateVisual"
387
+ >
388
+ <UEditorToolbar
389
+ :editor="editor"
390
+ :items="toolbar"
391
+ layout="bubble"
392
+ />
393
+ </UEditor>
394
+ <SourceEditor
395
+ ref="sourceInput"
396
+ :model-value="source"
397
+ :inert="mode !== 'source'"
398
+ label="Agent Markdown source"
399
+ :class="[layer, mode === 'source' ? shown : hidden]"
400
+ @update:model-value="updateSource"
401
+ />
402
+ </div>
403
+ </div>
404
+ <aside
405
+ class="absolute top-13 right-0 bottom-0 z-10 flex w-[min(440px,100%)] flex-col border-l border-default bg-(--gray-200) pb-19 shadow-[-16px_0_48px_rgb(0_0_0/0.12)] transition-[translate,opacity] duration-260 ease-[cubic-bezier(.2,0,0,1)] motion-reduce:transition-none dark:shadow-[-16px_0_48px_rgb(0_0_0/0.35)]"
406
+ :class="
407
+ isToolsOpen
408
+ ? 'translate-x-0 opacity-100'
409
+ : 'pointer-events-none translate-x-[calc(100%+24px)] opacity-0'
410
+ "
411
+ :inert="!isToolsOpen"
412
+ :aria-hidden="!isToolsOpen"
413
+ >
414
+ <AgentToolPicker
415
+ v-if="isToolsMounted"
416
+ v-model="draft"
417
+ :integration-tools="integrationTools"
418
+ @close="isToolsOpen = false"
419
+ />
420
+ </aside>
421
+ </section>
422
+ <UModal
423
+ v-model:open="isDiscardOpen"
424
+ title="Discard unsaved changes?"
425
+ >
426
+ <template #body>
427
+ <p class="text-sm text-muted">
428
+ Your changes could not be saved. Keep editing to retry, or discard them.
429
+ </p>
430
+ </template>
431
+ <template #footer>
432
+ <div class="flex w-full justify-end gap-2">
433
+ <UButton
434
+ label="Keep editing"
435
+ variant="outline"
436
+ @click="isDiscardOpen = false"
437
+ />
438
+ <UButton
439
+ label="Discard changes"
440
+ color="error"
441
+ @click="discard"
442
+ />
443
+ </div>
444
+ </template>
445
+ </UModal>
446
+ </template>