@kapeta/local-cluster-service 0.53.5 → 0.54.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/.eslintrc.cjs CHANGED
@@ -6,6 +6,7 @@ module.exports = {
6
6
  rules: {
7
7
  '@typescript-eslint/no-explicit-any': 'off',
8
8
  '@typescript-eslint/no-non-null-assertion': 'off',
9
+ '@typescript-eslint/no-use-before-define': 'off',
9
10
  '@typescript-eslint/no-unsafe-assignment': 'off',
10
11
  '@typescript-eslint/no-unsafe-member-access': 'off',
11
12
  '@typescript-eslint/no-unsafe-return': 'off',
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.54.0](https://github.com/kapetacom/local-cluster-service/compare/v0.53.5...v0.54.0) (2024-06-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * add support for split UI routes + screen context in AI service ([#178](https://github.com/kapetacom/local-cluster-service/issues/178)) ([4330a72](https://github.com/kapetacom/local-cluster-service/commit/4330a72eddb5fe82f9e7a9c2af412d47091cf9b0))
7
+
1
8
  ## [0.53.5](https://github.com/kapetacom/local-cluster-service/compare/v0.53.4...v0.53.5) (2024-06-17)
2
9
 
3
10
 
@@ -44,7 +44,6 @@ const path_2 = __importStar(require("path"));
44
44
  const node_os_1 = __importDefault(require("node:os"));
45
45
  const fs_1 = require("fs");
46
46
  const yaml_1 = __importDefault(require("yaml"));
47
- const fs = __importStar(require("node:fs"));
48
47
  const SIMULATED_DELAY = 1000;
49
48
  const ENABLE_SIMULATED_DELAY = false;
50
49
  class SimulatedFileDelay {
@@ -239,8 +238,11 @@ class StormCodegen {
239
238
  return;
240
239
  }
241
240
  const blockUri = (0, nodejs_utils_1.parseKapetaUri)(block.uri);
242
- const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
241
+ const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE &&
242
+ file.type !== codegen_1.AIFileTypes.WEB_SCREEN &&
243
+ file.type !== codegen_1.AIFileTypes.WEB_ROUTER);
243
244
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
245
+ const webRouters = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_ROUTER);
244
246
  const screenFiles = [];
245
247
  let filteredEvents = [];
246
248
  for (const event of this.events) {
@@ -249,14 +251,55 @@ class StormCodegen {
249
251
  filteredEvents = [];
250
252
  }
251
253
  }
252
- if (uiTemplates.length > 0) {
253
- const uiStream = await stormClient_1.stormClient.createUIImplementation({
254
+ const screenEvents = [];
255
+ // generate screens
256
+ const screenStream = await stormClient_1.stormClient.listScreens({
257
+ events: filteredEvents,
258
+ templates: uiTemplates,
259
+ context: relevantFiles,
260
+ blockName: block.aiName,
261
+ prompt: this.userPrompt,
262
+ });
263
+ screenStream.on('data', (evt) => {
264
+ if (evt.type === 'SCREEN') {
265
+ screenEvents.push(evt);
266
+ }
267
+ this.handleUiOutput(blockUri, block.aiName, evt);
268
+ });
269
+ this.out.on('aborted', () => {
270
+ screenStream.abort();
271
+ });
272
+ await screenStream.waitForDone();
273
+ // screenfiles
274
+ const screenTemplates = screenEvents
275
+ .map((screenEvent) => ({
276
+ ...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
277
+ filename: screenEvent.payload.filename,
278
+ }))
279
+ .filter((tpl) => !!tpl.content);
280
+ await Promise.all(screenTemplates.concat(webRouters).map(async (template) => {
281
+ const payload = {
254
282
  events: filteredEvents,
255
- templates: uiTemplates,
256
- context: relevantFiles,
257
283
  blockName: block.aiName,
284
+ filename: template.filename,
285
+ template: template,
286
+ context: relevantFiles.concat([
287
+ {
288
+ type: codegen_1.AIFileTypes.INSTRUCTIONS,
289
+ mode: codegen_1.MODE_CREATE_ONLY,
290
+ permissions: '0644',
291
+ filename: '<screens>.md',
292
+ content: `
293
+ # Generated screens
294
+
295
+ ${JSON.stringify({ screenEvents })}
296
+
297
+ `,
298
+ },
299
+ ]),
258
300
  prompt: this.userPrompt,
259
- });
301
+ };
302
+ const uiStream = await stormClient_1.stormClient.createUIImplementation(payload);
260
303
  uiStream.on('data', (evt) => {
261
304
  const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
262
305
  if (uiFile != undefined) {
@@ -267,21 +310,34 @@ class StormCodegen {
267
310
  uiStream.abort();
268
311
  });
269
312
  await uiStream.waitForDone();
270
- }
313
+ }));
271
314
  if (this.isAborted()) {
272
315
  return;
273
316
  }
317
+ const basePath = this.getBasePath(block.content.metadata.name);
318
+ const screenFilesConverted = screenFiles.map((screenFile) => {
319
+ return {
320
+ filename: screenFile.payload.filename,
321
+ content: screenFile.payload.content,
322
+ mode: codegen_1.MODE_CREATE_ONLY,
323
+ permissions: '0644',
324
+ type: codegen_1.AIFileTypes.WEB_SCREEN,
325
+ };
326
+ });
274
327
  // Gather the context files for implementation. These will be all be passed to the AI
275
- const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
328
+ const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN, codegen_1.AIFileTypes.WEB_ROUTER].includes(file.type));
276
329
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
277
330
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
278
331
  if (serviceFiles.length > 0) {
279
332
  await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
280
333
  }
281
- const basePath = this.getBasePath(block.content.metadata.name);
282
334
  if (this.isAborted()) {
283
335
  return;
284
336
  }
337
+ for (const screenFile of screenFilesConverted) {
338
+ const filePath = (0, path_2.join)(basePath, screenFile.filename);
339
+ await (0, promises_1.writeFile)(filePath, screenFile.content);
340
+ }
285
341
  for (const serviceFile of serviceFiles) {
286
342
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
287
343
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
@@ -296,16 +352,6 @@ class StormCodegen {
296
352
  const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
297
353
  await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
298
354
  }
299
- const screenFilesConverted = screenFiles.map((screenFile) => {
300
- return {
301
- filename: screenFile.payload.filename,
302
- content: screenFile.payload.content,
303
- mode: codegen_1.MODE_CREATE_ONLY,
304
- permissions: '0644',
305
- type: codegen_1.AIFileTypes.WEB_SCREEN,
306
- };
307
- });
308
- allFiles.push(...screenFilesConverted);
309
355
  const blockRef = block.uri;
310
356
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
311
357
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
@@ -466,7 +512,7 @@ class StormCodegen {
466
512
  const files = new Set(filesForContext);
467
513
  files.add(filename);
468
514
  const requestedFiles = Array.from(files).flatMap((file) => {
469
- if (fs.existsSync(file)) {
515
+ if ((0, fs_1.existsSync)(file)) {
470
516
  return file;
471
517
  }
472
518
  // file does not exist - look for similar
@@ -551,7 +597,7 @@ class StormCodegen {
551
597
  // They will need to be implemented by the AI
552
598
  return;
553
599
  }
554
- if (file.type === codegen_1.AIFileTypes.WEB_SCREEN) {
600
+ if ([codegen_1.AIFileTypes.WEB_ROUTER, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type)) {
555
601
  // Don't send the web screen files to the stream yet
556
602
  // They will need to be implemented by the AI
557
603
  return;
@@ -159,6 +159,7 @@ export interface StormEventScreen {
159
159
  template: string;
160
160
  description: string;
161
161
  url: string;
162
+ filename: string;
162
163
  };
163
164
  }
164
165
  export interface StormEventScreenCandidate {
@@ -1,4 +1,4 @@
1
- import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
1
+ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  declare class StormClient {
@@ -7,6 +7,7 @@ declare class StormClient {
7
7
  private createOptions;
8
8
  private send;
9
9
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
10
+ listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
10
11
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
11
12
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
13
  createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
@@ -80,6 +80,12 @@ class StormClient {
80
80
  conversationId,
81
81
  });
82
82
  }
83
+ listScreens(prompt, conversationId) {
84
+ return this.send('/v2/ui/list', {
85
+ prompt,
86
+ conversationId,
87
+ });
88
+ }
83
89
  createUIImplementation(prompt, conversationId) {
84
90
  return this.send('/v2/ui/merge', {
85
91
  prompt,
@@ -50,6 +50,14 @@ export interface StormFileImplementationPrompt {
50
50
  prompt: string;
51
51
  }
52
52
  export interface StormUIImplementationPrompt {
53
+ events: StormEvent[];
54
+ template: StormFileInfo;
55
+ filename: string;
56
+ context: StormFileInfo[];
57
+ blockName: string;
58
+ prompt: string;
59
+ }
60
+ export interface StormUIListPrompt {
53
61
  events: StormEvent[];
54
62
  templates: StormFileInfo[];
55
63
  context: StormFileInfo[];
@@ -44,7 +44,6 @@ const path_2 = __importStar(require("path"));
44
44
  const node_os_1 = __importDefault(require("node:os"));
45
45
  const fs_1 = require("fs");
46
46
  const yaml_1 = __importDefault(require("yaml"));
47
- const fs = __importStar(require("node:fs"));
48
47
  const SIMULATED_DELAY = 1000;
49
48
  const ENABLE_SIMULATED_DELAY = false;
50
49
  class SimulatedFileDelay {
@@ -239,8 +238,11 @@ class StormCodegen {
239
238
  return;
240
239
  }
241
240
  const blockUri = (0, nodejs_utils_1.parseKapetaUri)(block.uri);
242
- const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
241
+ const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE &&
242
+ file.type !== codegen_1.AIFileTypes.WEB_SCREEN &&
243
+ file.type !== codegen_1.AIFileTypes.WEB_ROUTER);
243
244
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
245
+ const webRouters = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_ROUTER);
244
246
  const screenFiles = [];
245
247
  let filteredEvents = [];
246
248
  for (const event of this.events) {
@@ -249,14 +251,55 @@ class StormCodegen {
249
251
  filteredEvents = [];
250
252
  }
251
253
  }
252
- if (uiTemplates.length > 0) {
253
- const uiStream = await stormClient_1.stormClient.createUIImplementation({
254
+ const screenEvents = [];
255
+ // generate screens
256
+ const screenStream = await stormClient_1.stormClient.listScreens({
257
+ events: filteredEvents,
258
+ templates: uiTemplates,
259
+ context: relevantFiles,
260
+ blockName: block.aiName,
261
+ prompt: this.userPrompt,
262
+ });
263
+ screenStream.on('data', (evt) => {
264
+ if (evt.type === 'SCREEN') {
265
+ screenEvents.push(evt);
266
+ }
267
+ this.handleUiOutput(blockUri, block.aiName, evt);
268
+ });
269
+ this.out.on('aborted', () => {
270
+ screenStream.abort();
271
+ });
272
+ await screenStream.waitForDone();
273
+ // screenfiles
274
+ const screenTemplates = screenEvents
275
+ .map((screenEvent) => ({
276
+ ...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
277
+ filename: screenEvent.payload.filename,
278
+ }))
279
+ .filter((tpl) => !!tpl.content);
280
+ await Promise.all(screenTemplates.concat(webRouters).map(async (template) => {
281
+ const payload = {
254
282
  events: filteredEvents,
255
- templates: uiTemplates,
256
- context: relevantFiles,
257
283
  blockName: block.aiName,
284
+ filename: template.filename,
285
+ template: template,
286
+ context: relevantFiles.concat([
287
+ {
288
+ type: codegen_1.AIFileTypes.INSTRUCTIONS,
289
+ mode: codegen_1.MODE_CREATE_ONLY,
290
+ permissions: '0644',
291
+ filename: '<screens>.md',
292
+ content: `
293
+ # Generated screens
294
+
295
+ ${JSON.stringify({ screenEvents })}
296
+
297
+ `,
298
+ },
299
+ ]),
258
300
  prompt: this.userPrompt,
259
- });
301
+ };
302
+ const uiStream = await stormClient_1.stormClient.createUIImplementation(payload);
260
303
  uiStream.on('data', (evt) => {
261
304
  const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
262
305
  if (uiFile != undefined) {
@@ -267,21 +310,34 @@ class StormCodegen {
267
310
  uiStream.abort();
268
311
  });
269
312
  await uiStream.waitForDone();
270
- }
313
+ }));
271
314
  if (this.isAborted()) {
272
315
  return;
273
316
  }
317
+ const basePath = this.getBasePath(block.content.metadata.name);
318
+ const screenFilesConverted = screenFiles.map((screenFile) => {
319
+ return {
320
+ filename: screenFile.payload.filename,
321
+ content: screenFile.payload.content,
322
+ mode: codegen_1.MODE_CREATE_ONLY,
323
+ permissions: '0644',
324
+ type: codegen_1.AIFileTypes.WEB_SCREEN,
325
+ };
326
+ });
274
327
  // Gather the context files for implementation. These will be all be passed to the AI
275
- const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
328
+ const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN, codegen_1.AIFileTypes.WEB_ROUTER].includes(file.type));
276
329
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
277
330
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
278
331
  if (serviceFiles.length > 0) {
279
332
  await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
280
333
  }
281
- const basePath = this.getBasePath(block.content.metadata.name);
282
334
  if (this.isAborted()) {
283
335
  return;
284
336
  }
337
+ for (const screenFile of screenFilesConverted) {
338
+ const filePath = (0, path_2.join)(basePath, screenFile.filename);
339
+ await (0, promises_1.writeFile)(filePath, screenFile.content);
340
+ }
285
341
  for (const serviceFile of serviceFiles) {
286
342
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
287
343
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
@@ -296,16 +352,6 @@ class StormCodegen {
296
352
  const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
297
353
  await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
298
354
  }
299
- const screenFilesConverted = screenFiles.map((screenFile) => {
300
- return {
301
- filename: screenFile.payload.filename,
302
- content: screenFile.payload.content,
303
- mode: codegen_1.MODE_CREATE_ONLY,
304
- permissions: '0644',
305
- type: codegen_1.AIFileTypes.WEB_SCREEN,
306
- };
307
- });
308
- allFiles.push(...screenFilesConverted);
309
355
  const blockRef = block.uri;
310
356
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
311
357
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
@@ -466,7 +512,7 @@ class StormCodegen {
466
512
  const files = new Set(filesForContext);
467
513
  files.add(filename);
468
514
  const requestedFiles = Array.from(files).flatMap((file) => {
469
- if (fs.existsSync(file)) {
515
+ if ((0, fs_1.existsSync)(file)) {
470
516
  return file;
471
517
  }
472
518
  // file does not exist - look for similar
@@ -551,7 +597,7 @@ class StormCodegen {
551
597
  // They will need to be implemented by the AI
552
598
  return;
553
599
  }
554
- if (file.type === codegen_1.AIFileTypes.WEB_SCREEN) {
600
+ if ([codegen_1.AIFileTypes.WEB_ROUTER, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type)) {
555
601
  // Don't send the web screen files to the stream yet
556
602
  // They will need to be implemented by the AI
557
603
  return;
@@ -159,6 +159,7 @@ export interface StormEventScreen {
159
159
  template: string;
160
160
  description: string;
161
161
  url: string;
162
+ filename: string;
162
163
  };
163
164
  }
164
165
  export interface StormEventScreenCandidate {
@@ -1,4 +1,4 @@
1
- import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
1
+ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  declare class StormClient {
@@ -7,6 +7,7 @@ declare class StormClient {
7
7
  private createOptions;
8
8
  private send;
9
9
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
10
+ listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
10
11
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
11
12
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
13
  createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
@@ -80,6 +80,12 @@ class StormClient {
80
80
  conversationId,
81
81
  });
82
82
  }
83
+ listScreens(prompt, conversationId) {
84
+ return this.send('/v2/ui/list', {
85
+ prompt,
86
+ conversationId,
87
+ });
88
+ }
83
89
  createUIImplementation(prompt, conversationId) {
84
90
  return this.send('/v2/ui/merge', {
85
91
  prompt,
@@ -50,6 +50,14 @@ export interface StormFileImplementationPrompt {
50
50
  prompt: string;
51
51
  }
52
52
  export interface StormUIImplementationPrompt {
53
+ events: StormEvent[];
54
+ template: StormFileInfo;
55
+ filename: string;
56
+ context: StormFileInfo[];
57
+ blockName: string;
58
+ prompt: string;
59
+ }
60
+ export interface StormUIListPrompt {
53
61
  events: StormEvent[];
54
62
  templates: StormFileInfo[];
55
63
  context: StormFileInfo[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.53.5",
3
+ "version": "0.54.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "homepage": "https://github.com/kapetacom/local-cluster-service#readme",
52
52
  "dependencies": {
53
- "@kapeta/codegen": "^1.6.0",
53
+ "@kapeta/codegen": "^1.6.1",
54
54
  "@kapeta/config-mapper": "^1.2.2",
55
55
  "@kapeta/kaplang-core": "^1.17.2",
56
56
  "@kapeta/local-cluster-config": "^0.4.2",
@@ -13,6 +13,7 @@ import {
13
13
  GeneratedResult,
14
14
  MODE_CREATE_ONLY,
15
15
  } from '@kapeta/codegen';
16
+
16
17
  import { BlockDefinition } from '@kapeta/schemas';
17
18
  import { codeGeneratorManager } from '../codeGeneratorManager';
18
19
  import { STORM_ID, stormClient } from './stormClient';
@@ -23,6 +24,7 @@ import {
23
24
  StormEventFileChunk,
24
25
  StormEventFileDone,
25
26
  StormEventFileLogical,
27
+ StormEventScreen,
26
28
  } from './events';
27
29
  import { BlockDefinitionInfo, StormEventParser } from './event-parser';
28
30
  import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
@@ -31,11 +33,14 @@ import { writeFile } from 'fs/promises';
31
33
  import path from 'path';
32
34
  import Path, { join } from 'path';
33
35
  import os from 'node:os';
34
- import { readFileSync, writeFileSync } from 'fs';
36
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
35
37
  import YAML from 'yaml';
36
- import * as fs from 'node:fs';
38
+ import assert from 'assert';
37
39
 
38
- type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
40
+ type ImplementationGenerator<T = StormFileImplementationPrompt> = (
41
+ prompt: T,
42
+ conversationId?: string
43
+ ) => Promise<StormStream>;
39
44
 
40
45
  interface ErrorClassification {
41
46
  error: string;
@@ -266,9 +271,13 @@ export class StormCodegen {
266
271
  const blockUri = parseKapetaUri(block.uri);
267
272
 
268
273
  const relevantFiles: StormFileInfo[] = allFiles.filter(
269
- (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
274
+ (file) =>
275
+ file.type !== AIFileTypes.IGNORE &&
276
+ file.type !== AIFileTypes.WEB_SCREEN &&
277
+ file.type !== AIFileTypes.WEB_ROUTER
270
278
  );
271
- const uiTemplates: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.WEB_SCREEN);
279
+ const uiTemplates = allFiles.filter((file) => file.type === AIFileTypes.WEB_SCREEN);
280
+ const webRouters = allFiles.filter((file) => file.type === AIFileTypes.WEB_ROUTER);
272
281
  const screenFiles: StormEventFileDone[] = [];
273
282
  let filteredEvents = [] as StormEvent[];
274
283
  for (const event of this.events) {
@@ -277,36 +286,96 @@ export class StormCodegen {
277
286
  filteredEvents = [];
278
287
  }
279
288
  }
280
- if (uiTemplates.length > 0) {
281
- const uiStream = await stormClient.createUIImplementation({
282
- events: filteredEvents,
283
- templates: uiTemplates,
284
- context: relevantFiles,
285
- blockName: block.aiName,
286
- prompt: this.userPrompt,
287
- });
288
289
 
289
- uiStream.on('data', (evt) => {
290
- const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
291
- if (uiFile != undefined) {
292
- screenFiles.push(uiFile);
293
- }
294
- });
290
+ const screenEvents: StormEventScreen[] = [];
291
+ // generate screens
292
+ const screenStream = await stormClient.listScreens({
293
+ events: filteredEvents,
294
+ templates: uiTemplates,
295
+ context: relevantFiles,
296
+ blockName: block.aiName,
297
+ prompt: this.userPrompt,
298
+ });
299
+ screenStream.on('data', (evt) => {
300
+ if (evt.type === 'SCREEN') {
301
+ screenEvents.push(evt);
302
+ }
303
+ this.handleUiOutput(blockUri, block.aiName, evt);
304
+ });
295
305
 
296
- this.out.on('aborted', () => {
297
- uiStream.abort();
298
- });
306
+ this.out.on('aborted', () => {
307
+ screenStream.abort();
308
+ });
299
309
 
300
- await uiStream.waitForDone();
301
- }
310
+ await screenStream.waitForDone();
311
+
312
+ // screenfiles
313
+ const screenTemplates = screenEvents
314
+ .map((screenEvent) => ({
315
+ ...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
316
+ filename: screenEvent.payload.filename,
317
+ }))
318
+ .filter((tpl): tpl is StormFileInfo => !!tpl.content);
319
+
320
+ await Promise.all(
321
+ screenTemplates.concat(webRouters).map(async (template) => {
322
+ const payload = {
323
+ events: filteredEvents,
324
+ blockName: block.aiName,
325
+ filename: template.filename,
326
+ template: template,
327
+ context: relevantFiles.concat([
328
+ {
329
+ type: AIFileTypes.INSTRUCTIONS,
330
+ mode: MODE_CREATE_ONLY,
331
+ permissions: '0644',
332
+ filename: '<screens>.md',
333
+ content: `
334
+ # Generated screens
335
+
336
+ ${JSON.stringify({ screenEvents })}
337
+
338
+ `,
339
+ },
340
+ ]),
341
+ prompt: this.userPrompt,
342
+ };
343
+
344
+ const uiStream = await stormClient.createUIImplementation(payload);
345
+
346
+ uiStream.on('data', (evt) => {
347
+ const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
348
+ if (uiFile != undefined) {
349
+ screenFiles.push(uiFile);
350
+ }
351
+ });
352
+
353
+ this.out.on('aborted', () => {
354
+ uiStream.abort();
355
+ });
356
+
357
+ await uiStream.waitForDone();
358
+ })
359
+ );
302
360
 
303
361
  if (this.isAborted()) {
304
362
  return;
305
363
  }
364
+ const basePath = this.getBasePath(block.content.metadata.name);
365
+
366
+ const screenFilesConverted = screenFiles.map((screenFile) => {
367
+ return {
368
+ filename: screenFile.payload.filename,
369
+ content: screenFile.payload.content,
370
+ mode: MODE_CREATE_ONLY,
371
+ permissions: '0644',
372
+ type: AIFileTypes.WEB_SCREEN,
373
+ };
374
+ });
306
375
 
307
376
  // Gather the context files for implementation. These will be all be passed to the AI
308
377
  const contextFiles: StormFileInfo[] = relevantFiles.filter(
309
- (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
378
+ (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN, AIFileTypes.WEB_ROUTER].includes(file.type)
310
379
  );
311
380
 
312
381
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
@@ -321,12 +390,15 @@ export class StormCodegen {
321
390
  );
322
391
  }
323
392
 
324
- const basePath = this.getBasePath(block.content.metadata.name);
325
-
326
393
  if (this.isAborted()) {
327
394
  return;
328
395
  }
329
396
 
397
+ for (const screenFile of screenFilesConverted) {
398
+ const filePath = join(basePath, screenFile.filename);
399
+ await writeFile(filePath, screenFile.content);
400
+ }
401
+
330
402
  for (const serviceFile of serviceFiles) {
331
403
  const filePath = join(basePath, serviceFile.filename);
332
404
  await writeFile(filePath, serviceFile.content);
@@ -345,16 +417,6 @@ export class StormCodegen {
345
417
  await writeFile(filePath, screenFile.payload.content);
346
418
  }
347
419
 
348
- const screenFilesConverted = screenFiles.map((screenFile) => {
349
- return {
350
- filename: screenFile.payload.filename,
351
- content: screenFile.payload.content,
352
- mode: MODE_CREATE_ONLY,
353
- permissions: '0644',
354
- type: AIFileTypes.WEB_SCREEN,
355
- };
356
- });
357
- allFiles.push(...screenFilesConverted);
358
420
  const blockRef = block.uri;
359
421
 
360
422
  this.emitBlockStatus(blockUri, block.aiName, StormEventBlockStatusType.QA);
@@ -594,7 +656,7 @@ export class StormCodegen {
594
656
  files.add(filename);
595
657
 
596
658
  const requestedFiles = Array.from(files).flatMap((file) => {
597
- if (fs.existsSync(file)) {
659
+ if (existsSync(file)) {
598
660
  return file;
599
661
  }
600
662
 
@@ -696,7 +758,7 @@ export class StormCodegen {
696
758
  return;
697
759
  }
698
760
 
699
- if (file.type === AIFileTypes.WEB_SCREEN) {
761
+ if ([AIFileTypes.WEB_ROUTER, AIFileTypes.WEB_SCREEN].includes(file.type)) {
700
762
  // Don't send the web screen files to the stream yet
701
763
  // They will need to be implemented by the AI
702
764
  return;
@@ -193,6 +193,7 @@ export interface StormEventScreen {
193
193
  template: string;
194
194
  description: string;
195
195
  url: string;
196
+ filename: string;
196
197
  };
197
198
  }
198
199
 
@@ -12,6 +12,7 @@ import {
12
12
  StormFileImplementationPrompt,
13
13
  StormStream,
14
14
  StormUIImplementationPrompt,
15
+ StormUIListPrompt,
15
16
  } from './stream';
16
17
  import { getRawAsset } from 'node:sea';
17
18
 
@@ -112,8 +113,15 @@ class StormClient {
112
113
  });
113
114
  }
114
115
 
116
+ public listScreens(prompt: StormUIListPrompt, conversationId?: string) {
117
+ return this.send('/v2/ui/list', {
118
+ prompt,
119
+ conversationId,
120
+ });
121
+ }
122
+
115
123
  public createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string) {
116
- return this.send<StormUIImplementationPrompt>('/v2/ui/merge', {
124
+ return this.send('/v2/ui/merge', {
117
125
  prompt,
118
126
  conversationId,
119
127
  });
@@ -117,6 +117,15 @@ export interface StormFileImplementationPrompt {
117
117
  }
118
118
 
119
119
  export interface StormUIImplementationPrompt {
120
+ events: StormEvent[];
121
+ template: StormFileInfo;
122
+ filename: string;
123
+ context: StormFileInfo[];
124
+ blockName: string;
125
+ prompt: string;
126
+ }
127
+
128
+ export interface StormUIListPrompt {
120
129
  events: StormEvent[];
121
130
  templates: StormFileInfo[];
122
131
  context: StormFileInfo[];