@nan0web/ui 1.9.0 → 1.11.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/README.md +97 -12
- package/package.json +54 -25
- package/src/App/Command/DepsCommand.js +3 -4
- package/src/Frame/Props.js +12 -18
- package/src/InterfaceTemplate/InterfaceTemplate.js +9 -7
- package/src/Model/index.js +86 -2
- package/src/StdIn.js +2 -6
- package/src/cli.js +1 -0
- package/src/core/Form/Form.js +8 -7
- package/src/core/Form/Message.js +1 -1
- package/src/core/GeneratorRunner.js +77 -7
- package/src/core/InputAdapter.js +3 -1
- package/src/core/Intent.js +214 -16
- package/src/core/IntentErrorModel.js +6 -1
- package/src/core/Message/Message.js +4 -7
- package/src/core/Message/OutputMessage.js +4 -9
- package/src/core/Stream.js +16 -5
- package/src/core/StreamEntry.js +20 -28
- package/src/core/index.js +2 -1
- package/src/domain/Content.js +196 -0
- package/src/domain/Document.js +17 -0
- package/src/domain/FooterModel.js +37 -19
- package/src/domain/HeaderModel.js +47 -21
- package/src/domain/HeroModel.js +24 -22
- package/src/domain/LayoutModel.js +43 -0
- package/src/domain/ModelAsApp.js +46 -0
- package/src/domain/SandboxModel.js +19 -16
- package/src/domain/app/GalleryCommand.js +53 -0
- package/src/domain/app/GalleryRenderIntent.js +77 -0
- package/src/domain/app/SnapshotAuditor.js +401 -0
- package/src/domain/app/SnapshotRunner.js +264 -0
- package/src/domain/app/UIApp.js +78 -0
- package/src/domain/components/BreadcrumbModel.js +10 -6
- package/src/domain/components/FeatureGridModel.js +62 -0
- package/src/domain/components/MarkdownModel.js +24 -0
- package/src/domain/components/ShellModel.js +243 -0
- package/src/domain/components/TableModel.js +10 -6
- package/src/domain/components/ToastModel.js +10 -6
- package/src/domain/components/index.js +3 -1
- package/src/domain/index.js +14 -4
- package/src/index.js +21 -2
- package/src/inspect.js +2 -0
- package/src/test/ScenarioAdapter.js +59 -0
- package/src/test/ScenarioTest.js +51 -0
- package/src/test/ScenarioTest.story.js +56 -0
- package/src/testing/CrashReporter.js +56 -0
- package/src/testing/GalleryGenerator.js +29 -0
- package/src/testing/LogicInspector.js +55 -0
- package/src/testing/SnapshotRunner.js +22 -0
- package/src/testing/SpecAdapter.js +115 -0
- package/src/testing/SpecRunner.js +121 -0
- package/src/testing/VisualAdapter.js +46 -0
- package/src/testing/index.js +7 -0
- package/src/testing/verifySnapshot.js +17 -0
- package/types/App/Command/DepsCommand.d.ts +0 -2
- package/types/Model/index.d.ts +56 -4
- package/types/StdIn.d.ts +3 -3
- package/types/cli.d.ts +1 -0
- package/types/core/Form/Form.d.ts +2 -2
- package/types/core/GeneratorRunner.d.ts +18 -1
- package/types/core/InputAdapter.d.ts +2 -1
- package/types/core/Intent.d.ts +232 -26
- package/types/core/IntentErrorModel.d.ts +4 -0
- package/types/core/Message/Message.d.ts +2 -2
- package/types/core/Message/OutputMessage.d.ts +0 -2
- package/types/core/index.d.ts +2 -1
- package/types/domain/Content.d.ts +340 -0
- package/types/domain/Document.d.ts +21 -0
- package/types/domain/FooterModel.d.ts +22 -12
- package/types/domain/HeaderModel.d.ts +36 -13
- package/types/domain/HeroModel.d.ts +19 -17
- package/types/domain/LayoutModel.d.ts +34 -0
- package/types/domain/ModelAsApp.d.ts +23 -0
- package/types/domain/SandboxModel.d.ts +10 -0
- package/types/domain/app/GalleryCommand.d.ts +55 -0
- package/types/domain/app/GalleryRenderIntent.d.ts +31 -0
- package/types/domain/app/SnapshotAuditor.d.ts +99 -0
- package/types/domain/app/SnapshotRunner.d.ts +45 -0
- package/types/domain/app/UIApp.d.ts +60 -0
- package/types/domain/components/BreadcrumbModel.d.ts +6 -8
- package/types/domain/components/FeatureGridModel.d.ts +50 -0
- package/types/domain/components/MarkdownModel.d.ts +19 -0
- package/types/domain/components/ShellModel.d.ts +56 -0
- package/types/domain/components/TableModel.d.ts +4 -0
- package/types/domain/components/ToastModel.d.ts +4 -0
- package/types/domain/components/index.d.ts +3 -0
- package/types/domain/index.d.ts +10 -4
- package/types/index.d.ts +19 -1
- package/types/inspect.d.ts +2 -0
- package/types/test/ScenarioAdapter.d.ts +43 -0
- package/types/test/ScenarioTest.d.ts +24 -0
- package/types/test/ScenarioTest.story.d.ts +1 -0
- package/types/testing/CrashReporter.d.ts +13 -0
- package/types/testing/GalleryGenerator.d.ts +1 -0
- package/types/testing/LogicInspector.d.ts +22 -0
- package/types/testing/SnapshotRunner.d.ts +7 -0
- package/types/testing/SpecAdapter.d.ts +57 -0
- package/types/testing/SpecRunner.d.ts +41 -0
- package/types/testing/VisualAdapter.d.ts +9 -0
- package/types/testing/index.d.ts +7 -0
- package/types/testing/verifySnapshot.d.ts +14 -0
- package/src/README.md.js +0 -436
- package/types/App/Command/Options.d.ts +0 -43
- package/types/App/Command/index.d.ts +0 -8
- package/types/App/User/Command/Options.d.ts +0 -34
- package/types/core/Message/InputMessage.d.ts +0 -71
- package/types/domain/components/HeroModel.d.ts +0 -24
- package/types/domain/components/ShowcaseAppModel.d.ts +0 -32
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* instead of writing its own while(true) loop.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { validateIntent } from './Intent.js'
|
|
15
|
+
import { validateIntent, result } from './Intent.js'
|
|
16
16
|
import { IntentErrorModel } from './IntentErrorModel.js'
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -21,8 +21,14 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
21
21
|
* Handler for 'ask' intents. Must return { value: ... }.
|
|
22
22
|
* @property {(intent: import('./Intent.js').ProgressIntent) => void | Promise<void>} [progress]
|
|
23
23
|
* Handler for 'progress' intents. Optional (defaults to no-op).
|
|
24
|
+
* @property {(intent: import('./Intent.js').ShowIntent) => void | Promise<void>} [show]
|
|
25
|
+
* Handler for 'show' intents. Optional (defaults to no-op).
|
|
24
26
|
* @property {(intent: import('./Intent.js').LogIntent) => void | Promise<void>} [log]
|
|
25
|
-
* Handler for 'log' intents. Optional
|
|
27
|
+
* Handler for 'log' intents. Optional.
|
|
28
|
+
* @property {(intent: import('./Intent.js').AgentIntent) => Promise<import('./Intent.js').AgentResponse>} [agent]
|
|
29
|
+
* Handler for 'agent' intents (AI Subagents). Optional (fallback to show if not implemented).
|
|
30
|
+
* @property {(intent: import('./Intent.js').RenderIntent) => void | Promise<void>} [render]
|
|
31
|
+
* Handler for 'render' intents (visual component injection). Optional.
|
|
26
32
|
* @property {(intent: import('./Intent.js').ResultIntent) => void | Promise<void>} [result]
|
|
27
33
|
* Handler for the final 'result'. Optional (defaults to no-op).
|
|
28
34
|
*/
|
|
@@ -35,6 +41,9 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
35
41
|
* Set to a positive value for CLI/Chat adapters where hanging is unacceptable.
|
|
36
42
|
* @property {AbortSignal} [signal]
|
|
37
43
|
* External AbortSignal for cancellation from outside.
|
|
44
|
+
* @property {import('./Intent.js').Intent[]} [trace]
|
|
45
|
+
* Array where all executed intents will be sequentially recorded.
|
|
46
|
+
* Useful for generating 'crash reports' or Nan0Spec files on failure.
|
|
38
47
|
*/
|
|
39
48
|
|
|
40
49
|
/**
|
|
@@ -51,18 +60,18 @@ function withTimeout(promise, ms, label) {
|
|
|
51
60
|
if (!ms || ms <= 0) return promise
|
|
52
61
|
|
|
53
62
|
return new Promise((resolve, reject) => {
|
|
54
|
-
const timer = setTimeout(() => {
|
|
63
|
+
const timer = globalThis.setTimeout(() => {
|
|
55
64
|
const error = IntentErrorModel.error('timeout', { label, ms })
|
|
56
65
|
reject(error)
|
|
57
66
|
}, ms)
|
|
58
67
|
|
|
59
68
|
promise.then(
|
|
60
69
|
(val) => {
|
|
61
|
-
clearTimeout(timer)
|
|
70
|
+
globalThis.clearTimeout(timer)
|
|
62
71
|
resolve(val)
|
|
63
72
|
},
|
|
64
73
|
(err) => {
|
|
65
|
-
clearTimeout(timer)
|
|
74
|
+
globalThis.clearTimeout(timer)
|
|
66
75
|
reject(err)
|
|
67
76
|
},
|
|
68
77
|
)
|
|
@@ -103,7 +112,7 @@ export async function runGenerator(generator, handlers, options = {}) {
|
|
|
103
112
|
while (true) {
|
|
104
113
|
// ─── Check external abort signal ───
|
|
105
114
|
if (signal?.aborted) {
|
|
106
|
-
await generator.return(
|
|
115
|
+
await generator.return(/** @type {any} */ (result(null)))
|
|
107
116
|
const error = IntentErrorModel.error('aborted')
|
|
108
117
|
error.name = 'AbortError'
|
|
109
118
|
throw error
|
|
@@ -138,6 +147,11 @@ export async function runGenerator(generator, handlers, options = {}) {
|
|
|
138
147
|
// ─── Validate intent structure (the Judge) ───
|
|
139
148
|
validateIntent(intent)
|
|
140
149
|
|
|
150
|
+
// Record intent into the global trace if provided (for Crash Reports in Nan0Spec format).
|
|
151
|
+
if (options.trace && Array.isArray(options.trace)) {
|
|
152
|
+
options.trace.push(intent)
|
|
153
|
+
}
|
|
154
|
+
|
|
141
155
|
// ─── Dispatch to adapter handler ───
|
|
142
156
|
switch (intent.type) {
|
|
143
157
|
case 'ask': {
|
|
@@ -165,7 +179,7 @@ export async function runGenerator(generator, handlers, options = {}) {
|
|
|
165
179
|
break
|
|
166
180
|
}
|
|
167
181
|
|
|
168
|
-
// Run field validation if schema has a validator
|
|
182
|
+
// Run field validation if schema has a validator
|
|
169
183
|
if (!intent.model) {
|
|
170
184
|
/** @type {import('./Intent.js').FieldSchema} */
|
|
171
185
|
const fieldSchema = /** @type {import('./Intent.js').FieldSchema} */ (intent.schema)
|
|
@@ -180,6 +194,17 @@ export async function runGenerator(generator, handlers, options = {}) {
|
|
|
180
194
|
}
|
|
181
195
|
}
|
|
182
196
|
|
|
197
|
+
if (options.trace && Array.isArray(options.trace)) {
|
|
198
|
+
const lastTrace = options.trace[options.trace.length - 1]
|
|
199
|
+
if (lastTrace && lastTrace.type === 'ask') {
|
|
200
|
+
// Store raw data for Crash Reports & Nan0Spec serialization
|
|
201
|
+
lastTrace.$value =
|
|
202
|
+
typeof response.value === 'object' && response.value !== null
|
|
203
|
+
? JSON.parse(JSON.stringify(response.value))
|
|
204
|
+
: response.value
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
// Instantiate Model-as-Schema class with collected data
|
|
184
209
|
if (intent.model && typeof intent.schema === 'function') {
|
|
185
210
|
const ModelClass = /** @type {new (data: any) => any} */ (intent.schema)
|
|
@@ -198,6 +223,14 @@ export async function runGenerator(generator, handlers, options = {}) {
|
|
|
198
223
|
break
|
|
199
224
|
}
|
|
200
225
|
|
|
226
|
+
case 'show': {
|
|
227
|
+
if (handlers.show) {
|
|
228
|
+
await handlers.show(intent)
|
|
229
|
+
}
|
|
230
|
+
nextVal = undefined
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
|
|
201
234
|
case 'log': {
|
|
202
235
|
if (handlers.log) {
|
|
203
236
|
await handlers.log(intent)
|
|
@@ -206,6 +239,43 @@ export async function runGenerator(generator, handlers, options = {}) {
|
|
|
206
239
|
break
|
|
207
240
|
}
|
|
208
241
|
|
|
242
|
+
case 'render': {
|
|
243
|
+
if (handlers.render) {
|
|
244
|
+
await handlers.render(intent)
|
|
245
|
+
}
|
|
246
|
+
nextVal = undefined
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case 'agent': {
|
|
251
|
+
if (handlers.agent) {
|
|
252
|
+
const response = await handlers.agent(intent)
|
|
253
|
+
|
|
254
|
+
if (options.trace && Array.isArray(options.trace)) {
|
|
255
|
+
const lastTrace = options.trace[options.trace.length - 1]
|
|
256
|
+
if (lastTrace && lastTrace.type === 'agent') {
|
|
257
|
+
// Record agent response for full trace replayability
|
|
258
|
+
/** @type {any} */ ;(lastTrace).$success = response.success
|
|
259
|
+
if (response.files) /** @type {any} */ (lastTrace).$files = response.files
|
|
260
|
+
if (response.message) /** @type {any} */ (lastTrace).$message = response.message
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
nextVal = response
|
|
265
|
+
} else {
|
|
266
|
+
// Fallback to show if agent goes unsupported by adapter
|
|
267
|
+
if (handlers.show) {
|
|
268
|
+
await handlers.show({
|
|
269
|
+
type: 'show',
|
|
270
|
+
level: 'warn',
|
|
271
|
+
message: `[Agent Task] ${intent.task}`,
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
nextVal = { success: false, message: 'Agent not supported by current UI adapter' }
|
|
275
|
+
}
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
|
|
209
279
|
default:
|
|
210
280
|
throw IntentErrorModel.error('unhandled_intent', { type: /** @type {any} */ (intent).type })
|
|
211
281
|
}
|
package/src/core/InputAdapter.js
CHANGED
|
@@ -8,7 +8,7 @@ import UiMessage from './Message/Message.js'
|
|
|
8
8
|
* @class InputAdapter
|
|
9
9
|
* @extends Event
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
11
|
+
export class InputAdapter extends Event {
|
|
12
12
|
static CancelError = CancelError
|
|
13
13
|
/** @returns {typeof CancelError} */
|
|
14
14
|
get CancelError() {
|
|
@@ -59,3 +59,5 @@ export default class InputAdapter extends Event {
|
|
|
59
59
|
throw new Error('select() method must be implemented in subclass')
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
export default InputAdapter
|
package/src/core/Intent.js
CHANGED
|
@@ -38,17 +38,32 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
38
38
|
* @typedef {Object} ProgressIntent
|
|
39
39
|
* @property {'progress'} type
|
|
40
40
|
* @property {number} [value] - Progress value (0-1).
|
|
41
|
-
* @property {
|
|
41
|
+
* @property {number} [total] - Absolute total (if value is absolute).
|
|
42
|
+
* @property {string} [id] - Progress ID for tracking by Adapter to calculate speed/eta.
|
|
42
43
|
* @property {string} message - Status message from Model (i18n static field value).
|
|
43
44
|
*/
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
+
* @typedef {'info' | 'warn' | 'error' | 'success'} ShowLevel
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** @typedef {ShowLevel} LogLevel */
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Model emits a show message. No response expected.
|
|
47
54
|
* Message MUST come from the Model (i18n static field value).
|
|
55
|
+
* @typedef {Object} ShowIntent
|
|
56
|
+
* @property {'show'} type
|
|
57
|
+
* @property {ShowLevel} level
|
|
58
|
+
* @property {string} message - Show message from Model (i18n static field value).
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Model emits a log message intended for debugging/developer (Not UI).
|
|
48
63
|
* @typedef {Object} LogIntent
|
|
49
64
|
* @property {'log'} type
|
|
50
|
-
* @property {
|
|
51
|
-
* @property {string} message -
|
|
65
|
+
* @property {LogLevel} level
|
|
66
|
+
* @property {string} message - Internal log message.
|
|
52
67
|
*/
|
|
53
68
|
|
|
54
69
|
/**
|
|
@@ -58,9 +73,42 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
58
73
|
* @property {*} data - The raw result data (JSON-serializable).
|
|
59
74
|
*/
|
|
60
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Model requests rendering of a pure UI component (Header, Footer, Static Map).
|
|
78
|
+
* No response expected from the logic loop.
|
|
79
|
+
* @typedef {Object} RenderIntent
|
|
80
|
+
* @property {'render'} type
|
|
81
|
+
* @property {string} component - Component name (e.g. 'App.Layout.Header').
|
|
82
|
+
* @property {object} props - Static props for the component.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Contextual data and attachments for the AI subagent.
|
|
87
|
+
* @typedef {Object} AgentContext
|
|
88
|
+
* @property {string[]} [instructions] - List of instructions or guidelines (e.g. ['Use 1-char emojis only']).
|
|
89
|
+
* @property {Record<string, string>} [files] - Hash map of file paths to their string contents.
|
|
90
|
+
* @property {Record<string, any>} [data] - Any arbitrary JSON data (e.g. parsed errors, ASTs, metadata) useful for the task.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Model delegates a task to an AI subagent. The Adapter should launch the agent
|
|
95
|
+
* with the provided task and context, and return the result. If the agent is skipped,
|
|
96
|
+
* it returns { success: false } but allows user to generate a prompt.
|
|
97
|
+
* @typedef {Object} AgentIntent
|
|
98
|
+
* @property {'agent'} type
|
|
99
|
+
* @property {string} task - The instructional task for the AI agent.
|
|
100
|
+
* @property {AgentContext} context - Contextual data, files, and instructions for the task.
|
|
101
|
+
* @property {() => string} toPrompt - Helper to format task and context as an LLM prompt.
|
|
102
|
+
*/
|
|
103
|
+
|
|
61
104
|
/**
|
|
62
105
|
* Union of all possible yielded intents.
|
|
63
|
-
* @typedef {AskIntent | ProgressIntent | LogIntent
|
|
106
|
+
* @typedef {(AskIntent | ProgressIntent | LogIntent | ShowIntent | RenderIntent | AgentIntent | ResultIntent) & {
|
|
107
|
+
* $value?: any;
|
|
108
|
+
* $success?: boolean;
|
|
109
|
+
* $files?: Record<string, string>;
|
|
110
|
+
* $message?: string;
|
|
111
|
+
* }} Intent
|
|
64
112
|
*/
|
|
65
113
|
|
|
66
114
|
// ─── Response Types (Adapter → Model) ───
|
|
@@ -73,6 +121,21 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
73
121
|
* @property {boolean} [cancelled] - Whether the user cancelled this interaction (e.g. pressed ESC).
|
|
74
122
|
*/
|
|
75
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Response to an AgentIntent.
|
|
126
|
+
* The underlying Adapter (Orchestrator) is responsible for communicating with the LLM,
|
|
127
|
+
* enforcing output formats (e.g. Unified Diff or Tool Calls like `updateFile`),
|
|
128
|
+
* and resolving common LLM hallucinations (like Grok truncating code with `// ...`).
|
|
129
|
+
*
|
|
130
|
+
* The Model (e.g. IconsAuditor) receives this clean, resolved response and does not
|
|
131
|
+
* need to parse Markdown or interpret diffs itself.
|
|
132
|
+
*
|
|
133
|
+
* @typedef {Object} AgentResponse
|
|
134
|
+
* @property {boolean} success - True if the agent successfully processed the task.
|
|
135
|
+
* @property {Record<string, string>} [files] - Hash map of fully resolved, updated file contents.
|
|
136
|
+
* @property {string} [message] - Optional summary or explanation returned by the AI.
|
|
137
|
+
*/
|
|
138
|
+
|
|
76
139
|
// ─── Abort Support ───
|
|
77
140
|
|
|
78
141
|
/**
|
|
@@ -92,10 +155,14 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
92
155
|
|
|
93
156
|
/**
|
|
94
157
|
* Union of all possible responses an Adapter can send back via iterator.next().
|
|
95
|
-
* @typedef {AskResponse | AbortResponse | undefined} IntentResponse
|
|
158
|
+
* @typedef {AskResponse | AgentResponse | AbortResponse | undefined} IntentResponse
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @typedef {'ask' | 'show' | 'progress' | 'render' | 'agent'} IntentType
|
|
96
163
|
*/
|
|
97
164
|
|
|
98
|
-
export const INTENT_TYPES = /** @type {const} */ (['ask', 'progress', 'log'])
|
|
165
|
+
export const INTENT_TYPES = /** @type {const} */ (['ask', 'progress', 'show', 'log', 'render', 'agent'])
|
|
99
166
|
|
|
100
167
|
/**
|
|
101
168
|
* Detects if a value is a Model-as-Schema class (has static fields with `help`).
|
|
@@ -104,7 +171,7 @@ export const INTENT_TYPES = /** @type {const} */ (['ask', 'progress', 'log'])
|
|
|
104
171
|
*/
|
|
105
172
|
export function isModelSchema(schema) {
|
|
106
173
|
if (typeof schema !== 'function') return false
|
|
107
|
-
return Object.
|
|
174
|
+
return Object.getOwnPropertyNames(schema).some((key) => {
|
|
108
175
|
const meta = schema[key]
|
|
109
176
|
return meta && typeof meta === 'object' && 'help' in meta
|
|
110
177
|
})
|
|
@@ -133,16 +200,29 @@ export function validateIntent(intent) {
|
|
|
133
200
|
}
|
|
134
201
|
// Accept both: plain schema {help: '...'} and Model-as-Schema class/instance
|
|
135
202
|
const isModel = !!intent.model
|
|
136
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
!isModel &&
|
|
205
|
+
(!intent.schema || typeof intent.schema !== 'object' || !('help' in intent.schema))
|
|
206
|
+
) {
|
|
137
207
|
throw IntentErrorModel.error('ask_missing_schema_help')
|
|
138
208
|
}
|
|
139
209
|
}
|
|
140
|
-
if (intent.type === 'progress' || intent.type === 'log') {
|
|
141
|
-
const
|
|
142
|
-
if (!
|
|
210
|
+
if (intent.type === 'progress' || intent.type === 'show' || intent.type === 'log') {
|
|
211
|
+
const isComponentShow = (intent.type === 'show' || intent.type === 'log') && intent.component
|
|
212
|
+
if (!isComponentShow && typeof intent.message !== 'string') {
|
|
143
213
|
throw IntentErrorModel.error('intent_missing_message', { type: intent.type })
|
|
144
214
|
}
|
|
145
215
|
}
|
|
216
|
+
if (intent.type === 'render') {
|
|
217
|
+
if (typeof intent.component !== 'string' || !intent.component) {
|
|
218
|
+
throw IntentErrorModel.error('render_missing_component')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (intent.type === 'agent') {
|
|
222
|
+
if (typeof intent.task !== 'string' || !intent.task) {
|
|
223
|
+
throw IntentErrorModel.error('agent_missing_task')
|
|
224
|
+
}
|
|
225
|
+
}
|
|
146
226
|
return true
|
|
147
227
|
}
|
|
148
228
|
|
|
@@ -157,13 +237,131 @@ export function validateIntent(intent) {
|
|
|
157
237
|
* @param {object | Function} schema - Field descriptor or Model-as-Schema class.
|
|
158
238
|
* @returns {AskIntent}
|
|
159
239
|
*/
|
|
160
|
-
export
|
|
240
|
+
export function ask(field, schema) {
|
|
161
241
|
if (isModelSchema(schema)) {
|
|
162
242
|
return { type: 'ask', field, schema, model: true }
|
|
163
243
|
}
|
|
164
244
|
return { type: 'ask', field, schema }
|
|
165
245
|
}
|
|
166
246
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Create a progress intent.
|
|
249
|
+
* @param {string} message - Status message from Model (i18n static field value).
|
|
250
|
+
* @param {number} [value=0] - Progress value (current step or percentage).
|
|
251
|
+
* @param {number|string} [totalOrId] - Absolute total steps (number) OR progress tracking ID (string).
|
|
252
|
+
* @param {string} [id='default'] - Progress ID (if total is provided).
|
|
253
|
+
* @returns {ProgressIntent}
|
|
254
|
+
*/
|
|
255
|
+
export function progress(message, value = 0, totalOrId, id) {
|
|
256
|
+
let total = undefined
|
|
257
|
+
let progressId = 'default'
|
|
258
|
+
|
|
259
|
+
if (typeof totalOrId === 'number') {
|
|
260
|
+
total = totalOrId
|
|
261
|
+
if (typeof id === 'string') progressId = id
|
|
262
|
+
} else if (typeof totalOrId === 'string') {
|
|
263
|
+
progressId = totalOrId
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { type: 'progress', message, value, total, id: progressId }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function log(level, message, data = {}) {
|
|
270
|
+
return { type: 'log', level, message, ...data }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Create a render intent.
|
|
275
|
+
* @param {string} component - Component name (e.g. 'App.Layout.Header').
|
|
276
|
+
* @param {object} [props] - Static props for the component.
|
|
277
|
+
* @returns {RenderIntent}
|
|
278
|
+
*/
|
|
279
|
+
export function render(component, props = {}) {
|
|
280
|
+
return { type: 'render', component, props }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create a result intent.
|
|
285
|
+
* @param {*} data - The raw result data.
|
|
286
|
+
* @returns {ResultIntent}
|
|
287
|
+
*/
|
|
288
|
+
export function result(data) {
|
|
289
|
+
return { type: 'result', data }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @typedef {Object} ShowData
|
|
294
|
+
* @property {any} [component]
|
|
295
|
+
* @property {import('@nan0web/types').Model} [model]
|
|
296
|
+
*/
|
|
297
|
+
/**
|
|
298
|
+
* Create a show intent.
|
|
299
|
+
* @param {string | any} message Message to display.
|
|
300
|
+
* @param {ShowLevel|ShowData} [level='info'] Level of message or additional data then `level = 'info'`.
|
|
301
|
+
* @param {ShowData} [data={}] Additional data to display.
|
|
302
|
+
* @returns {ShowIntent}
|
|
303
|
+
*/
|
|
304
|
+
export function show(message, level = 'info', data = {}) {
|
|
305
|
+
if ('string' === typeof level) {
|
|
306
|
+
return { type: 'show', level, message, ...data }
|
|
307
|
+
}
|
|
308
|
+
return { type: 'show', level: 'info', message, ...level, ...data }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create an agent intent to delegate a task to an AI subagent.
|
|
313
|
+
* @param {string} task - The instructional task for the AI agent.
|
|
314
|
+
* @param {AgentContext} [context={}] - Contextual data (files, errors, docs).
|
|
315
|
+
* @returns {AgentIntent}
|
|
316
|
+
*/
|
|
317
|
+
export function agent(task, context = {}) {
|
|
318
|
+
return {
|
|
319
|
+
type: 'agent',
|
|
320
|
+
task,
|
|
321
|
+
context,
|
|
322
|
+
toPrompt() {
|
|
323
|
+
let ctxStr = ''
|
|
324
|
+
if (this.context.data) {
|
|
325
|
+
try {
|
|
326
|
+
ctxStr = JSON.stringify(this.context.data, null, 2)
|
|
327
|
+
} catch (e) {
|
|
328
|
+
ctxStr = String(this.context.data)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Format input files for the LLM using the boundary concept
|
|
333
|
+
const filesStr = Object.entries(this.context.files || {})
|
|
334
|
+
.map(([path, content]) => `---boundary:${path}---\n${content}\n---boundary---`)
|
|
335
|
+
.join('\n\n')
|
|
336
|
+
|
|
337
|
+
const outputRules = `
|
|
338
|
+
[Output Format Rules]
|
|
339
|
+
You must return your code modifications using the following strictly parsable boundary format. Do NOT use markdown code blocks (\`\`\`) or JSON for your code outputs.
|
|
340
|
+
|
|
341
|
+
To replace an ENTIRE file:
|
|
342
|
+
---boundary:path/to/file.js---
|
|
343
|
+
<full new file content here>
|
|
344
|
+
---boundary---
|
|
345
|
+
|
|
346
|
+
To replace a SPECIFIC SNIPPET (e.g. replacing 3 lines starting at line 33):
|
|
347
|
+
---boundary:path/to/file.js:33:3---
|
|
348
|
+
<new snippet content here>
|
|
349
|
+
---boundary---
|
|
350
|
+
`
|
|
351
|
+
const inst = Array.isArray(this.context.instructions)
|
|
352
|
+
? this.context.instructions.join('\n')
|
|
353
|
+
: this.context.instructions
|
|
354
|
+
|
|
355
|
+
return [
|
|
356
|
+
`[Subagent Task]`,
|
|
357
|
+
this.task,
|
|
358
|
+
inst ? `\n[Instructions]\n${inst}` : '',
|
|
359
|
+
ctxStr ? `\n[Context]\n${ctxStr}` : '',
|
|
360
|
+
filesStr ? `\n[Files]\n${filesStr}` : '',
|
|
361
|
+
outputRules,
|
|
362
|
+
]
|
|
363
|
+
.filter(Boolean)
|
|
364
|
+
.join('\n')
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -39,7 +39,12 @@ export class IntentErrorModel {
|
|
|
39
39
|
|
|
40
40
|
static intent_missing_message = {
|
|
41
41
|
help: 'Progress and Log intents require a message',
|
|
42
|
-
error: '
|
|
42
|
+
error: "'{type}' intent requires a \"message\" string",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static render_missing_component = {
|
|
46
|
+
help: 'Render intent requires a component name',
|
|
47
|
+
error: 'Render intent requires a non-empty "component" string',
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
// ─── Runner Contract Errors ───
|
|
@@ -43,11 +43,6 @@ export default class UiMessage extends Message {
|
|
|
43
43
|
NAVIGATION: 'navigation',
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/** @type {string} */
|
|
47
|
-
type = ''
|
|
48
|
-
/** @type {string} */
|
|
49
|
-
id = ''
|
|
50
|
-
|
|
51
46
|
/**
|
|
52
47
|
* Creates a UiMessage.
|
|
53
48
|
*
|
|
@@ -56,9 +51,11 @@ export default class UiMessage extends Message {
|
|
|
56
51
|
constructor(input = {}) {
|
|
57
52
|
super(input)
|
|
58
53
|
|
|
59
|
-
const { type
|
|
54
|
+
const { type, id } = input
|
|
55
|
+
/** @type {string} */
|
|
60
56
|
this.id = id || `ui-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
61
|
-
|
|
57
|
+
/** @type {string} */
|
|
58
|
+
this.type = type ? String(type) : ''
|
|
62
59
|
|
|
63
60
|
if (!('body' in input) && 'content' in input) {
|
|
64
61
|
this.body = Array.isArray(input.content) ? input.content : [input.content]
|
|
@@ -14,15 +14,6 @@ export default class OutputMessage extends UiMessage {
|
|
|
14
14
|
CRITICAL: 3,
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** @type {string[]} */
|
|
18
|
-
body
|
|
19
|
-
/** @type {Object} */
|
|
20
|
-
meta = {}
|
|
21
|
-
/** @type {Error|null} */
|
|
22
|
-
error = null
|
|
23
|
-
/** @type {number} */
|
|
24
|
-
priority = OutputMessage.PRIORITY.NORMAL
|
|
25
|
-
|
|
26
17
|
/**
|
|
27
18
|
* Creates an OutputMessage.
|
|
28
19
|
*
|
|
@@ -41,14 +32,18 @@ export default class OutputMessage extends UiMessage {
|
|
|
41
32
|
|
|
42
33
|
const contentSource = 'body' in input ? body : 'content' in input ? content : []
|
|
43
34
|
|
|
35
|
+
/** @type {string[]} */
|
|
44
36
|
this.body = Array.isArray(contentSource)
|
|
45
37
|
? contentSource
|
|
46
38
|
: contentSource
|
|
47
39
|
? [String(contentSource)]
|
|
48
40
|
: []
|
|
49
41
|
|
|
42
|
+
/** @type {Object} */
|
|
50
43
|
this.meta = meta
|
|
44
|
+
/** @type {Error|null} */
|
|
51
45
|
this.error = error instanceof Error ? error : error ? new Error(String(error)) : null
|
|
46
|
+
/** @type {number} */
|
|
52
47
|
this.priority = Number(priority)
|
|
53
48
|
|
|
54
49
|
if (!this.type && this.error) {
|
package/src/core/Stream.js
CHANGED
|
@@ -44,19 +44,30 @@ export default class UIStream {
|
|
|
44
44
|
static async process(signal, generatorFn, onProgress, onError, onComplete) {
|
|
45
45
|
const iter = generatorFn()
|
|
46
46
|
|
|
47
|
+
/** @type {Promise<never>} */
|
|
48
|
+
const abortPromise = new Promise((_, reject) => {
|
|
49
|
+
const onAbort = () => reject(new DOMException('The operation was aborted', 'AbortError'))
|
|
50
|
+
if (signal.aborted) return onAbort()
|
|
51
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
47
54
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
while (true) {
|
|
56
|
+
const { value: item, done } = await Promise.race([iter.next(), abortPromise])
|
|
57
|
+
|
|
58
|
+
if (done) {
|
|
59
|
+
if (item) onComplete?.(item)
|
|
60
|
+
break
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
if (item.done) {
|
|
54
64
|
onComplete?.(item)
|
|
55
65
|
break
|
|
56
|
-
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (item.error) {
|
|
57
69
|
onError?.(item.error, item)
|
|
58
70
|
} else {
|
|
59
|
-
// Intermediate results
|
|
60
71
|
onProgress?.(null, item)
|
|
61
72
|
}
|
|
62
73
|
}
|
package/src/core/StreamEntry.js
CHANGED
|
@@ -2,30 +2,6 @@
|
|
|
2
2
|
* Represents an entry in a stream with value, completion status, cancellation status, and error message.
|
|
3
3
|
*/
|
|
4
4
|
export default class StreamEntry {
|
|
5
|
-
/**
|
|
6
|
-
* The value of the stream entry.
|
|
7
|
-
* @type {any}
|
|
8
|
-
*/
|
|
9
|
-
value = undefined
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Indicates if the stream entry is done (completed).
|
|
13
|
-
* @type {boolean}
|
|
14
|
-
*/
|
|
15
|
-
done = false
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Indicates if the stream entry has been cancelled.
|
|
19
|
-
* @type {boolean}
|
|
20
|
-
*/
|
|
21
|
-
cancelled = false
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Error message associated with the stream entry.
|
|
25
|
-
* @type {string}
|
|
26
|
-
*/
|
|
27
|
-
error = ''
|
|
28
|
-
|
|
29
5
|
/**
|
|
30
6
|
* Creates a new StreamEntry instance.
|
|
31
7
|
* @param {Object} [input={}] - Input object to initialize the stream entry.
|
|
@@ -36,14 +12,30 @@ export default class StreamEntry {
|
|
|
36
12
|
*/
|
|
37
13
|
constructor(input = {}) {
|
|
38
14
|
const {
|
|
39
|
-
value =
|
|
40
|
-
done =
|
|
41
|
-
cancelled =
|
|
42
|
-
error =
|
|
15
|
+
value = undefined,
|
|
16
|
+
done = false,
|
|
17
|
+
cancelled = false,
|
|
18
|
+
error = '',
|
|
43
19
|
} = input
|
|
20
|
+
/**
|
|
21
|
+
* The value of the stream entry.
|
|
22
|
+
* @type {any}
|
|
23
|
+
*/
|
|
44
24
|
this.value = value
|
|
25
|
+
/**
|
|
26
|
+
* Indicates if the stream entry is done (completed).
|
|
27
|
+
* @type {boolean}
|
|
28
|
+
*/
|
|
45
29
|
this.done = Boolean(done)
|
|
30
|
+
/**
|
|
31
|
+
* Indicates if the stream entry has been cancelled.
|
|
32
|
+
* @type {boolean}
|
|
33
|
+
*/
|
|
46
34
|
this.cancelled = Boolean(cancelled)
|
|
35
|
+
/**
|
|
36
|
+
* Error message associated with the stream entry.
|
|
37
|
+
* @type {string}
|
|
38
|
+
*/
|
|
47
39
|
this.error = String(error)
|
|
48
40
|
}
|
|
49
41
|
|
package/src/core/index.js
CHANGED
|
@@ -41,8 +41,9 @@ export {
|
|
|
41
41
|
export { default as Flow } from './Flow.js'
|
|
42
42
|
|
|
43
43
|
// OLMUI Generator Engine — Intent-based Model→Adapter contract
|
|
44
|
-
export { validateIntent, ask, progress, log, result, INTENT_TYPES, isModelSchema } from './Intent.js'
|
|
44
|
+
export { validateIntent, ask, progress, log, render, result, INTENT_TYPES, isModelSchema } from './Intent.js'
|
|
45
45
|
export { IntentErrorModel } from './IntentErrorModel.js'
|
|
46
46
|
export { runGenerator } from './GeneratorRunner.js'
|
|
47
47
|
|
|
48
48
|
export { MaskHandler } from './MaskHandler.js'
|
|
49
|
+
export { LayoutModel } from '../domain/LayoutModel.js'
|