@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.
Files changed (108) hide show
  1. package/README.md +97 -12
  2. package/package.json +54 -25
  3. package/src/App/Command/DepsCommand.js +3 -4
  4. package/src/Frame/Props.js +12 -18
  5. package/src/InterfaceTemplate/InterfaceTemplate.js +9 -7
  6. package/src/Model/index.js +86 -2
  7. package/src/StdIn.js +2 -6
  8. package/src/cli.js +1 -0
  9. package/src/core/Form/Form.js +8 -7
  10. package/src/core/Form/Message.js +1 -1
  11. package/src/core/GeneratorRunner.js +77 -7
  12. package/src/core/InputAdapter.js +3 -1
  13. package/src/core/Intent.js +214 -16
  14. package/src/core/IntentErrorModel.js +6 -1
  15. package/src/core/Message/Message.js +4 -7
  16. package/src/core/Message/OutputMessage.js +4 -9
  17. package/src/core/Stream.js +16 -5
  18. package/src/core/StreamEntry.js +20 -28
  19. package/src/core/index.js +2 -1
  20. package/src/domain/Content.js +196 -0
  21. package/src/domain/Document.js +17 -0
  22. package/src/domain/FooterModel.js +37 -19
  23. package/src/domain/HeaderModel.js +47 -21
  24. package/src/domain/HeroModel.js +24 -22
  25. package/src/domain/LayoutModel.js +43 -0
  26. package/src/domain/ModelAsApp.js +46 -0
  27. package/src/domain/SandboxModel.js +19 -16
  28. package/src/domain/app/GalleryCommand.js +53 -0
  29. package/src/domain/app/GalleryRenderIntent.js +77 -0
  30. package/src/domain/app/SnapshotAuditor.js +401 -0
  31. package/src/domain/app/SnapshotRunner.js +264 -0
  32. package/src/domain/app/UIApp.js +78 -0
  33. package/src/domain/components/BreadcrumbModel.js +10 -6
  34. package/src/domain/components/FeatureGridModel.js +62 -0
  35. package/src/domain/components/MarkdownModel.js +24 -0
  36. package/src/domain/components/ShellModel.js +243 -0
  37. package/src/domain/components/TableModel.js +10 -6
  38. package/src/domain/components/ToastModel.js +10 -6
  39. package/src/domain/components/index.js +3 -1
  40. package/src/domain/index.js +14 -4
  41. package/src/index.js +21 -2
  42. package/src/inspect.js +2 -0
  43. package/src/test/ScenarioAdapter.js +59 -0
  44. package/src/test/ScenarioTest.js +51 -0
  45. package/src/test/ScenarioTest.story.js +56 -0
  46. package/src/testing/CrashReporter.js +56 -0
  47. package/src/testing/GalleryGenerator.js +29 -0
  48. package/src/testing/LogicInspector.js +55 -0
  49. package/src/testing/SnapshotRunner.js +22 -0
  50. package/src/testing/SpecAdapter.js +115 -0
  51. package/src/testing/SpecRunner.js +121 -0
  52. package/src/testing/VisualAdapter.js +46 -0
  53. package/src/testing/index.js +7 -0
  54. package/src/testing/verifySnapshot.js +17 -0
  55. package/types/App/Command/DepsCommand.d.ts +0 -2
  56. package/types/Model/index.d.ts +56 -4
  57. package/types/StdIn.d.ts +3 -3
  58. package/types/cli.d.ts +1 -0
  59. package/types/core/Form/Form.d.ts +2 -2
  60. package/types/core/GeneratorRunner.d.ts +18 -1
  61. package/types/core/InputAdapter.d.ts +2 -1
  62. package/types/core/Intent.d.ts +232 -26
  63. package/types/core/IntentErrorModel.d.ts +4 -0
  64. package/types/core/Message/Message.d.ts +2 -2
  65. package/types/core/Message/OutputMessage.d.ts +0 -2
  66. package/types/core/index.d.ts +2 -1
  67. package/types/domain/Content.d.ts +340 -0
  68. package/types/domain/Document.d.ts +21 -0
  69. package/types/domain/FooterModel.d.ts +22 -12
  70. package/types/domain/HeaderModel.d.ts +36 -13
  71. package/types/domain/HeroModel.d.ts +19 -17
  72. package/types/domain/LayoutModel.d.ts +34 -0
  73. package/types/domain/ModelAsApp.d.ts +23 -0
  74. package/types/domain/SandboxModel.d.ts +10 -0
  75. package/types/domain/app/GalleryCommand.d.ts +55 -0
  76. package/types/domain/app/GalleryRenderIntent.d.ts +31 -0
  77. package/types/domain/app/SnapshotAuditor.d.ts +99 -0
  78. package/types/domain/app/SnapshotRunner.d.ts +45 -0
  79. package/types/domain/app/UIApp.d.ts +60 -0
  80. package/types/domain/components/BreadcrumbModel.d.ts +6 -8
  81. package/types/domain/components/FeatureGridModel.d.ts +50 -0
  82. package/types/domain/components/MarkdownModel.d.ts +19 -0
  83. package/types/domain/components/ShellModel.d.ts +56 -0
  84. package/types/domain/components/TableModel.d.ts +4 -0
  85. package/types/domain/components/ToastModel.d.ts +4 -0
  86. package/types/domain/components/index.d.ts +3 -0
  87. package/types/domain/index.d.ts +10 -4
  88. package/types/index.d.ts +19 -1
  89. package/types/inspect.d.ts +2 -0
  90. package/types/test/ScenarioAdapter.d.ts +43 -0
  91. package/types/test/ScenarioTest.d.ts +24 -0
  92. package/types/test/ScenarioTest.story.d.ts +1 -0
  93. package/types/testing/CrashReporter.d.ts +13 -0
  94. package/types/testing/GalleryGenerator.d.ts +1 -0
  95. package/types/testing/LogicInspector.d.ts +22 -0
  96. package/types/testing/SnapshotRunner.d.ts +7 -0
  97. package/types/testing/SpecAdapter.d.ts +57 -0
  98. package/types/testing/SpecRunner.d.ts +41 -0
  99. package/types/testing/VisualAdapter.d.ts +9 -0
  100. package/types/testing/index.d.ts +7 -0
  101. package/types/testing/verifySnapshot.d.ts +14 -0
  102. package/src/README.md.js +0 -436
  103. package/types/App/Command/Options.d.ts +0 -43
  104. package/types/App/Command/index.d.ts +0 -8
  105. package/types/App/User/Command/Options.d.ts +0 -34
  106. package/types/core/Message/InputMessage.d.ts +0 -71
  107. package/types/domain/components/HeroModel.d.ts +0 -24
  108. 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 (defaults to no-op).
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({ type: 'result', data: null })
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 (the Judge again)
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
  }
@@ -8,7 +8,7 @@ import UiMessage from './Message/Message.js'
8
8
  * @class InputAdapter
9
9
  * @extends Event
10
10
  */
11
- export default class InputAdapter extends Event {
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
@@ -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 {string} [id] - Progress ID for tracking multiple parallel operations.
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
- * Model emits a log message. No response expected.
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 {'info' | 'warn' | 'error' | 'success'} level
51
- * @property {string} message - Log message from Model (i18n static field value).
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} Intent
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.keys(schema).some((key) => {
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 (!isModel && (!intent.schema || typeof intent.schema !== 'object' || !('help' in intent.schema))) {
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 isComponentLog = intent.type === 'log' && intent.component
142
- if (!isComponentLog && typeof intent.message !== 'string') {
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 const ask = (field, schema) => {
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
- export const progress = (message) => ({ type: 'progress', message })
168
- export const log = (level, message, data = {}) => ({ type: 'log', level, message, ...data })
169
- export const result = (data) => ({ type: 'result', data })
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: '\'{type}\' intent requires a "message" string',
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 = this.type, id = this.id } = input
54
+ const { type, id } = input
55
+ /** @type {string} */
60
56
  this.id = id || `ui-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
61
- this.type = String(type)
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) {
@@ -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
- for await (const item of iter) {
49
- if (signal.aborted) {
50
- throw new DOMException('Aborted', 'AbortError')
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
- } else if (item.error) {
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
  }
@@ -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 = this.value,
40
- done = this.done,
41
- cancelled = this.cancelled,
42
- error = this.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'