@slicemachine/manager 0.25.7-alpha.jp-figma-to-prismic.5 → 0.25.7-alpha.jp-figma-to-prismic.7

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.
@@ -134,6 +134,8 @@ type CustomTypeFieldIdChangedMeta = {
134
134
  type LinkCustomType = NonNullable<LinkConfig["customtypes"]>[number];
135
135
 
136
136
  export class CustomTypesManager extends BaseManager {
137
+ private inferSliceAbortControllers = new Map<string, AbortController>();
138
+
137
139
  async readCustomTypeLibrary(): Promise<SliceMachineManagerReadCustomTypeLibraryReturnType> {
138
140
  assertPluginsInitialized(this.sliceMachinePluginRunner);
139
141
 
@@ -540,339 +542,382 @@ export class CustomTypesManager extends BaseManager {
540
542
  }
541
543
 
542
544
  async inferSlice(
543
- args: { imageUrl: string } & (
545
+ args: { imageUrl: string; requestId?: string } & (
544
546
  | { source: "upload" }
545
547
  | { source: "figma"; libraryID: string }
546
548
  ),
547
549
  ): Promise<InferSliceResponse> {
548
- const { source, imageUrl } = args;
549
-
550
- const exp = await this.telemetry.getExperimentVariant("llm-proxy-access");
551
- if (exp?.value !== "on") {
552
- throw new Error("LLM proxy access is not enabled.");
553
- }
554
- const { llmProxyUrl } = z
555
- .object({ llmProxyUrl: z.string() })
556
- .parse(exp.payload);
550
+ const { source, imageUrl, requestId } = args;
557
551
 
558
552
  const authToken = await this.user.getAuthenticationToken();
559
553
  const repository = await this.project.getResolvedRepositoryName();
560
554
 
561
- if (source === "figma") {
562
- const { libraryID } = args;
563
- // eslint-disable-next-line no-console
564
- console.log(`inferSlice started`);
565
- const startTime = Date.now();
555
+ let abortController: AbortController | undefined;
556
+ if (requestId) {
557
+ abortController = new AbortController();
558
+ abortController.signal.addEventListener("abort", () => {
559
+ console.warn(`inferSlice (${source}) request ${requestId} was aborted`);
560
+ });
561
+ this.inferSliceAbortControllers.set(requestId, abortController);
562
+ }
566
563
 
567
- let tmpDir: string | undefined;
568
- try {
569
- const config = await this.project.getSliceMachineConfig();
570
-
571
- let framework:
572
- | { type: "nextjs" | "nuxt" | "sveltekit"; label: string }
573
- | undefined;
574
- if (config.adapter === "@slicemachine/adapter-next") {
575
- framework = { type: "nextjs", label: "Next.js (React)" };
576
- } else if (
577
- config.adapter === "@slicemachine/adapter-nuxt" ||
578
- config.adapter === "@slicemachine/adapter-nuxt2"
579
- ) {
580
- framework = { type: "nuxt", label: "Nuxt (Vue)" };
581
- } else if (config.adapter === "@slicemachine/adapter-sveltekit") {
582
- framework = { type: "sveltekit", label: "SvelteKit (Svelte)" };
583
- }
564
+ console.info(
565
+ `inferSlice (${source}) started${
566
+ requestId ? ` for request ${requestId}` : ""
567
+ }`,
568
+ );
569
+ const startTime = Date.now();
584
570
 
585
- if (!framework) {
586
- throw new Error(
587
- "Could not determine framework from Slice Machine config.",
588
- );
589
- }
571
+ try {
572
+ if (source === "figma") {
573
+ const { libraryID } = args;
590
574
 
591
- let frameworkFileExtension: string | undefined;
592
- if (framework.type === "nextjs") {
593
- frameworkFileExtension = "tsx";
594
- } else if (framework.type === "nuxt") {
595
- frameworkFileExtension = "vue";
596
- } else if (framework.type === "sveltekit") {
597
- frameworkFileExtension = "svelte";
575
+ const exp =
576
+ await this.telemetry.getExperimentVariant("llm-proxy-access");
577
+ if (exp?.value !== "on") {
578
+ throw new Error("User does not have access to the LLM proxy.");
598
579
  }
599
580
 
600
- if (!frameworkFileExtension) {
601
- throw new Error(
602
- "Could not determine framework from Slice Machine config.",
603
- );
604
- }
581
+ const { llmProxyUrl } = z
582
+ .object({ llmProxyUrl: z.string().url() })
583
+ .parse(exp.payload);
584
+
585
+ let tmpDir: string | undefined;
586
+ try {
587
+ const config = await this.project.getSliceMachineConfig();
588
+
589
+ let framework:
590
+ | { type: "nextjs" | "nuxt" | "sveltekit"; label: string }
591
+ | undefined;
592
+ if (config.adapter === "@slicemachine/adapter-next") {
593
+ framework = { type: "nextjs", label: "Next.js (React)" };
594
+ } else if (
595
+ config.adapter === "@slicemachine/adapter-nuxt" ||
596
+ config.adapter === "@slicemachine/adapter-nuxt2"
597
+ ) {
598
+ framework = { type: "nuxt", label: "Nuxt (Vue)" };
599
+ } else if (config.adapter === "@slicemachine/adapter-sveltekit") {
600
+ framework = { type: "sveltekit", label: "SvelteKit (Svelte)" };
601
+ }
602
+
603
+ if (!framework) {
604
+ throw new Error(
605
+ "Could not determine framework from Slice Machine config.",
606
+ );
607
+ }
605
608
 
606
- const projectRoot = await this.project.getRoot();
607
- const libraryAbsPath = path.join(projectRoot, libraryID);
609
+ let frameworkFileExtension: string | undefined;
610
+ if (framework.type === "nextjs") {
611
+ frameworkFileExtension = "tsx";
612
+ } else if (framework.type === "nuxt") {
613
+ frameworkFileExtension = "vue";
614
+ } else if (framework.type === "sveltekit") {
615
+ frameworkFileExtension = "svelte";
616
+ }
608
617
 
609
- tmpDir = await mkdtemp(
610
- path.join(tmpdir(), "slice-machine-infer-slice-tmp-"),
611
- );
612
- const tmpImagePath = path.join(tmpDir, `${randomUUID()}.png`);
613
- const response = await fetch(imageUrl);
614
- if (!response.ok) {
615
- throw new Error(
616
- `Failed to download image: ${response.status} ${response.statusText}`,
617
- );
618
- }
619
- await writeFile(
620
- tmpImagePath,
621
- Buffer.from(await response.arrayBuffer()),
622
- );
618
+ if (!frameworkFileExtension) {
619
+ throw new Error(
620
+ "Could not determine framework from Slice Machine config.",
621
+ );
622
+ }
623
623
 
624
- const queries = queryClaude({
625
- prompt: `CRITICAL INSTRUCTIONS - READ FIRST:
626
- - You MUST start immediately with Step 1.1. DO NOT read, analyze, or explore any project files first.
627
- - Work step-by-step through the numbered tasks below.
628
- - DO NOT present any summary, explanation, or completion message after finishing.
629
- - DO NOT create TODO lists while performing tasks.
630
- - Keep responses minimal - only show necessary tool calls and brief progress notes.
631
-
632
- # CONTEXT
633
-
634
- The user wants to build a new Prismic Slice based on a design image they provided.
635
- Your goal is to analyze the design image and generate the JSON model data and boilerplate code for the slice following Prismic requirements.
636
-
637
- You will work under the slice library at <slice_library_path>, where all the slices are stored.
638
-
639
- # AVAILABLE RESOURCES
640
-
641
- <design_image_path>
642
- ${tmpImagePath}
643
- </design_image_path>
644
-
645
- <slice_library_path>
646
- ${libraryAbsPath}
647
- </slice_library_path>
648
-
649
- <framework>
650
- ${framework.label}
651
- </framework>
652
-
653
- # AVAILABLE TOOLS
654
-
655
- You have access to specialized Prismic MCP tools for this task:
624
+ const projectRoot = await this.project.getRoot();
625
+ const libraryAbsPath = path.join(projectRoot, libraryID);
656
626
 
657
- <tool name="mcp__prismic__how_to_model_slice">
658
- <description>
659
- Provides detailed guidance on creating Prismic slice models, including field types, naming conventions, and best practices.
660
- </description>
661
- <when_to_use>
662
- Call this tool in Step 2.1 to learn how to structure the slice model data for the design you analysed.
663
- </when_to_use>
664
- </tool>
665
-
666
- <tool name="mcp__prismic__how_to_code_slice">
667
- <description>
668
- Provides guidance on implementing Prismic slice components, including how to use Prismic field components, props structure, and best practices.
669
- </description>
670
- <when_to_use>
671
- Call this tool in Step 2.1 to learn how to properly structure the slice component with Prismic fields.
672
- </when_to_use>
673
- </tool>
674
-
675
- <tool name="mcp__prismic__save_slice_data">
676
- <description>
677
- Validates and saves the slice model data to model.json. This is the ONLY way to create the model file.
678
- </description>
679
- <when_to_use>
680
- Call this tool in Step 2.3 after you have built the complete slice model structure in memory.
681
- </when_to_use>
682
- </tool>
683
-
684
- # TASK REQUIREMENTS
685
-
686
- ## Step 1: Gather information from the design image
687
- 1.1. Analyse the design image at <design_image_path>.
688
- 1.2. Identify all elements in the image that should be dynamically editable (e.g., headings, paragraphs, images, links, buttons, etc.).
689
- 1.3. List the slice directories under <slice_library_path>.
690
- 1.4. Come up with a unique name for the new slice based on the content of the image and the slice directories.
691
-
692
- ## Step 2: Model the Prismic slice
693
- 2.1. Call mcp__prismic__how_to_model_slice to learn how to structure the model for this design.
694
- - Make sure the name you use for the new slice does not yet exist in the slice library at <slice_library_path>. If it does, use a different name.
695
- 2.2. Build the complete slice JSON model data in memory based on the guidance received and the information extracted from the image.
696
- 2.3. Call mcp__prismic__save_slice_data to save the model (DO NOT manually write model.json) in the slice library at <slice_library_path>.
697
-
698
- ## Step 3: Code a boilerplate slice component based on the model
699
- 3.1. Call mcp__prismic__how_to_code_slice to learn how to properly structure the slice component with Prismic fields.
700
- 3.2. Update the slice component code at <slice_library_path>/index.${frameworkFileExtension}, replacing the placeholder code with boilerplate code with the following requirements:
701
- - Must NOT be based on existing slices or components from the codebase.
702
- - Must render all the Prismic components to display the fields of the slice model created at <slice_model_path>.
703
- - Must be a valid ${framework.label} component.
704
- - Must NOT have any styling/CSS. No inlines styles or classNames. Just the skeleton component structure.
705
- - Must NOT use any other custom component or functions from the user's codebase.
706
- - Avoid creating unnecessary wrapper elements, like if they only wrap a single component (e.g., <div><PrismicRichText /></div>).
707
-
708
- ## Step 4: Present the newly created slice path
709
- 4.1. Present the path to the newly created slice in the following format: <new_slice_path>${libraryAbsPath}/MyNewSlice</new_slice_path>.
710
- - "MyNewSlice" must be the name of the directory of the newly created slice.
711
-
712
- # EXAMPLE OF CORRECT EXECUTION
713
-
714
- <example>
715
- Assistant: Step 1.1: Analysing design image...
716
- [reads <design_image_path>]
717
-
718
- Step 1.2: Identifying editable content elements...
719
- [identifies: title field, description field, buttonText field, buttonLink field, backgroundImage field]
720
-
721
- Step 1.3: Listing slice directories under <slice_library_path>...
722
- [lists slice directories: Hero, Hero2, Hero3]
723
-
724
- Step 1.4: Coming up with a unique name for the new slice...
725
- [comes up with a unique name for the new slice: Hero4]
726
-
727
- Step 2.1: Getting Prismic modeling guidance...
728
- [calls mcp__prismic__how_to_model_slice]
729
-
730
- Step 2.2: Building slice model based on guidance and the information extracted...
731
- [creates model with title field, description field, buttonText field, buttonLink field, backgroundImage field]
732
-
733
- Step 2.3: Saving slice model...
734
- [calls mcp__prismic__save_slice_data]
735
-
736
- Step 3.1: Learning Prismic slice coding requirements...
737
- [calls mcp__prismic__how_to_code_slice]
738
-
739
- Step 3.2: Coding boilerplate slice component based on the model...
740
- [updates component with Prismic field components, no styling, no other components]
741
-
742
- Step 4.1: Presenting the path to the newly created slice...
743
- [presents <new_slice_path>${path.join(
744
- libraryAbsPath,
745
- "MyNewSlice",
746
- )}</new_slice_path>]
747
-
748
- # DELIVERABLES
749
- - Slice model saved to <slice_library_path>/model.json using mcp__prismic__save_slice_data
750
- - Slice component at <slice_library_path>/index.${frameworkFileExtension} updated with boilerplate code
751
- - New slice path presented in the format mentioned in Step 3.1
752
-
753
- YOU ARE NOT FINISHED UNTIL YOU HAVE THESE DELIVERABLES.
754
-
755
- ---
756
-
757
- FINAL REMINDERS:
758
- - You MUST use mcp__prismic__save_slice_data to save the model
759
- - You MUST call mcp__prismic__how_to_code_slice in Step 3.1
760
- - DO NOT ATTEMPT TO BUILD THE APPLICATION
761
- - START IMMEDIATELY WITH STEP 1.1 - NO PRELIMINARY ANALYSIS;`,
762
- options: {
763
- cwd: libraryAbsPath,
764
- stderr: (data) => console.error(data),
765
- model: "claude-haiku-4-5",
766
- permissionMode: "bypassPermissions",
767
- allowedTools: [
768
- "Bash",
769
- "Read",
770
- "FileSearch",
771
- "Grep",
772
- "Glob",
773
- "Task",
774
- "Edit",
775
- "Write",
776
- "MultiEdit",
777
- "mcp__prismic__how_to_model_slice",
778
- "mcp__prismic__how_to_code_slice",
779
- "mcp__prismic__save_slice_data",
780
- ],
781
- disallowedTools: [
782
- `Edit(**/model.json)`,
783
- `Write(**/model.json)`,
784
- "Edit(**/mocks.json)",
785
- "Write(**/mocks.json)",
786
- ],
787
- env: {
788
- ...process.env,
789
- ANTHROPIC_BASE_URL: llmProxyUrl,
790
- ANTHROPIC_CUSTOM_HEADERS:
791
- `x-prismic-token: ${authToken}\n` +
792
- `x-prismic-repository: ${repository}\n`,
793
- },
794
- mcpServers: {
795
- prismic: {
796
- type: "stdio",
797
- command: "npx",
798
- args: ["-y", "@prismicio/mcp-server@0.0.20-alpha.6"],
627
+ tmpDir = await mkdtemp(
628
+ path.join(tmpdir(), "slice-machine-infer-slice-tmp-"),
629
+ );
630
+ const tmpImagePath = path.join(tmpDir, `${randomUUID()}.png`);
631
+ const response = await fetch(imageUrl);
632
+ if (!response.ok) {
633
+ throw new Error(
634
+ `Failed to download image: ${response.status} ${response.statusText}`,
635
+ );
636
+ }
637
+ await writeFile(
638
+ tmpImagePath,
639
+ Buffer.from(await response.arrayBuffer()),
640
+ );
641
+
642
+ const queries = queryClaude({
643
+ prompt: `CRITICAL INSTRUCTIONS - READ FIRST:
644
+ - You MUST start immediately with Step 1.1. DO NOT read, analyze, or explore any project files first.
645
+ - Work step-by-step through the numbered tasks below.
646
+ - DO NOT present any summary, explanation, or completion message after finishing.
647
+ - DO NOT create TODO lists while performing tasks.
648
+ - Keep responses minimal - only show necessary tool calls and brief progress notes.
649
+
650
+ # CONTEXT
651
+
652
+ The user wants to build a new Prismic Slice based on a design image they provided.
653
+ Your goal is to analyze the design image and generate the JSON model data and boilerplate code for the slice following Prismic requirements.
654
+
655
+ You will work under the slice library at <slice_library_path>, where all the slices are stored.
656
+
657
+ # AVAILABLE RESOURCES
658
+
659
+ <design_image_path>
660
+ ${tmpImagePath}
661
+ </design_image_path>
662
+
663
+ <slice_library_path>
664
+ ${libraryAbsPath}
665
+ </slice_library_path>
666
+
667
+ <framework>
668
+ ${framework.label}
669
+ </framework>
670
+
671
+ # AVAILABLE TOOLS
672
+
673
+ You have access to specialized Prismic MCP tools for this task:
674
+
675
+ <tool name="mcp__prismic__how_to_model_slice">
676
+ <description>
677
+ Provides detailed guidance on creating Prismic slice models, including field types, naming conventions, and best practices.
678
+ </description>
679
+ <when_to_use>
680
+ Call this tool in Step 2.1 to learn how to structure the slice model data for the design you analysed.
681
+ </when_to_use>
682
+ </tool>
683
+
684
+ <tool name="mcp__prismic__how_to_code_slice">
685
+ <description>
686
+ Provides guidance on implementing Prismic slice components, including how to use Prismic field components, props structure, and best practices.
687
+ </description>
688
+ <when_to_use>
689
+ Call this tool in Step 2.1 to learn how to properly structure the slice component with Prismic fields.
690
+ </when_to_use>
691
+ </tool>
692
+
693
+ <tool name="mcp__prismic__save_slice_data">
694
+ <description>
695
+ Validates and saves the slice model data to model.json. This is the ONLY way to create the model file.
696
+ </description>
697
+ <when_to_use>
698
+ Call this tool in Step 2.3 after you have built the complete slice model structure in memory.
699
+ </when_to_use>
700
+ </tool>
701
+
702
+ # TASK REQUIREMENTS
703
+
704
+ ## Step 1: Gather information from the design image
705
+ 1.1. Analyse the design image at <design_image_path>.
706
+ 1.2. Identify all elements in the image that should be dynamically editable (e.g., headings, paragraphs, images, links, buttons, etc.).
707
+ 1.3. List the slice directories under <slice_library_path>.
708
+ 1.4. Come up with a unique name for the new slice based on the content of the image and the slice directories.
709
+
710
+ ## Step 2: Model the Prismic slice
711
+ 2.1. Call mcp__prismic__how_to_model_slice to learn how to structure the model for this design.
712
+ - Make sure the name you use for the new slice does not yet exist in the slice library at <slice_library_path>. If it does, use a different name.
713
+ 2.2. Build the complete slice JSON model data in memory based on the guidance received and the information extracted from the image.
714
+ 2.3. Call mcp__prismic__save_slice_data to save the model (DO NOT manually write model.json) in the slice library at <slice_library_path>.
715
+
716
+ ## Step 3: Code a boilerplate slice component based on the model
717
+ 3.1. Call mcp__prismic__how_to_code_slice to learn how to properly structure the slice component with Prismic fields.
718
+ 3.2. Update the slice component code at <slice_library_path>/index.${frameworkFileExtension}, replacing the placeholder code with boilerplate code with the following requirements:
719
+ - Must NOT be based on existing slices or components from the codebase.
720
+ - Must render all the Prismic components to display the fields of the slice model created at <slice_model_path>.
721
+ - Must be a valid ${framework.label} component.
722
+ - Must NOT have any styling/CSS. No inlines styles or classNames. Just the skeleton component structure.
723
+ - Must NOT use any other custom component or functions from the user's codebase.
724
+ - Avoid creating unnecessary wrapper elements, like if they only wrap a single component (e.g., <div><PrismicRichText /></div>).
725
+
726
+ ## Step 4: Present the newly created slice path
727
+ 4.1. Present the path to the newly created slice in the following format: <new_slice_path>${libraryAbsPath}/MyNewSlice</new_slice_path>.
728
+ - "MyNewSlice" must be the name of the directory of the newly created slice.
729
+
730
+ # EXAMPLE OF CORRECT EXECUTION
731
+
732
+ <example>
733
+ Assistant: Step 1.1: Analysing design image...
734
+ [reads <design_image_path>]
735
+
736
+ Step 1.2: Identifying editable content elements...
737
+ [identifies: title field, description field, buttonText field, buttonLink field, backgroundImage field]
738
+
739
+ Step 1.3: Listing slice directories under <slice_library_path>...
740
+ [lists slice directories: Hero, Hero2, Hero3]
741
+
742
+ Step 1.4: Coming up with a unique name for the new slice...
743
+ [comes up with a unique name for the new slice: Hero4]
744
+
745
+ Step 2.1: Getting Prismic modeling guidance...
746
+ [calls mcp__prismic__how_to_model_slice]
747
+
748
+ Step 2.2: Building slice model based on guidance and the information extracted...
749
+ [creates model with title field, description field, buttonText field, buttonLink field, backgroundImage field]
750
+
751
+ Step 2.3: Saving slice model...
752
+ [calls mcp__prismic__save_slice_data]
753
+
754
+ Step 3.1: Learning Prismic slice coding requirements...
755
+ [calls mcp__prismic__how_to_code_slice]
756
+
757
+ Step 3.2: Coding boilerplate slice component based on the model...
758
+ [updates component with Prismic field components, no styling, no other components]
759
+
760
+ Step 4.1: Presenting the path to the newly created slice...
761
+ [presents <new_slice_path>${path.join(
762
+ libraryAbsPath,
763
+ "MyNewSlice",
764
+ )}</new_slice_path>]
765
+
766
+ # DELIVERABLES
767
+ - Slice model saved to <slice_library_path>/model.json using mcp__prismic__save_slice_data
768
+ - Slice component at <slice_library_path>/index.${frameworkFileExtension} updated with boilerplate code
769
+ - New slice path presented in the format mentioned in Step 3.1
770
+
771
+ YOU ARE NOT FINISHED UNTIL YOU HAVE THESE DELIVERABLES.
772
+
773
+ ---
774
+
775
+ FINAL REMINDERS:
776
+ - You MUST use mcp__prismic__save_slice_data to save the model
777
+ - You MUST call mcp__prismic__how_to_code_slice in Step 3.1
778
+ - DO NOT ATTEMPT TO BUILD THE APPLICATION
779
+ - START IMMEDIATELY WITH STEP 1.1 - NO PRELIMINARY ANALYSIS;`,
780
+ options: {
781
+ cwd: libraryAbsPath,
782
+ stderr: (data) => console.error("inferSlice error:" + data),
783
+ model: "claude-haiku-4-5",
784
+ permissionMode: "bypassPermissions",
785
+ allowedTools: [
786
+ "Bash",
787
+ "Read",
788
+ "FileSearch",
789
+ "Grep",
790
+ "Glob",
791
+ "Task",
792
+ "Edit",
793
+ "Write",
794
+ "MultiEdit",
795
+ "mcp__prismic__how_to_model_slice",
796
+ "mcp__prismic__how_to_code_slice",
797
+ "mcp__prismic__save_slice_data",
798
+ ],
799
+ disallowedTools: [
800
+ `Edit(**/model.json)`,
801
+ `Write(**/model.json)`,
802
+ "Edit(**/mocks.json)",
803
+ "Write(**/mocks.json)",
804
+ ],
805
+ env: {
806
+ ...process.env,
807
+ ANTHROPIC_BASE_URL: llmProxyUrl,
808
+ ANTHROPIC_CUSTOM_HEADERS:
809
+ `x-prismic-token: ${authToken}\n` +
810
+ `x-prismic-repository: ${repository}\n`,
811
+ },
812
+ mcpServers: {
813
+ prismic: {
814
+ type: "stdio",
815
+ command: "npx",
816
+ args: ["-y", "@prismicio/mcp-server@0.0.20-alpha.6"],
817
+ },
799
818
  },
819
+ ...(abortController && { abortController }),
800
820
  },
801
- },
802
- });
803
-
804
- let newSliceAbsPath: string | undefined;
821
+ });
805
822
 
806
- for await (const query of queries) {
807
- switch (query.type) {
808
- case "result":
809
- if (query.subtype === "success") {
810
- newSliceAbsPath = query.result.match(
811
- /<new_slice_path>(.*)<\/new_slice_path>/s,
812
- )?.[1];
813
- }
814
- break;
823
+ let newSliceAbsPath: string | undefined;
824
+
825
+ for await (const query of queries) {
826
+ switch (query.type) {
827
+ case "result":
828
+ if (query.subtype === "success") {
829
+ newSliceAbsPath = query.result.match(
830
+ /<new_slice_path>(.*)<\/new_slice_path>/s,
831
+ )?.[1];
832
+ }
833
+ break;
834
+ }
815
835
  }
816
- }
817
836
 
818
- if (!newSliceAbsPath) {
819
- throw new Error("Could not find path for the newly created slice.");
820
- }
837
+ if (!newSliceAbsPath) {
838
+ throw new Error("Could not find path for the newly created slice.");
839
+ }
821
840
 
822
- const model = await readFile(
823
- path.join(newSliceAbsPath, "model.json"),
824
- "utf8",
825
- );
841
+ const model = await readFile(
842
+ path.join(newSliceAbsPath, "model.json"),
843
+ "utf8",
844
+ );
826
845
 
827
- if (!model) {
828
- throw new Error("Could not find model for the newly created slice.");
829
- }
846
+ if (!model) {
847
+ throw new Error(
848
+ "Could not find model for the newly created slice.",
849
+ );
850
+ }
830
851
 
831
- // move the screenshot image to the new slice directory
832
- await rename(
833
- tmpImagePath,
834
- path.join(newSliceAbsPath, "screenshot-default.png"),
835
- );
836
- await rm(tmpDir, { recursive: true });
852
+ // move the screenshot image to the new slice directory
853
+ await rename(
854
+ tmpImagePath,
855
+ path.join(newSliceAbsPath, "screenshot-default.png"),
856
+ );
837
857
 
838
- const elapsedTimeSeconds = (Date.now() - startTime) / 1000;
839
- // eslint-disable-next-line no-console
840
- console.log(`inferSlice took ${elapsedTimeSeconds}s`);
858
+ return InferSliceResponse.parse({ slice: JSON.parse(model) });
859
+ } finally {
860
+ if (tmpDir && existsSync(tmpDir)) {
861
+ await rm(tmpDir, { recursive: true });
862
+ }
863
+ }
864
+ } else {
865
+ const searchParams = new URLSearchParams({ repository });
866
+ const url = new URL("./slices/infer", API_ENDPOINTS.CustomTypeService);
867
+ url.search = searchParams.toString();
868
+
869
+ const response = await fetch(url.toString(), {
870
+ method: "POST",
871
+ headers: { Authorization: `Bearer ${authToken}` },
872
+ body: JSON.stringify({ imageUrl }),
873
+ ...(abortController && { signal: abortController.signal }),
874
+ });
841
875
 
842
- return InferSliceResponse.parse({ slice: JSON.parse(model) });
843
- } catch (error) {
844
- if (tmpDir && existsSync(tmpDir)) {
845
- await rm(tmpDir, { recursive: true });
876
+ if (!response.ok) {
877
+ throw new Error(`Failed to infer slice: ${response.statusText}`);
846
878
  }
847
879
 
848
- throw error;
849
- }
850
- } else {
851
- const headers = {
852
- Authorization: `Bearer ${authToken}`,
853
- };
880
+ const json = await response.json();
854
881
 
855
- const searchParams = new URLSearchParams({
856
- repository,
857
- });
882
+ return InferSliceResponse.parse(json);
883
+ }
884
+ } catch (error) {
885
+ console.error(
886
+ `inferSlice (${source}) failed${
887
+ requestId ? ` for request ${requestId}` : ""
888
+ }`,
889
+ error,
890
+ );
858
891
 
859
- const url = new URL("./slices/infer", API_ENDPOINTS.CustomTypeService);
860
- url.search = searchParams.toString();
892
+ throw error;
893
+ } finally {
894
+ if (requestId) {
895
+ this.inferSliceAbortControllers.delete(requestId);
896
+ }
861
897
 
862
- const response = await fetch(url.toString(), {
863
- method: "POST",
864
- headers: headers,
865
- body: JSON.stringify({ imageUrl }),
866
- });
898
+ const elapsedTimeSeconds = (Date.now() - startTime) / 1000;
899
+ console.info(
900
+ `inferSlice took ${elapsedTimeSeconds}s${
901
+ requestId ? ` for request ${requestId}` : ""
902
+ }`,
903
+ );
904
+ }
905
+ }
867
906
 
868
- if (!response.ok) {
869
- throw new Error(`Failed to infer slice: ${response.statusText}`);
870
- }
907
+ async cancelInferSlice(args: {
908
+ requestId: string;
909
+ }): Promise<{ cancelled: boolean }> {
910
+ const { requestId } = args;
911
+ const abortController = this.inferSliceAbortControllers.get(requestId);
871
912
 
872
- const json = await response.json();
913
+ if (abortController) {
914
+ abortController.abort();
915
+ this.inferSliceAbortControllers.delete(requestId);
873
916
 
874
- return InferSliceResponse.parse(json);
917
+ return { cancelled: true };
875
918
  }
919
+
920
+ return { cancelled: false };
876
921
  }
877
922
  }
878
923