@nuxtagentic/workspace 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.
- package/LICENSE.md +9 -0
- package/README.md +62 -2
- package/module.ts +24 -0
- package/package.json +46 -3
- package/runtime/composables/useAgentActions.ts +39 -0
- package/runtime/composables/useInstalledFeatures.ts +24 -0
- package/runtime/composables/useWorkspaceContext.ts +18 -0
- package/runtime/composables/useWorkspaceModules.ts +126 -0
- package/runtime/composables/useWorkspaceRoute.ts +7 -0
- package/runtime/server/middleware/modules.ts +41 -0
- package/runtime/shared/agent-actions.ts +40 -0
- package/runtime/shared/agent.ts +1 -0
- package/runtime/shared/dashboard-search.ts +50 -0
- package/runtime/shared/modules.ts +6 -0
- package/runtime/types.ts +171 -0
- package/schema.ts +43 -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,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Workspace contracts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@nuxtagentic/workspace` connects feature packages to the host application.
|
|
4
|
+
It supplies contribution types, context and action registration, feature discovery,
|
|
5
|
+
and account-scoped installation state. Its Nuxt dependencies install Identity,
|
|
6
|
+
Data, and Prompt. It does not install dashboard layouts, pages, components, or styles.
|
|
7
|
+
|
|
8
|
+
The starter owns its shell in `app/`: dashboard pages and layout,
|
|
9
|
+
prompt and search controllers, navigation, operation review, settings composition,
|
|
10
|
+
UI defaults, and color tokens. Shell regression tests live in
|
|
11
|
+
`tests/workspace/`. Features keep their own UI, server
|
|
12
|
+
code, schemas, and contributions in their packages.
|
|
13
|
+
|
|
14
|
+
## Connect a feature to the host
|
|
15
|
+
|
|
16
|
+
Available features implement [the contribution contracts](runtime/types.ts).
|
|
17
|
+
They provide collection components, prompt parsers, search providers, resource
|
|
18
|
+
navigation, actions, settings panels, and layout contributions as needed. Kit
|
|
19
|
+
builds `#workspace/features` and `#workspace/modules` from those registrations.
|
|
20
|
+
The host consumes the generated contributions without importing business features.
|
|
21
|
+
|
|
22
|
+
The host layout provides `WorkspaceContext` through `provideWorkspaceContext`
|
|
23
|
+
and an action registry through `provideAgentActionHandlers`. Feature pages read
|
|
24
|
+
`useWorkspaceContext` and call `useWorkspaceRoute` to ask the host for navigation
|
|
25
|
+
based on that page's own Nuxt route. Feature components register active commands
|
|
26
|
+
with `useAgentActionHandler`. These APIs keep feature packages independent of
|
|
27
|
+
application files.
|
|
28
|
+
|
|
29
|
+
See the starter's `app/layouts/dashboard.vue`, `app/composables/useDashboardWorkspace.ts`,
|
|
30
|
+
and `app/composables/useDashboardRoute.ts` for the host implementation. Include the
|
|
31
|
+
starter's editable `app/` shell when installing these packages in another application,
|
|
32
|
+
or supply your own layout and controllers.
|
|
33
|
+
|
|
34
|
+
Input, selection, project, and view state stay in the URL. Changes to the prompt,
|
|
35
|
+
route, or account scope invalidate stale planning results. Mutations finish on
|
|
36
|
+
the server; late results refresh the current scope without replacing a newer
|
|
37
|
+
prompt or restoring a stale completion message.
|
|
38
|
+
|
|
39
|
+
## Installation state
|
|
40
|
+
|
|
41
|
+
`workspaceModels` stores module installation choices; business models stay with
|
|
42
|
+
their features. Compose it into the host's CloudConfig schema. This starter disables
|
|
43
|
+
module management and keeps every registered agentic feature available.
|
|
44
|
+
|
|
45
|
+
Hosts that enable `workspace: { moduleManagement: true }` use `useWorkspaceModules`
|
|
46
|
+
to manage installation state. Defaults come from the generated catalog, and removal
|
|
47
|
+
retains records. Hosts own the catalog page and route guards; the starter's
|
|
48
|
+
`isModuleManagementEnabled` flag in `nuxt.config.ts` controls both the module option
|
|
49
|
+
and whether `/dashboard/modules` is registered.
|
|
50
|
+
|
|
51
|
+
Core features omit their catalog entry and remain available regardless of saved
|
|
52
|
+
installation choices. Agents, Integrations, and Automations use this registration.
|
|
53
|
+
Prompt tools and Google connections use the same installation state on the server.
|
|
54
|
+
The host keys its layout by workspace and installation revision to recreate
|
|
55
|
+
controllers after those values change.
|
|
56
|
+
|
|
57
|
+
## Presentation
|
|
58
|
+
|
|
59
|
+
Dashboard colors live in the host's `app/assets/css/main.css`
|
|
60
|
+
and component defaults in `app/app.config.ts`. Use Nuxt
|
|
61
|
+
UI's semantic `success` and `error` tokens for status indicators and switches.
|
|
62
|
+
Avatars use `bg-avatar` with `--avatar-hue`; shared lightness and chroma keep them
|
|
63
|
+
softer than status indicators.
|
package/module.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
2
|
+
import { registerWorkspaceModule } from '@nuxtagentic/kit'
|
|
3
|
+
|
|
4
|
+
export default defineNuxtModule<{ moduleManagement: boolean }>({
|
|
5
|
+
meta: { name: '@nuxtagentic/workspace', configKey: 'workspace' },
|
|
6
|
+
defaults: { moduleManagement: false },
|
|
7
|
+
moduleDependencies: {
|
|
8
|
+
'@nuxtagentic/identity': {},
|
|
9
|
+
'@nuxtagentic/data': {},
|
|
10
|
+
'@nuxtagentic/prompt': {},
|
|
11
|
+
},
|
|
12
|
+
setup(options, nuxt) {
|
|
13
|
+
const resolver = createResolver(import.meta.url)
|
|
14
|
+
registerWorkspaceModule(
|
|
15
|
+
{
|
|
16
|
+
id: 'workspace',
|
|
17
|
+
runtime: resolver.resolve('./runtime'),
|
|
18
|
+
metadata: resolver.resolve('./runtime/shared/agent.ts'),
|
|
19
|
+
},
|
|
20
|
+
nuxt,
|
|
21
|
+
)
|
|
22
|
+
nuxt.options.runtimeConfig.public.workspaceModules = { managed: options.moduleManagement }
|
|
23
|
+
},
|
|
24
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtagentic/workspace",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"module.ts",
|
|
7
|
+
"runtime",
|
|
8
|
+
"schema.ts",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./module.ts",
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./schema": "./schema.ts",
|
|
16
|
+
"./runtime/*.vue": "./runtime/*.vue",
|
|
17
|
+
"./runtime/*.css": "./runtime/*.css",
|
|
18
|
+
"./runtime/*.json": "./runtime/*.json",
|
|
19
|
+
"./runtime/*.svg": "./runtime/*.svg",
|
|
20
|
+
"./runtime/*": "./runtime/*.ts"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@cloudconfig/core": "0.2.0-beta.2",
|
|
24
|
+
"@nuxt/kit": "4.6.1",
|
|
25
|
+
"@nuxt/ui": "^4.11.1",
|
|
26
|
+
"vue-router": "^5.4.0",
|
|
27
|
+
"h3": "1.15.11",
|
|
28
|
+
"@nuxtagentic/prompt": "0.1.1",
|
|
29
|
+
"@nuxtagentic/data": "0.1.1",
|
|
30
|
+
"@nuxtagentic/identity": "0.1.1",
|
|
31
|
+
"@nuxtagentic/kit": "0.1.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"nuxt": "^4.6.1",
|
|
35
|
+
"vue": "^3.5.43"
|
|
36
|
+
},
|
|
37
|
+
"cloudconfig": {
|
|
38
|
+
"models": "workspaceModels"
|
|
39
|
+
},
|
|
40
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
41
|
+
"description": "Feature contracts and host registration for Nuxt Agentic.",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": "^22.22.3 || ^24.15.0 || >=26.0.0"
|
|
48
|
+
}
|
|
6
49
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
shallowReactive,
|
|
3
|
+
provide,
|
|
4
|
+
inject,
|
|
5
|
+
watchEffect,
|
|
6
|
+
toValue,
|
|
7
|
+
type InjectionKey,
|
|
8
|
+
type MaybeRefOrGetter,
|
|
9
|
+
} from 'vue'
|
|
10
|
+
|
|
11
|
+
export type AgentActionHandler = () => unknown | Promise<unknown>
|
|
12
|
+
const handlersKey: InjectionKey<Map<string, AgentActionHandler>> = Symbol('agent-action-handlers')
|
|
13
|
+
|
|
14
|
+
/** Provide the action registry that feature components populate while active. */
|
|
15
|
+
export function provideAgentActionHandlers() {
|
|
16
|
+
const handlers = shallowReactive(new Map<string, AgentActionHandler>())
|
|
17
|
+
provide(handlersKey, handlers)
|
|
18
|
+
return handlers
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Register a feature action and remove it when the feature becomes inactive or unmounts. */
|
|
22
|
+
export function useAgentActionHandler(
|
|
23
|
+
command: string,
|
|
24
|
+
handler: AgentActionHandler,
|
|
25
|
+
active: MaybeRefOrGetter<boolean>,
|
|
26
|
+
) {
|
|
27
|
+
const handlers = inject(handlersKey, undefined)
|
|
28
|
+
watchEffect((onCleanup) => {
|
|
29
|
+
if (!handlers || !toValue(active)) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
handlers.set(command, handler)
|
|
33
|
+
onCleanup(() => {
|
|
34
|
+
if (handlers.get(command) === handler) {
|
|
35
|
+
handlers.delete(command)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { WORKSPACE_CONTRIBUTIONS, WORKSPACE_FEATURES } from '#workspace/features'
|
|
2
|
+
import { useWorkspaceModules } from './useWorkspaceModules'
|
|
3
|
+
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
4
|
+
|
|
5
|
+
export function findWorkspaceRouteContribution(route: RouteLocationNormalizedLoaded) {
|
|
6
|
+
return (
|
|
7
|
+
WORKSPACE_CONTRIBUTIONS.find((entry) => entry.matchesRoute?.(route)) ??
|
|
8
|
+
(route.path === '/dashboard'
|
|
9
|
+
? WORKSPACE_FEATURES.find(
|
|
10
|
+
(entry) => entry.id === route.query.module || !!route.query[entry.selectionKey],
|
|
11
|
+
)
|
|
12
|
+
: undefined)
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useInstalledContributions() {
|
|
17
|
+
const modules = useWorkspaceModules()
|
|
18
|
+
return WORKSPACE_CONTRIBUTIONS.filter((entry) => modules.isInstalled(entry.moduleId ?? entry.id))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useInstalledFeatures() {
|
|
22
|
+
const modules = useWorkspaceModules()
|
|
23
|
+
return WORKSPACE_FEATURES.filter((entry) => modules.isInstalled(entry.moduleId ?? entry.id))
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { inject, provide, type InjectionKey } from 'vue'
|
|
2
|
+
import type { WorkspaceContext } from '../types'
|
|
3
|
+
|
|
4
|
+
const workspaceContextKey: InjectionKey<WorkspaceContext> = Symbol('workspace-context')
|
|
5
|
+
|
|
6
|
+
/** Connect the host shell to the feature pages rendered within it. */
|
|
7
|
+
export function provideWorkspaceContext(context: WorkspaceContext) {
|
|
8
|
+
provide(workspaceContextKey, context)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Read the host context; the enclosing application layout must provide it. */
|
|
12
|
+
export function useWorkspaceContext() {
|
|
13
|
+
const context = inject(workspaceContextKey)
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error('Feature pages require a layout that provides the workspace context.')
|
|
16
|
+
}
|
|
17
|
+
return context
|
|
18
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
import { useAsyncData, useCloudConfig, useRuntimeConfig, useState } from '#imports'
|
|
3
|
+
import { useWorkspaceScope } from '@nuxtagentic/identity/runtime/composables/useWorkspaceScope'
|
|
4
|
+
import { MODULE_CATALOG } from '#workspace/modules'
|
|
5
|
+
import { isWorkspaceModuleInstalled } from '../shared/modules'
|
|
6
|
+
import type { WorkspaceConfig } from '../../schema'
|
|
7
|
+
|
|
8
|
+
export function useWorkspaceModules() {
|
|
9
|
+
const scope = useWorkspaceScope()
|
|
10
|
+
const managed =
|
|
11
|
+
(useRuntimeConfig().public.workspaceModules as { managed?: boolean }).managed === true
|
|
12
|
+
const defaults = MODULE_CATALOG.filter((entry) => entry.defaultInstalled).map((entry) => entry.id)
|
|
13
|
+
const revision = useState('workspace-modules-revision', () => 0)
|
|
14
|
+
const api = useCloudConfig<WorkspaceConfig>()
|
|
15
|
+
const request = useAsyncData(
|
|
16
|
+
computed(() => `workspace:modules:${scope.value}`),
|
|
17
|
+
async () => {
|
|
18
|
+
if (!managed || !scope.value) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
const workspace = scope.value.slice(scope.value.indexOf(':') + 1)
|
|
22
|
+
const result = await api.workspaceInstallation.list({
|
|
23
|
+
pageSize: 1,
|
|
24
|
+
filters: {
|
|
25
|
+
type: 'group',
|
|
26
|
+
logic: 'and',
|
|
27
|
+
children: [{ type: 'condition', field: 'workspace', operator: 'eq', value: workspace }],
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
return result.data[0] ?? null
|
|
31
|
+
},
|
|
32
|
+
{ deep: false },
|
|
33
|
+
)
|
|
34
|
+
const installed = computed<string[]>(() =>
|
|
35
|
+
!managed
|
|
36
|
+
? MODULE_CATALOG.map((entry) => entry.id)
|
|
37
|
+
: (Array.isArray(request.data.value?.installed)
|
|
38
|
+
? request.data.value.installed
|
|
39
|
+
: defaults
|
|
40
|
+
).filter((id: string) => MODULE_CATALOG.some((entry) => entry.id === id)),
|
|
41
|
+
)
|
|
42
|
+
const busy = ref<string | null>(null)
|
|
43
|
+
const mutationError = ref('')
|
|
44
|
+
|
|
45
|
+
function isInstalled(id: string) {
|
|
46
|
+
return isWorkspaceModuleInstalled(id, installed.value)
|
|
47
|
+
}
|
|
48
|
+
function requiredBy(id: string) {
|
|
49
|
+
return MODULE_CATALOG.filter(
|
|
50
|
+
(entry) => installed.value.includes(entry.id) && entry.dependencies?.includes(id),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
async function setInstalled(id: string, enabled: boolean) {
|
|
54
|
+
if (busy.value || !managed) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
const module = MODULE_CATALOG.find((entry) => entry.id === id)
|
|
58
|
+
if (!module) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
if (!enabled && requiredBy(id).length) {
|
|
62
|
+
mutationError.value = `Remove ${requiredBy(id)
|
|
63
|
+
.map((entry) => entry.label)
|
|
64
|
+
.join(' and ')} first.`
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
busy.value = id
|
|
68
|
+
mutationError.value = ''
|
|
69
|
+
const currentScope = scope.value
|
|
70
|
+
const next = new Set(installed.value)
|
|
71
|
+
function include(key: string) {
|
|
72
|
+
next.add(key)
|
|
73
|
+
for (const dependency of MODULE_CATALOG.find((entry) => entry.id === key)?.dependencies ??
|
|
74
|
+
[]) {
|
|
75
|
+
include(dependency)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (enabled) {
|
|
79
|
+
include(id)
|
|
80
|
+
} else {
|
|
81
|
+
next.delete(id)
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
if (request.error.value) {
|
|
85
|
+
throw request.error.value
|
|
86
|
+
}
|
|
87
|
+
const fields = { installed: [...next] }
|
|
88
|
+
if (request.data.value) {
|
|
89
|
+
await api.workspaceInstallation.update(request.data.value.id, fields)
|
|
90
|
+
} else {
|
|
91
|
+
await api.workspaceInstallation.create({
|
|
92
|
+
...fields,
|
|
93
|
+
workspace: currentScope.slice(currentScope.indexOf(':') + 1),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
if (scope.value !== currentScope) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
await request.refresh()
|
|
100
|
+
if (request.error.value) {
|
|
101
|
+
throw request.error.value
|
|
102
|
+
}
|
|
103
|
+
revision.value++
|
|
104
|
+
return true
|
|
105
|
+
} catch (error) {
|
|
106
|
+
mutationError.value =
|
|
107
|
+
error instanceof Error ? error.message : 'Could not save this module. Try again.'
|
|
108
|
+
return false
|
|
109
|
+
} finally {
|
|
110
|
+
busy.value = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
catalog: MODULE_CATALOG,
|
|
115
|
+
installed,
|
|
116
|
+
managed,
|
|
117
|
+
busy,
|
|
118
|
+
error: request.error,
|
|
119
|
+
mutationError,
|
|
120
|
+
ready: request,
|
|
121
|
+
refresh: request.refresh,
|
|
122
|
+
isInstalled,
|
|
123
|
+
requiredBy,
|
|
124
|
+
setInstalled,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useRoute } from '#imports'
|
|
2
|
+
import { useWorkspaceContext } from './useWorkspaceContext'
|
|
3
|
+
|
|
4
|
+
/** Ask the host to create navigation using this feature page's own Nuxt route. */
|
|
5
|
+
export function useWorkspaceRoute() {
|
|
6
|
+
return useWorkspaceContext().createNavigation(useRoute())
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineEventHandler, getRequestURL } from 'h3'
|
|
2
|
+
import { useRuntimeConfig } from '#imports'
|
|
3
|
+
import { useCloudConfigServer } from '#cloudconfig/server'
|
|
4
|
+
import { MODULE_CATALOG } from '#workspace/modules'
|
|
5
|
+
import type { WorkspaceConfig } from '../../../schema'
|
|
6
|
+
import { useAgentSession } from '@nuxtagentic/prompt/runtime/server/utils/agent-session'
|
|
7
|
+
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
const path = getRequestURL(event).pathname
|
|
10
|
+
if (!/^\/api\/agent(?:\/|$)/.test(path)) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
const runtime = useRuntimeConfig(event)
|
|
14
|
+
if (!(runtime.public.workspaceModules as { managed?: boolean }).managed) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const session = await useAgentSession(event)
|
|
18
|
+
if (!session?.user) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
const api = await useCloudConfigServer<WorkspaceConfig>(event)
|
|
22
|
+
const result = await api.workspaceInstallation.list({
|
|
23
|
+
pageSize: 1,
|
|
24
|
+
filters: {
|
|
25
|
+
type: 'group',
|
|
26
|
+
logic: 'and',
|
|
27
|
+
children: [
|
|
28
|
+
{
|
|
29
|
+
type: 'condition',
|
|
30
|
+
field: 'workspace',
|
|
31
|
+
operator: 'eq',
|
|
32
|
+
value: String(session.session.activeOrganizationId ?? 'personal'),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
const installed = result.data[0]?.installed
|
|
38
|
+
event.context.workspaceModules = Array.isArray(installed)
|
|
39
|
+
? installed.filter((id) => MODULE_CATALOG.some((entry) => entry.id === id))
|
|
40
|
+
: MODULE_CATALOG.filter((entry) => entry.defaultInstalled).map((entry) => entry.id)
|
|
41
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { LocationQueryRaw } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
export interface AgentActionContext {
|
|
4
|
+
/** Current page/resource, URL values, and selected record are available to handlers. */
|
|
5
|
+
page: string
|
|
6
|
+
resource: string | null
|
|
7
|
+
selection: string | null
|
|
8
|
+
read: (key: string) => string
|
|
9
|
+
navigate: (path: string, query?: LocationQueryRaw) => Promise<unknown>
|
|
10
|
+
prompt: (input: string) => Promise<void>
|
|
11
|
+
/** Invoke a handler registered by the active editor or resource component. */
|
|
12
|
+
run: (command: string) => Promise<unknown>
|
|
13
|
+
canRun: (command: string) => boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AgentAction {
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
description: string
|
|
20
|
+
icon: string
|
|
21
|
+
/** Alternative phrases accepted on submit and included in typing suggestions. */
|
|
22
|
+
aliases?: readonly string[]
|
|
23
|
+
available?: (context: AgentActionContext) => boolean
|
|
24
|
+
run: (context: AgentActionContext) => unknown | Promise<unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentActionSuggestion {
|
|
28
|
+
id: string
|
|
29
|
+
name: string
|
|
30
|
+
description: string
|
|
31
|
+
icon: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeActionName(input: string) {
|
|
35
|
+
return input
|
|
36
|
+
.trim()
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.replace(/[.!?]+$/, '')
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { excludedModels: ['workspaceInstallation'] }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Feature IDs come from the app registry; shared search records are extensible. */
|
|
2
|
+
export type SearchModule = string
|
|
3
|
+
export interface SearchLink {
|
|
4
|
+
path: string
|
|
5
|
+
query: Record<string, string>
|
|
6
|
+
}
|
|
7
|
+
export interface SearchResultItem {
|
|
8
|
+
id: number | string
|
|
9
|
+
label: string
|
|
10
|
+
description: string
|
|
11
|
+
icon: string
|
|
12
|
+
to: SearchLink
|
|
13
|
+
}
|
|
14
|
+
export interface SearchResultGroup {
|
|
15
|
+
id: SearchModule
|
|
16
|
+
label: string
|
|
17
|
+
icon: string
|
|
18
|
+
count: number
|
|
19
|
+
items: SearchResultItem[]
|
|
20
|
+
to: SearchLink
|
|
21
|
+
loading?: boolean
|
|
22
|
+
error?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const INTENT_WORDS = new Set(
|
|
26
|
+
(
|
|
27
|
+
'show find search list count filter sort order select users user people team accounts files file ' +
|
|
28
|
+
'please can could would me my all only for from by in on to and or not ' +
|
|
29
|
+
'calendar agenda schedule meeting event create add book today tomorrow week month emails email mail inbox boards board kanban tasks task ' +
|
|
30
|
+
'active banned verified unverified disabled joined before after newest oldest latest ' +
|
|
31
|
+
'role roles admin user who whose where with'
|
|
32
|
+
).split(' '),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
/** Short keyword searches need no AI call; structured queries keep their existing routes. */
|
|
36
|
+
export function getDashboardSearchText(input: string): string {
|
|
37
|
+
const text = input.trim()
|
|
38
|
+
const quoted = /^(?:"([^"]+)"|'([^']+)')$/.exec(text)
|
|
39
|
+
if (quoted) {
|
|
40
|
+
return (quoted[1] ?? quoted[2]!).trim()
|
|
41
|
+
}
|
|
42
|
+
if (!text || /[:<>=?;\n]/.test(text)) {
|
|
43
|
+
return ''
|
|
44
|
+
}
|
|
45
|
+
const words = text.toLowerCase().split(/\s+/)
|
|
46
|
+
if (words.length > 4 || words.some((word) => INTENT_WORDS.has(word))) {
|
|
47
|
+
return ''
|
|
48
|
+
}
|
|
49
|
+
return text
|
|
50
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { MODULE_CATALOG } from '#workspace/modules'
|
|
2
|
+
|
|
3
|
+
export function isWorkspaceModuleInstalled(id: string, installed?: readonly string[]) {
|
|
4
|
+
const module = MODULE_CATALOG.find((entry) => entry.id === id)
|
|
5
|
+
return !module || (installed ? installed.includes(id) : !!module.defaultInstalled)
|
|
6
|
+
}
|
package/runtime/types.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Component, Ref } from 'vue'
|
|
2
|
+
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
|
|
3
|
+
import type {
|
|
4
|
+
LocationQueryRaw,
|
|
5
|
+
NavigationFailure,
|
|
6
|
+
RouteLocationNormalizedLoaded,
|
|
7
|
+
RouteLocationRaw,
|
|
8
|
+
} from 'vue-router'
|
|
9
|
+
import type { AgentToolCall, AgentToolName } from '@nuxtagentic/prompt/runtime/shared/agent-tools'
|
|
10
|
+
import type { AgentAction } from './shared/agent-actions'
|
|
11
|
+
import type { SearchLink, SearchResultGroup } from './shared/dashboard-search'
|
|
12
|
+
|
|
13
|
+
export type WorkspaceNavigationResult =
|
|
14
|
+
Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw
|
|
15
|
+
|
|
16
|
+
export interface WorkspaceNavigation {
|
|
17
|
+
route: RouteLocationNormalizedLoaded
|
|
18
|
+
read: (key: string) => string
|
|
19
|
+
prompt: Ref<string>
|
|
20
|
+
module: Ref<WorkspaceFeature | null>
|
|
21
|
+
selection: Ref<WorkspaceFeature | null>
|
|
22
|
+
hasSelection: Ref<boolean>
|
|
23
|
+
navigate: (patch: LocationQueryRaw, replace?: boolean) => WorkspaceNavigationResult
|
|
24
|
+
back: (query: LocationQueryRaw, overview?: boolean) => WorkspaceNavigationResult
|
|
25
|
+
closeDetail: () => WorkspaceNavigationResult
|
|
26
|
+
goBack: () => WorkspaceNavigationResult
|
|
27
|
+
resolveFeature: (intent: WorkspacePromptIntent | null) => WorkspaceFeature | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkspaceEntryContext {
|
|
31
|
+
input: Ref<string>
|
|
32
|
+
intent: Ref<WorkspacePromptIntent | null>
|
|
33
|
+
keyword: Ref<string>
|
|
34
|
+
navigation: WorkspaceNavigation
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Host state that feature pages consume without importing the application's shell. */
|
|
38
|
+
export interface WorkspaceContext {
|
|
39
|
+
entryContext: WorkspaceEntryContext
|
|
40
|
+
activeFeature: Ref<WorkspaceFeature | null>
|
|
41
|
+
createNavigation: (route: RouteLocationNormalizedLoaded) => WorkspaceNavigation
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkspaceSearchContext {
|
|
45
|
+
text: Ref<string>
|
|
46
|
+
link: (extra?: Record<string, string>) => SearchLink
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WorkspaceSearchProvider {
|
|
50
|
+
featureId: string
|
|
51
|
+
group: Ref<Omit<SearchResultGroup, 'id' | 'label' | 'icon'>>
|
|
52
|
+
meta: Ref<string>
|
|
53
|
+
refresh?: () => void | Promise<void>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WorkspacePromptIntent {
|
|
57
|
+
featureId: string
|
|
58
|
+
action: 'query' | 'create'
|
|
59
|
+
source: 'sql' | 'explicit' | 'jev'
|
|
60
|
+
call?: AgentToolCall
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WorkspacePromptMatch<
|
|
64
|
+
Intent extends WorkspacePromptIntent = WorkspacePromptIntent,
|
|
65
|
+
> {
|
|
66
|
+
intent: Intent | null
|
|
67
|
+
local: boolean
|
|
68
|
+
/** An explicit model prefix survives local parsing failure and can skip routing. */
|
|
69
|
+
resource?: string
|
|
70
|
+
error?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface WorkspacePromptParser<
|
|
74
|
+
Intent extends WorkspacePromptIntent = WorkspacePromptIntent,
|
|
75
|
+
> {
|
|
76
|
+
parse: (input: string) => WorkspacePromptMatch<Intent> | null
|
|
77
|
+
tools?: Partial<Record<AgentToolName, (input: string, call: AgentToolCall) => Intent | null>>
|
|
78
|
+
reset?: () => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface WorkspaceWorkProvider {
|
|
82
|
+
section: Ref<string | null>
|
|
83
|
+
location: (project: number | null) => RouteLocationRaw
|
|
84
|
+
navigation?: Ref<RouteLocationRaw>
|
|
85
|
+
navigationItems?: Ref<readonly NavigationMenuItem[]>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface WorkspacePromptPresentation {
|
|
89
|
+
hidden?: boolean
|
|
90
|
+
class?: string | string[]
|
|
91
|
+
key?: string
|
|
92
|
+
placeholder?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface WorkspaceLayout {
|
|
96
|
+
navbar?: boolean
|
|
97
|
+
expand?: boolean | 'wide'
|
|
98
|
+
edgeToEdge?: boolean
|
|
99
|
+
flat?: boolean
|
|
100
|
+
ownsDetailAside?: boolean
|
|
101
|
+
workspaceClass?: string
|
|
102
|
+
navbarClass?: string
|
|
103
|
+
navbarRightClass?: string
|
|
104
|
+
hideTitle?: boolean
|
|
105
|
+
hideExpand?: boolean
|
|
106
|
+
hideSeparator?: boolean
|
|
107
|
+
immersive?: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface WorkspaceContribution {
|
|
111
|
+
id: string
|
|
112
|
+
moduleId?: string
|
|
113
|
+
label: string
|
|
114
|
+
icon: string
|
|
115
|
+
resource?: string
|
|
116
|
+
preservePrompt?: boolean
|
|
117
|
+
preparePrompt?: (input: string) => string
|
|
118
|
+
selectionKey?: string
|
|
119
|
+
selectionParam?: string
|
|
120
|
+
matchesRoute?: (route: RouteLocationNormalizedLoaded) => boolean
|
|
121
|
+
recordLocation?: (id: number, query: LocationQueryRaw) => RouteLocationRaw
|
|
122
|
+
collectionLocation?: (query: LocationQueryRaw) => RouteLocationRaw
|
|
123
|
+
handoff?: (action: string, route: RouteLocationNormalizedLoaded) => RouteLocationRaw | undefined
|
|
124
|
+
useRefresh?: () => () => Promise<unknown>
|
|
125
|
+
actions?: Record<string, readonly AgentAction[]>
|
|
126
|
+
useActionPage?: () => Ref<string>
|
|
127
|
+
additionalActionScopes?: readonly string[]
|
|
128
|
+
navigation?: {
|
|
129
|
+
order: number
|
|
130
|
+
label?: string
|
|
131
|
+
icon?: string
|
|
132
|
+
group?: 'workspace' | 'create' | 'footer'
|
|
133
|
+
}
|
|
134
|
+
navigationShortcuts?: readonly NavigationMenuItem[]
|
|
135
|
+
projectHome?: boolean
|
|
136
|
+
useWork?: () => WorkspaceWorkProvider
|
|
137
|
+
settingsPanels?: readonly Component[]
|
|
138
|
+
generalPanels?: readonly Component[]
|
|
139
|
+
appearancePanels?: readonly Component[]
|
|
140
|
+
useAppearanceMenu?: () => Ref<DropdownMenuItem[]>
|
|
141
|
+
promptDropComponent?: Component
|
|
142
|
+
promptLeadingComponent?: Component
|
|
143
|
+
usePromptPresentation?: () => Ref<WorkspacePromptPresentation>
|
|
144
|
+
layout?: WorkspaceLayout
|
|
145
|
+
useLayout?: () => Ref<WorkspaceLayout>
|
|
146
|
+
headerComponent?: Component
|
|
147
|
+
sidebarComponent?: Component
|
|
148
|
+
workspaceSwitcherComponent?: Component
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Collection features connect the prompt shell to records and workflows. */
|
|
152
|
+
export interface WorkspaceFeature extends WorkspaceContribution {
|
|
153
|
+
prompt: {
|
|
154
|
+
query: string
|
|
155
|
+
useMeta: () => Ref<string>
|
|
156
|
+
}
|
|
157
|
+
urlKeys: readonly string[]
|
|
158
|
+
selectionKey: string
|
|
159
|
+
component: Component
|
|
160
|
+
entryProps?: (
|
|
161
|
+
expanded: boolean,
|
|
162
|
+
prompt: (input: string) => Promise<void>,
|
|
163
|
+
) => Record<string, unknown>
|
|
164
|
+
statusComponent?: Component
|
|
165
|
+
detailAside?: boolean
|
|
166
|
+
draftKeys?: readonly string[]
|
|
167
|
+
backKeys?: readonly string[]
|
|
168
|
+
reviewIntent?: (intent: WorkspacePromptIntent) => LocationQueryRaw
|
|
169
|
+
createPromptParser?: (referenceDay: Date) => WorkspacePromptParser
|
|
170
|
+
useSearch?: (context: WorkspaceSearchContext) => WorkspaceSearchProvider
|
|
171
|
+
}
|
package/schema.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { model, t } from '@cloudconfig/core'
|
|
2
|
+
|
|
3
|
+
export const workspaceModels = {
|
|
4
|
+
workspaceInstallation: model(
|
|
5
|
+
{
|
|
6
|
+
key: t.text().required().unique().serverOnly(),
|
|
7
|
+
ownerId: t.number().required().serverOnly(),
|
|
8
|
+
workspace: t.text().required(),
|
|
9
|
+
installed: t.json().required(),
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
access: {
|
|
13
|
+
list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
|
|
14
|
+
read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
|
|
15
|
+
create: ({ user }) => !!user,
|
|
16
|
+
update: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
|
|
17
|
+
},
|
|
18
|
+
hooks: {
|
|
19
|
+
beforeCreate: ({ user }, input) => ({
|
|
20
|
+
...input,
|
|
21
|
+
ownerId: user!.id,
|
|
22
|
+
key: `${user!.id}:${input.workspace}`,
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
validate: (input, existing) => {
|
|
26
|
+
if (existing && (input.workspace !== existing.workspace || input.key !== existing.key)) {
|
|
27
|
+
return [
|
|
28
|
+
{ path: 'workspace', message: 'Module settings cannot be moved to another workspace.' },
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
return typeof input.workspace !== 'string' ||
|
|
32
|
+
!input.workspace ||
|
|
33
|
+
!Array.isArray(input.installed) ||
|
|
34
|
+
input.installed.length > 50 ||
|
|
35
|
+
input.installed.some((id) => typeof id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(id))
|
|
36
|
+
? [{ path: 'installed', message: 'Choose valid modules for this workspace.' }]
|
|
37
|
+
: []
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type WorkspaceConfig = { models: typeof workspaceModels }
|