@nuxtagentic/workspace 0.0.0-stage → 0.1.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.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
- # Temporary Holding Version
1
+ # Workspace contracts
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
+ `@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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
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.0",
29
+ "@nuxtagentic/data": "0.1.0",
30
+ "@nuxtagentic/kit": "0.1.0",
31
+ "@nuxtagentic/identity": "0.1.0"
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
+ }
@@ -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 }