@keemakr/ui-sdk 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/package.json +37 -0
- package/src/define.ts +51 -0
- package/src/index.ts +36 -0
- package/src/schema.ts +316 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keemakr/ui-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Product screens for keemakr marketplace modules: a versioned, server-driven UI tree schema plus defineScreen() handlers. Module code never ships to the browser — keemakr-core validates the tree and renders it with platform components.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/fsztpartners/keemakr.git",
|
|
21
|
+
"directory": "packages/ui-sdk"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"zod": "^4.4.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.19.43",
|
|
31
|
+
"typescript": "^5",
|
|
32
|
+
"zod": "^4.4.3"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20.9"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineScreen — the module-dev half of the contract.
|
|
3
|
+
*
|
|
4
|
+
* A screen is two things: a `load` that returns a ScreenTree, and named
|
|
5
|
+
* `actions` the tree's buttons/forms reference by key. Handlers run
|
|
6
|
+
* server-side in the module's own eve deployment; keemakr-core calls them
|
|
7
|
+
* through the grant-authed screens channel and validates every response
|
|
8
|
+
* against the schemas in ./schema before rendering.
|
|
9
|
+
*
|
|
10
|
+
* `Ctx` is generic on purpose: this package depends on nothing. The eve
|
|
11
|
+
* channel adapter (shipped alongside the reference module) binds Ctx to the
|
|
12
|
+
* eve tool ctx, so inside handlers `useKee(ctx)` works exactly as it does in
|
|
13
|
+
* agent tools — tenant and scopes come from the verified grant, never from
|
|
14
|
+
* params.
|
|
15
|
+
*/
|
|
16
|
+
import type { ActionResultInput, ScreenTreeInput } from './schema';
|
|
17
|
+
|
|
18
|
+
export type ScreenParams = Record<string, string>;
|
|
19
|
+
|
|
20
|
+
export type LoadHandler<Ctx> = (
|
|
21
|
+
ctx: Ctx,
|
|
22
|
+
params: ScreenParams,
|
|
23
|
+
) => ScreenTreeInput | Promise<ScreenTreeInput>;
|
|
24
|
+
|
|
25
|
+
export type ActionHandler<Ctx> = (
|
|
26
|
+
ctx: Ctx,
|
|
27
|
+
input: Record<string, unknown>,
|
|
28
|
+
params: ScreenParams,
|
|
29
|
+
) => ActionResultInput | Promise<ActionResultInput>;
|
|
30
|
+
|
|
31
|
+
export interface ScreenDefinition<Ctx = unknown> {
|
|
32
|
+
load: LoadHandler<Ctx>;
|
|
33
|
+
actions?: Record<string, ActionHandler<Ctx>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ACTION_KEY = /^[a-z][a-z0-9_-]*$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Identity function (the defineProvider pattern): exists for inference and
|
|
40
|
+
* cheap authoring-time validation, not runtime behavior.
|
|
41
|
+
*/
|
|
42
|
+
export function defineScreen<Ctx = unknown>(screen: ScreenDefinition<Ctx>): ScreenDefinition<Ctx> {
|
|
43
|
+
for (const key of Object.keys(screen.actions ?? {})) {
|
|
44
|
+
if (!ACTION_KEY.test(key) || key.length > 64) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`defineScreen: action key "${key}" must be a lowercase slug (a-z, 0-9, -, _), max 64 chars`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return screen;
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @keemakr/ui-sdk — product screens for keemakr marketplace modules.
|
|
2
|
+
//
|
|
3
|
+
// One contract, two consumers (the operator-sdk pattern):
|
|
4
|
+
// - module devs: defineScreen() handlers running in their eve deployment
|
|
5
|
+
// - keemakr-core: validates trees/results with these schemas, renders with
|
|
6
|
+
// platform components, proxies load/action with a grant
|
|
7
|
+
//
|
|
8
|
+
// Module code never ships to the browser. UI is data, not code.
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
UI_SCHEMA_VERSION,
|
|
12
|
+
pressSchema,
|
|
13
|
+
buttonSchema,
|
|
14
|
+
cellSchema,
|
|
15
|
+
nodeSchema,
|
|
16
|
+
screenTreeSchema,
|
|
17
|
+
actionResultSchema,
|
|
18
|
+
screenLoadRequestSchema,
|
|
19
|
+
screenLoadResponseSchema,
|
|
20
|
+
screenActionRequestSchema,
|
|
21
|
+
surfaceSchema,
|
|
22
|
+
} from './schema';
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
Press,
|
|
26
|
+
ScreenNode,
|
|
27
|
+
ScreenTree,
|
|
28
|
+
ActionResult,
|
|
29
|
+
Surface,
|
|
30
|
+
ScreenTreeInput,
|
|
31
|
+
ActionResultInput,
|
|
32
|
+
} from './schema';
|
|
33
|
+
|
|
34
|
+
export { defineScreen } from './define';
|
|
35
|
+
|
|
36
|
+
export type { ScreenDefinition, ScreenParams, LoadHandler, ActionHandler } from './define';
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server-driven UI contract between a module runtime and keemakr-core.
|
|
3
|
+
*
|
|
4
|
+
* A module's `load` handler returns a ScreenTree; core validates it against
|
|
5
|
+
* this schema BEFORE rendering — that validation is the security boundary.
|
|
6
|
+
* Nothing here ever becomes raw HTML: every node maps to a platform-owned
|
|
7
|
+
* component, so module output is data, not code.
|
|
8
|
+
*
|
|
9
|
+
* Versioning: `version` is a literal. A breaking change to any node shape
|
|
10
|
+
* bumps UI_SCHEMA_VERSION and adds a new literal; core keeps rendering old
|
|
11
|
+
* versions until modules migrate.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
export const UI_SCHEMA_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
const slug = z
|
|
18
|
+
.string()
|
|
19
|
+
.regex(/^[a-z][a-z0-9_-]*$/, 'lowercase slug (a-z, 0-9, -, _)')
|
|
20
|
+
.max(64);
|
|
21
|
+
|
|
22
|
+
/** What a button (or table row) does when pressed. */
|
|
23
|
+
export const pressSchema = z.discriminatedUnion('kind', [
|
|
24
|
+
z.object({
|
|
25
|
+
kind: z.literal('action'),
|
|
26
|
+
/** Key into the screen's `actions` map on the module runtime. */
|
|
27
|
+
action: slug,
|
|
28
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
29
|
+
/** If set, core shows this confirmation before dispatching. */
|
|
30
|
+
confirm: z.string().max(500).optional(),
|
|
31
|
+
}),
|
|
32
|
+
z.object({
|
|
33
|
+
kind: z.literal('navigate'),
|
|
34
|
+
/** Another screen key of the same module. */
|
|
35
|
+
screen: slug,
|
|
36
|
+
params: z.record(z.string(), z.string()).optional(),
|
|
37
|
+
}),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const tone = z.enum(['default', 'success', 'warning', 'destructive']);
|
|
41
|
+
|
|
42
|
+
export const buttonSchema = z.object({
|
|
43
|
+
type: z.literal('button'),
|
|
44
|
+
label: z.string().max(80),
|
|
45
|
+
onPress: pressSchema,
|
|
46
|
+
variant: z.enum(['primary', 'secondary', 'destructive']).default('secondary'),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** A table cell. Dates are ISO 8601; core formats them tenant-locale-aware. */
|
|
50
|
+
export const cellSchema = z.discriminatedUnion('kind', [
|
|
51
|
+
z.object({ kind: z.literal('text'), value: z.string().max(500) }),
|
|
52
|
+
z.object({ kind: z.literal('badge'), value: z.string().max(40), tone: tone.default('default') }),
|
|
53
|
+
z.object({ kind: z.literal('date'), value: z.iso.datetime() }),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const choiceCardsSchema = z.object({
|
|
57
|
+
type: z.literal('choice_cards'),
|
|
58
|
+
choices: z
|
|
59
|
+
.array(
|
|
60
|
+
z.object({
|
|
61
|
+
id: z.string().min(1).max(64),
|
|
62
|
+
title: z.string().min(1).max(120),
|
|
63
|
+
description: z.string().max(500).optional(),
|
|
64
|
+
meta: z.string().max(120).optional(),
|
|
65
|
+
selected: z.boolean().default(false),
|
|
66
|
+
onPress: pressSchema,
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(20),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const progressSchema = z.object({
|
|
74
|
+
type: z.literal('progress'),
|
|
75
|
+
steps: z
|
|
76
|
+
.array(
|
|
77
|
+
z.object({
|
|
78
|
+
label: z.string().min(1).max(200),
|
|
79
|
+
status: z.enum(['pending', 'active', 'done', 'error']),
|
|
80
|
+
detail: z.string().max(1000).optional(),
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
.min(1)
|
|
84
|
+
.max(100),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const chartSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
type: z.literal('chart'),
|
|
90
|
+
chartType: z.enum(['bar', 'line']),
|
|
91
|
+
title: z.string().max(120).optional(),
|
|
92
|
+
categories: z.array(z.string().max(80)).min(1).max(50),
|
|
93
|
+
series: z
|
|
94
|
+
.array(
|
|
95
|
+
z.object({
|
|
96
|
+
name: z.string().min(1).max(80),
|
|
97
|
+
values: z.array(z.number().finite()).min(1).max(50),
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
.min(1)
|
|
101
|
+
.max(8),
|
|
102
|
+
})
|
|
103
|
+
.superRefine((chart, ctx) => {
|
|
104
|
+
for (let i = 0; i < chart.series.length; i += 1) {
|
|
105
|
+
if (chart.series[i].values.length !== chart.categories.length) {
|
|
106
|
+
ctx.addIssue({
|
|
107
|
+
code: 'custom',
|
|
108
|
+
path: ['series', i, 'values'],
|
|
109
|
+
message: 'must contain one value per category',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const leafNodeSchema = z.union([
|
|
116
|
+
z.object({ type: z.literal('heading'), text: z.string().max(120) }),
|
|
117
|
+
z.object({
|
|
118
|
+
type: z.literal('text'),
|
|
119
|
+
value: z.string().max(5000),
|
|
120
|
+
muted: z.boolean().default(false),
|
|
121
|
+
}),
|
|
122
|
+
/** Rendered by core's sanitizing markdown renderer — never raw HTML. */
|
|
123
|
+
z.object({ type: z.literal('markdown'), value: z.string().max(20000) }),
|
|
124
|
+
z.object({
|
|
125
|
+
type: z.literal('stat'),
|
|
126
|
+
label: z.string().max(80),
|
|
127
|
+
value: z.string().max(40),
|
|
128
|
+
hint: z.string().max(120).optional(),
|
|
129
|
+
}),
|
|
130
|
+
buttonSchema,
|
|
131
|
+
z.object({
|
|
132
|
+
type: z.literal('form'),
|
|
133
|
+
key: slug,
|
|
134
|
+
fields: z
|
|
135
|
+
.array(
|
|
136
|
+
z.object({
|
|
137
|
+
kind: z.enum(['text', 'textarea', 'select']),
|
|
138
|
+
name: slug,
|
|
139
|
+
label: z.string().max(80),
|
|
140
|
+
value: z.string().max(20000).optional(),
|
|
141
|
+
options: z.array(z.object({ value: z.string(), label: z.string() })).optional(),
|
|
142
|
+
required: z.boolean().default(false),
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
.max(30),
|
|
146
|
+
/** Field values are posted as the action's `input`, keyed by field name. */
|
|
147
|
+
submit: z.object({ label: z.string().max(80), action: slug }),
|
|
148
|
+
}),
|
|
149
|
+
z.object({
|
|
150
|
+
type: z.literal('table'),
|
|
151
|
+
columns: z.array(z.object({ key: slug, label: z.string().max(80) })).max(12),
|
|
152
|
+
rows: z
|
|
153
|
+
.array(
|
|
154
|
+
z.object({
|
|
155
|
+
key: z.string().max(128),
|
|
156
|
+
cells: z.record(slug, cellSchema),
|
|
157
|
+
onPress: pressSchema.optional(),
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
.max(500),
|
|
161
|
+
/** Shown when `rows` is empty — every data-bound surface needs an empty state. */
|
|
162
|
+
empty: z.string().max(200),
|
|
163
|
+
}),
|
|
164
|
+
z.object({
|
|
165
|
+
type: z.literal('timeline'),
|
|
166
|
+
items: z
|
|
167
|
+
.array(
|
|
168
|
+
z.object({
|
|
169
|
+
label: z.string().max(200),
|
|
170
|
+
status: z.enum(['pending', 'active', 'done', 'error']),
|
|
171
|
+
at: z.iso.datetime().optional(),
|
|
172
|
+
detail: z.string().max(1000).optional(),
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
.max(200),
|
|
176
|
+
}),
|
|
177
|
+
/**
|
|
178
|
+
* A HITL approval slot — a REFERENCE, never content. The module places the
|
|
179
|
+
* request id where the card should appear; core resolves it at render time
|
|
180
|
+
* (tenant + this-install + still-pending checks) and renders the platform
|
|
181
|
+
* card wired to the respond endpoint. The module cannot author the card
|
|
182
|
+
* (a forged prompt/options here is ignored — this node carries no fields
|
|
183
|
+
* for them) and the tree transports no PHI, only the opaque id.
|
|
184
|
+
*/
|
|
185
|
+
z.object({ type: z.literal('pending_request'), requestId: z.string().min(1).max(128) }),
|
|
186
|
+
/**
|
|
187
|
+
* The zero-bookkeeping HITL slot: "render ALL of this module's still-pending
|
|
188
|
+
* approvals here." Core fills it from its own pending store — the module
|
|
189
|
+
* needs no request-id tracking (which the id-form above requires). Requests
|
|
190
|
+
* already placed individually via `pending_request` elsewhere in the same
|
|
191
|
+
* tree are not repeated here.
|
|
192
|
+
*/
|
|
193
|
+
z.object({ type: z.literal('pending_requests') }),
|
|
194
|
+
choiceCardsSchema,
|
|
195
|
+
progressSchema,
|
|
196
|
+
chartSchema,
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
type LeafScreenNode = z.infer<typeof leafNodeSchema>;
|
|
200
|
+
type LeafScreenNodeInput = z.input<typeof leafNodeSchema>;
|
|
201
|
+
|
|
202
|
+
export type ScreenNode =
|
|
203
|
+
| LeafScreenNode
|
|
204
|
+
| {
|
|
205
|
+
type: 'section';
|
|
206
|
+
title?: string;
|
|
207
|
+
description?: string;
|
|
208
|
+
layout: 'stack' | 'grid';
|
|
209
|
+
columns: number;
|
|
210
|
+
children: ScreenNode[];
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
type ScreenNodeInput =
|
|
214
|
+
| LeafScreenNodeInput
|
|
215
|
+
| {
|
|
216
|
+
type: 'section';
|
|
217
|
+
title?: string;
|
|
218
|
+
description?: string;
|
|
219
|
+
layout: 'stack' | 'grid';
|
|
220
|
+
columns: number;
|
|
221
|
+
children: ScreenNodeInput[];
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const MAX_SECTION_DEPTH = 8;
|
|
225
|
+
const nodeSchemas = new Map<number, z.ZodType<ScreenNode, ScreenNodeInput>>();
|
|
226
|
+
|
|
227
|
+
function nodeSchemaAtDepth(depth: number): z.ZodType<ScreenNode, ScreenNodeInput> {
|
|
228
|
+
const cached = nodeSchemas.get(depth);
|
|
229
|
+
if (cached) return cached;
|
|
230
|
+
if (depth === MAX_SECTION_DEPTH) return leafNodeSchema;
|
|
231
|
+
|
|
232
|
+
const schema: z.ZodType<ScreenNode, ScreenNodeInput> = z.union([
|
|
233
|
+
leafNodeSchema,
|
|
234
|
+
z.object({
|
|
235
|
+
type: z.literal('section'),
|
|
236
|
+
title: z.string().max(120).optional(),
|
|
237
|
+
description: z.string().max(300).optional(),
|
|
238
|
+
layout: z.enum(['stack', 'grid']),
|
|
239
|
+
columns: z.number().int().min(1).max(4),
|
|
240
|
+
children: z.array(z.lazy(() => nodeSchemaAtDepth(depth + 1))).max(100),
|
|
241
|
+
}),
|
|
242
|
+
]);
|
|
243
|
+
nodeSchemas.set(depth, schema);
|
|
244
|
+
return schema;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const nodeSchema = nodeSchemaAtDepth(0);
|
|
248
|
+
|
|
249
|
+
export const screenTreeSchema = z.object({
|
|
250
|
+
version: z.literal(UI_SCHEMA_VERSION),
|
|
251
|
+
title: z.string().max(120),
|
|
252
|
+
description: z.string().max(300).optional(),
|
|
253
|
+
/** Page-level actions, rendered in the header (e.g. "New draft"). */
|
|
254
|
+
actions: z.array(buttonSchema).max(4).optional(),
|
|
255
|
+
children: z.array(nodeSchema).max(100),
|
|
256
|
+
// ponytail: polling keeps live views (agent activity) simple; switch to the
|
|
257
|
+
// existing run-event NDJSON stream when polling measurably falls short.
|
|
258
|
+
refreshSeconds: z.number().int().min(2).max(300).optional(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/** What an action handler returns. `refresh` re-runs `load` after the action. */
|
|
262
|
+
export const actionResultSchema = z.object({
|
|
263
|
+
toast: z.string().max(200).optional(),
|
|
264
|
+
navigate: z
|
|
265
|
+
.object({ screen: slug, params: z.record(z.string(), z.string()).optional() })
|
|
266
|
+
.optional(),
|
|
267
|
+
refresh: z.boolean().default(true),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/* --------------------------------------------------------------------- */
|
|
271
|
+
/* Wire protocol: core proxy <-> module runtime (grant in Authorization). */
|
|
272
|
+
/* eve runtimes serve these from agent/channels/screens.ts (custom channel */
|
|
273
|
+
/* routes mount at the runtime root): */
|
|
274
|
+
/* POST {runtime}/screens/{screen}/load -> ScreenLoadResponse */
|
|
275
|
+
/* POST {runtime}/screens/{screen}/actions/{action} -> ActionResult */
|
|
276
|
+
/* --------------------------------------------------------------------- */
|
|
277
|
+
|
|
278
|
+
export const screenLoadRequestSchema = z.object({
|
|
279
|
+
params: z.record(z.string(), z.string()).default({}),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
export const screenLoadResponseSchema = z.object({ tree: screenTreeSchema });
|
|
283
|
+
|
|
284
|
+
export const screenActionRequestSchema = z.object({
|
|
285
|
+
params: z.record(z.string(), z.string()).default({}),
|
|
286
|
+
input: z.record(z.string(), z.unknown()).default({}),
|
|
287
|
+
/** Core-generated, forwarded so the handler can be idempotent on retries too
|
|
288
|
+
* (core already dedupes via command_log; this is the belt-and-suspenders
|
|
289
|
+
* half of at-least-once). Optional for runtime version-skew tolerance. */
|
|
290
|
+
idempotencyKey: z.string().max(200).optional(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Manifest addition: `manifest.surfaces[]` in entry.json. Each surface is a
|
|
295
|
+
* side-menu entry pointing at an entry screen. Core's marketplace
|
|
296
|
+
* validateSubmission composes this in (wired in the proxy phase).
|
|
297
|
+
*/
|
|
298
|
+
export const surfaceSchema = z.object({
|
|
299
|
+
key: slug,
|
|
300
|
+
label: z.string().max(60),
|
|
301
|
+
/** Lucide icon name, same convention as entry.json `icon`. */
|
|
302
|
+
icon: z.string().max(60).optional(),
|
|
303
|
+
screen: slug,
|
|
304
|
+
description: z.string().max(200).optional(),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
export type Press = z.infer<typeof pressSchema>;
|
|
308
|
+
export type ScreenTree = z.infer<typeof screenTreeSchema>;
|
|
309
|
+
export type ActionResult = z.infer<typeof actionResultSchema>;
|
|
310
|
+
export type Surface = z.infer<typeof surfaceSchema>;
|
|
311
|
+
|
|
312
|
+
// Authoring-side types: fields with schema defaults (variant, muted, required,
|
|
313
|
+
// refresh…) are optional here; core's parse() fills them in. Handlers return
|
|
314
|
+
// these; renderers consume the parsed output types above.
|
|
315
|
+
export type ScreenTreeInput = z.input<typeof screenTreeSchema>;
|
|
316
|
+
export type ActionResultInput = z.input<typeof actionResultSchema>;
|